require "observer" require "tempfile" require 'httpclient' require 'nokogiri' module Stella class Client include Observable attr_reader :client_id attr_accessor :base_uri attr_accessor :proxy attr_reader :stats def initialize(base_uri=nil, client_id=1) @base_uri, @client_id = base_uri, client_id @cookie_file = Tempfile.new('stella-cookie') @stats = Stella::Stats.new("Client #{@client_id}") end def execute(usecase) http_client = generate_http_client container = Container.new(usecase) counter = 0 usecase.requests.each do |req| counter += 1 uri_obj = URI.parse(req.uri) params = prepare_params(usecase, req.params) uri = build_request_uri uri_obj, params, container raise NoHostDefined, uri_obj if uri.host.nil? || uri.host.empty? meth = req.http_method.to_s.downcase Stella.ld "#{meth}: " << "#{uri_obj.to_s} " << req.params.inspect changed and notify_observers(:send_request, @client_id, usecase, meth, uri, req, params, counter) begin container.response = http_client.send(meth, uri, params) # booya! changed and notify_observers(:receive_response, @client_id, usecase, meth, uri, req, params, container) rescue => ex changed and notify_observers(:request_error, @client_id, usecase, meth, uri, req, params, ex) next end ret = execute_response_handler container, req Drydock::Screen.flush if ret.kind_of?(ResponseModifier) case ret.class.to_s when "Stella::Client::Repeat" Stella.ld "REPETITION: #{counter} of #{ret.times+1}" redo if counter <= ret.times end end counter = 0 # reset run_sleeper(req.wait) if req.wait && !benchmark? end end def enable_benchmark_mode; @bm = true; end def disable_benchmark_mode; @bm = false; end def benchmark?; @bm == true; end private def run_sleeper(wait) if wait.is_a?(Range) ms = rand(wait.last * 1000).to_f ms = wait.first if ms < wait.first else ms = wait * 1000 end sleep ms / 1000 end def generate_http_client if @proxy http_client = HTTPClient.new(@proxy.uri) http_client.set_proxy_auth(@proxy.user, @proxy.pass) if @proxy.user else http_client = HTTPClient.new end http_client.set_cookie_store @cookie_file.to_s http_client end def prepare_params(usecase, params) newparams = {} params.each_pair do |n,v| v = usecase.instance_eval &v if v.is_a?(Proc) newparams[n] = v end newparams end # Testplan URIs can be relative or absolute. Either one can # contain variables in the form :varname, as in: # # http://example.com/product/:productid # # This method creates a new URI object using the @base_uri # if necessary and replaces all variables with literal values. # If no replacement value can be found, the variable is not touched. def build_request_uri(requri, params, container) uri = "" request_uri = requri.to_s if requri.host.nil? uri = base_uri.to_s uri.gsub! /\/$/, '' # Don't double up on the first slash request_uri = '/' << request_uri unless request_uri.match(/^\//) end # We call req.uri again because we need # to modify request_uri inside the loop. requri.to_s.scan(/:([a-z_]+)/i) do |instances| instances.each do |varname| val = find_replacement_value(varname, params, container) #Stella.ld "FOUND: #{val}" request_uri.gsub! /:#{varname}/, val.to_s unless val.nil? end end uri << request_uri URI.parse uri end # Testplan URIs can contain variables in the form :varname. # This method looks at the request parameters and then at the # usecase's resource hash for a replacement value. # If not found, returns nil. def find_replacement_value(name, params, container) value = nil #Stella.ld "REPLACE: #{name}" #Stella.ld "PARAMS: #{params.inspect}" #Stella.ld "IVARS: #{container.instance_variables}" value = params[name.to_sym] value = container.resource name.to_sym if value.nil? value end # Find the appropriate response handler by executing the # HTTP response status against the configured handlers. # If several match, the first one is used. def execute_response_handler(container, req) handlers = req.response.select do |regex,handler| regex = /#{regex}/ unless regex.is_a? Regexp Stella.ld "HANDLER REGEX: #{regex} (#{container.status})" container.status.to_s =~ regex end ret = nil unless handlers.empty? begin changed ret = container.instance_eval &handlers.values.first notify_observers(:execute_response_handler, @client_id, req, container) rescue => ex notify_observers(:error_execute_response_handler, @client_id, ex, req, container) Stella.ld ex.message, ex.backtrace end end ret end class Container attr_accessor :usecase attr_accessor :response def initialize(usecase) @usecase = usecase end def doc # NOTE: It's important to parse the document on every # request because this container is available for the # entire life of a usecase. case @response.header['Content-Type'] when ['text/html'] Nokogiri::HTML(body) when ['text/yaml'] YAML.load(body) end end def body; @response.body.content; end def headers; @response.header; end alias_method :header, :headers def status; @response.status; end def set(n, v); usecase.resource n, v; end def resource(n); usecase.resource n; end def wait(t); sleep t; end def repeat(t=1); Repeat.new(t); end end class ResponseModifier; end class Repeat < ResponseModifier; attr_accessor :times def initialize(times) @times = times end end end end