require "observer"
require "tempfile"
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}")
@proxy = OpenStruct.new
end
def execute(usecase)
http_client = create_http_client
container = Container.new(usecase)
counter = 0
usecase.requests.each do |req|
counter += 1
update(:prepare_request, usecase, req, counter)
uri_obj = URI.parse(req.uri)
params = prepare_params(usecase, req.params)
headers = prepare_headers(usecase, req.headers)
uri = build_request_uri uri_obj, params, container
raise NoHostDefined, uri_obj if uri.host.nil? || uri.host.empty?
unique_id = [req, params, headers, counter].gibbler
req_id = req.gibbler
meth = req.http_method.to_s.downcase
Stella.ld "#{req.http_method}: " << "#{uri_obj.to_s} " << params.inspect
begin
update(:send_request, usecase, uri, req, params, container)
container.response = http_client.send(meth, uri, params, headers) # booya!
update(:receive_response, usecase, uri, req, params, container)
rescue => ex
update(:request_error, usecase, uri, req, params, ex)
next
end
ret = execute_response_handler container, req
Stella.lflush
# TODO: consider throw/catch
case ret.class.to_s
when "Stella::Client::Repeat"
Stella.ld "REPETITION: #{counter} of #{ret.times+1}"
redo if counter <= ret.times
when "Stella::Client::Quit"
Stella.ld "QUIT USECASE: #{ret.message}"
break
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 update(kind, *args)
changed and notify_observers(kind, @client_id, *args)
end
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 create_http_client
opts = {
:proxy => @proxy.uri || nil, # a tautology for clarity
:agent_name => "Stella/#{Stella::VERSION}",
:from => nil
}
http_client = HTTPClient.new opts
http_client.set_proxy_auth(@proxy.user, @proxy.pass) if @proxy.user
http_client.debug_dev = STDOUT if Stella.debug? && Stella.loglev > 3
http_client.set_cookie_store @cookie_file.to_s
#http_client.redirect_uri_callback = ??
http_client
end
def prepare_params(usecase, params)
newparams = {}
params.each_pair do |n,v|
Stella.ld "PREPARE PARAM: #{n}"
v = usecase.instance_eval &v if v.is_a?(Proc)
newparams[n] = v
end
newparams
end
def prepare_headers(usecase, headers)
Stella.ld "PREPARE HEADERS: #{headers}"
headers = usecase.instance_eval &headers if headers.is_a?(Proc)
headers
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)
handler = nil
req.response.each_pair do |regex,h|
Stella.ld "HANDLER REGEX: #{regex.to_s} (#{container.status})"
regex = /#{regex}/ unless regex.is_a? Regexp
handler = h and break if container.status.to_s =~ regex
end
ret = nil
unless handler.nil?
begin
ret = container.instance_eval &handler
update(:execute_response_handler, req, container)
rescue => ex
update(:error_execute_response_handler, ex, req, container)
Stella.ld ex.message, ex.backtrace
end
end
ret
end
class ResponseError < Stella::Error
def initialize(k, m=nil)
@kind, @msg = k, m
end
def message
msg = "#{@kind}"
msg << ": #{@msg}" unless @msg.nil?
msg
end
end
class Container
attr_accessor :usecase
attr_accessor :response
def initialize(usecase)
@usecase = usecase
end
def self.const_missing(const, *args)
ResponseError.new(const)
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 quit(msg=nil); Quit.new(msg); 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
class Quit < ResponseModifier;
attr_accessor :message
def initialize(msg=nil)
@message = msg
end
end
end
end