module RubyJmeter class ExtendedDSL < DSL include Parser attr_accessor :root def initialize(params = {}) @root = Nokogiri::XML(<<-EOF.strip_heredoc) EOF node = RubyJmeter::TestPlan.new(params) @current_node = @root.at_xpath("//jmeterTestPlan/hashTree") @current_node = attach_to_last(node) end ## # Config Elements def user_defined_variables(params, &block) if params.is_a?(Hash) params['Argument.name'] = params[:name] end super end alias_method :variables, :user_defined_variables def http_request_defaults(params={}, &block) params[:image_parser] = true if params.keys.include? :download_resources params[:concurrentDwn] = true if params.keys.include? :use_concurrent_pool params[:concurrentPool] = params[:use_concurrent_pool] if params.keys.include? :use_concurrent_pool params[:embedded_url_re] = params[:urls_must_match] if params.keys.include? :urls_must_match super end alias_method :defaults, :http_request_defaults def http_cookie_manager(params={}, &block) params[:clearEachIteration] = true if params.keys.include? :clear_each_iteration super end alias_method :cookies, :http_cookie_manager def http_cache_manager(params={}, &block) params[:clearEachIteration] = true if params.keys.include? :clear_each_iteration super end alias_method :cache, :http_cache_manager def with_user_agent(device) http_header_manager name: 'User-Agent', value: RubyJmeter::UserAgent.new(device).string end def http_header_manager(params, &block) if params.is_a?(Hash) params['Header.name'] = params[:name] end super end alias_method :header, :http_header_manager alias_method :auth, :http_authorization_manager def thread_group(*args, &block) params = args.shift || {} params = { count: params }.merge(args.shift || {}) if params.class == Fixnum params[:num_threads] = params[:count] || 1 params[:ramp_time] = params[:rampup] || (params[:num_threads]/2.0).ceil params[:start_time] = params[:start_time] || Time.now.to_i * 1000 params[:end_time] = params[:end_time] || Time.now.to_i * 1000 params[:duration] ||= 60 params[:continue_forever] ||= false params[:loops] = -1 if params[:continue_forever] node = RubyJmeter::ThreadGroup.new(params) attach_node(node, &block) end alias_method :threads, :thread_group ## # HTTP Samplers def get(*args, &block) params = args.shift || {} params = { url: params }.merge(args.shift || {}) if params.class == String params[:method] ||= 'GET' params[:name] ||= params[:url] parse_http_request(params) node = RubyJmeter::HttpRequest.new(params) attach_node(node, &block) end alias_method :visit, :get def post(*args, &block) params = args.shift || {} params = { url: params }.merge(args.shift || {}) if params.class == String params[:method] ||= 'POST' params[:name] ||= params[:url] parse_http_request(params) node = RubyJmeter::HttpRequest.new(params) attach_node(node, &block) end alias_method :submit, :post def delete(*args, &block) params = args.shift || {} params = { url: params }.merge(args.shift || {}) if params.class == String params[:method] ||= 'DELETE' params[:name] ||= params[:url] parse_http_request(params) node = RubyJmeter::HttpRequest.new(params) attach_node(node, &block) end def put(*args, &block) params = args.shift || {} params = { url: params }.merge(args.shift || {}) if params.class == String params[:method] ||= 'PUT' params[:name] ||= params[:url] parse_http_request(params) node = RubyJmeter::HttpRequest.new(params) attach_node(node, &block) end def patch(*args, &block) params = args.shift || {} params = { url: params }.merge(args.shift || {}) if params.class == String params[:method] ||= 'PATCH' params[:name] ||= params[:url] parse_http_request(params) node = RubyJmeter::HttpRequest.new(params) attach_node(node, &block) end def with_xhr http_header_manager name: 'X-Requested-With', value: 'XMLHttpRequest' end def with_gzip http_header_manager name: 'Accept-Encoding', value: 'gzip, deflate' end def with_json http_header_manager name: 'Accept', value: 'text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8, application/json' end def test_data(*args, &block) params = args.shift || {} params = { key: params.to_s }.merge(args.shift || {}) if(params.class == String || params.class == Symbol) params[:command] ||= 'SRANDMEMBER' params[:name] ||= 'testdata' params[:regex] ||= '"(.+?)"' params[:match_num] ||= -1 params[:default] ||= '' params[:host] ||= '54.252.206.143' params[:url] = params[:key] if URI.parse(URI::encode(params[:key])).scheme params[:url] = if params[:host] "http://#{params[:host]}/data/#{params[:command]}/#{params[:key]}?type=text" end params[:url] = 'http://54.252.206.143/data/' if params[:stub] get name: '__testdata', url: params[:url] do extract name: params[:name], regex: params[:regex], match_num: params[:match_num], default: params[:default] end end ## # Other Samplers def soapxmlrpc_request(params, &block) params[:method] ||= 'POST' super end alias_method :soap, :soapxmlrpc_request alias_method :ldap, :ldap_request alias_method :ldap_ext, :ldap_extended_request alias_method :ldap_extended, :ldap_extended_request ## # Controllers def transaction_controller(*args, &block) params = args.shift || {} params = { name: params }.merge(args.shift || {}) if params.class == String params[:parent] = params[:parent] || false params[:includeTimers] = params[:include_timers] || false node = RubyJmeter::TransactionController.new(params) attach_node(node, &block) end alias_method :transaction, :transaction_controller def exists(variable, &block) params ||= {} params[:condition] = "\"${#{variable}}\" != \"\\${#{variable}}\"" params[:useExpression] = false params[:name] = "if ${#{variable}}" node = RubyJmeter::IfController.new(params) attach_node(node, &block) end alias_method :If, :if_controller def loop_controller(params, &block) params[:loops] = params[:count] || 1 super end alias_method :Loop, :loop_controller def throughput_controller(params, &block) params[:style] = 1 if params[:percent] params[:maxThroughput] = params[:total] || params[:percent] || 1 node = RubyJmeter::ThroughputController.new(params) node.doc.xpath(".//FloatProperty/value").first.content = params[:maxThroughput].to_f attach_node(node, &block) end alias_method :Throughput, :throughput_controller alias_method :Switch, :switch_controller alias_method :While, :while_controller alias_method :Interleave, :random_controller alias_method :Random_order, :random_order_controller alias_method :Simple, :simple_controller alias_method :Once, :once_only_controller ## # Listeners alias_method :view_results, :view_results_tree alias_method :log, :simple_data_writer alias_method :response_graph, :response_time_graph ## # Other Elements def module_controller(params, &block) node = RubyJmeter::ModuleController.new(params) if params[:test_fragment] params[:test_fragment].kind_of?(String) && params[:test_fragment].split('/') elsif params[:node_path] params[:node_path] else [] end.each_with_index do |node_name, index| node.doc.at_xpath('//collectionProp') << Nokogiri::XML(<<-EOS.strip_heredoc).children #{node_name} EOS end attach_node(node, &block) end def user_parameters(params, &block) if params.is_a?(Hash) params['Argument.name'] = params[:name] end params[:names] = Nokogiri::XML::Builder.new do |b| b.builder do params[:names].each do |name| b.stringProp name, name: name end end end params[:thread_values] = Nokogiri::XML::Builder.new do |b| b.builder do params[:thread_values].map do |user, values| b.collectionProp name: user do values.each_with_index.map do |value, index| b.stringProp value, name: index end end end end end super end alias_method :bsh_pre, :beanshell_preprocessor alias_method :bsh_post, :beanshell_postprocessor def extract(params, &block) node = if params[:regex] params[:refname] = params[:name] params[:regex] = params[:regex] #CGI.escapeHTML params[:template] = params[:template] || "$1$" RubyJmeter::RegularExpressionExtractor.new(params) elsif params[:xpath] params[:refname] = params[:name] params[:xpathQuery] = params[:xpath] RubyJmeter::XpathExtractor.new(params) elsif params[:json] params[:VAR] = params[:name] params[:JSONPATH] = params[:json] RubyJmeter::Plugins::JsonPathExtractor.new(params) end attach_node(node, &block) end alias_method :web_reg_save_param, :extract def random_timer(delay=0, range=0, &block) params={} params[:delay] = delay params[:range] = range node = RubyJmeter::GaussianRandomTimer.new(params) attach_node(node, &block) end alias_method :think_time, :random_timer def constant_throughput_timer(params, &block) params[:value] ||= params[:throughput] || 0.0 node = RubyJmeter::ConstantThroughputTimer.new(params) node.doc.xpath(".//value").first.content = params[:value].to_f attach_node(node, &block) end alias_method :ConstantThroughputTimer, :constant_throughput_timer def response_assertion(params={}, &block) params[:test_type] = parse_test_type(params) params["0"] = params.values.first node = RubyJmeter::ResponseAssertion.new(params) node.doc.xpath("//stringProp[@name='Assertion.scope']").remove if params[:scope] == 'main' || params['scope'] == 'main' attach_node(node, &block) end alias_method :assert, :response_assertion alias_method :web_reg_find, :response_assertion ## # JMeter Plugins def response_codes_per_second(name="Response Codes per Second", params={}, &block) node = RubyJmeter::Plugins::ResponseCodesPerSecond.new(name, params) attach_node(node, &block) end def response_times_distribution(name="Response Times Distribution", params={}, &block) node = RubyJmeter::Plugins::ResponseTimesDistribution.new(name, params) attach_node(node, &block) end def response_times_over_time(name="Response Times Over Time", params={}, &block) node = RubyJmeter::Plugins::ResponseTimesOverTime.new(name, params) attach_node(node, &block) end def response_times_percentiles(name="Response Times Percentiles", params={}, &block) node = RubyJmeter::Plugins::ResponseTimesPercentiles.new(name, params) attach_node(node, &block) end def transactions_per_second(name="Transactions per Second", params={}, &block) node = RubyJmeter::Plugins::TransactionsPerSecond.new(name, params) attach_node(node, &block) end def latencies_over_time(name="Response Latencies Over Time", params={}, &block) node = RubyJmeter::Plugins::LatenciesOverTime.new(name, params) attach_node(node, &block) end def console_status_logger(name="Console Status Logger", params={}, &block) node = RubyJmeter::Plugins::ConsoleStatusLogger.new(name, params) attach_node(node, &block) end alias_method :console, :console_status_logger def throughput_shaper(name="Throughput Shaping Timer", steps=[], params={}, &block) node = RubyJmeter::Plugins::ThroughputShapingTimer.new(name, steps) attach_node(node, &block) end alias_method :shaper, :throughput_shaper def dummy_sampler(name="Dummy Sampler", params={}, &block) node = RubyJmeter::Plugins::DummySampler.new(name, params) attach_node(node, &block) end alias_method :dummy, :dummy_sampler def stepping_thread_group(params={}, &block) node = RubyJmeter::Plugins::SteppingThreadGroup.new(params) attach_node(node, &block) end alias_method :step, :stepping_thread_group # API Methods def out(params={}) puts doc.to_xml(:indent => 2) end def jmx(params={}) file(params) logger.info "Test plan saved to: #{params[:file]}" end def to_xml doc.to_xml(:indent => 2) end def to_doc doc.clone end def run(params={}) file(params) logger.warn "Test executing locally ..." properties = params.has_key?(:properties) ? params[:properties] : "#{File.dirname(__FILE__)}/helpers/jmeter.properties" properties = "-q #{properties}" if properties if params[:remote_hosts] remote_hosts = params[:remote_hosts] remote_hosts = remote_hosts.join(',') if remote_hosts.kind_of?(Array) remote_hosts = "-R #{remote_hosts}" end cmd = "#{params[:path]}jmeter #{"-n" unless params[:gui] } -t #{params[:file]} -j #{params[:log] ? params[:log] : 'jmeter.log' } -l #{params[:jtl] ? params[:jtl] : 'jmeter.jtl' } #{properties} #{remote_hosts}" logger.debug cmd if params[:debug] Open3.popen2e("#{cmd}") do |stdin, stdout_err, wait_thr| while line = stdout_err.gets logger.debug line.chomp if params[:debug] end exit_status = wait_thr.value unless exit_status.success? abort "FAILED !!! #{cmd}" end end logger.info "Local Results at: #{params[:jtl] ? params[:jtl] : 'jmeter.jtl'}" end def flood(token, params={}) if params[:region] == 'local' logger.info "Starting test ..." params[:started] = Time.now run params params[:stopped] = Time.now logger.info "Completed test ..." logger.debug "Uploading results ..." if params[:debug] end RestClient.proxy = params[:proxy] if params[:proxy] begin file = Tempfile.new(['jmeter', '.jmx']) file.write(doc.to_xml(:indent => 2)) file.rewind response = RestClient.post "#{params[:endpoint] ? params[:endpoint] : 'https://api.flood.io'}/floods?auth_token=#{token}", { :flood => { :tool => 'jmeter-2.11', :url => params[:url], :name => params[:name], :notes => params[:notes], :tag_list => params[:tag_list], :threads => params[:threads], :rampup => params[:rampup], :duration => params[:duration], :override_hosts => params[:override_hosts], :override_parameters => params[:override_parameters], # specials for API :started => params[:started], :stopped => params[:stopped] }, :flood_files => { :file => File.new("#{file.path}", 'rb') }, :results => (File.new("#{params[:jtl] ? params[:jtl] : 'jmeter.jtl'}", 'rb') if params[:region] == 'local'), :region => params[:region], :multipart => true, :content_type => 'application/octet-stream' }.merge(params) if response.code == 200 logger.info "Flood results at: #{JSON.parse(response)["response"]["results"]["link"]}" else logger.fatal "Sorry there was an error: #{JSON.parse(response)["error_description"]}" end rescue => e logger.fatal "Sorry there was an error: #{JSON.parse(e.response)["error_description"]}" end end alias_method :grid, :flood private def hash_tree Nokogiri::XML::Node.new("hashTree", @root) end def attach_to_last(node) ht = hash_tree last_node = @current_node last_node << node.doc.children << ht ht end def attach_node(node, &block) ht = attach_to_last(node) previous = @current_node @current_node = ht self.instance_exec(&block) if block @current_node = previous end def file(params={}) params[:file] ||= 'jmeter.jmx' File.open(params[:file], 'w') { |file| file.write(doc.to_xml(:indent => 2)) } end def doc Nokogiri::XML(@root.to_s, &:noblanks) end def logger @log ||= Logger.new(STDOUT) @log.level = Logger::DEBUG @log end end end def test(params = {}, &block) RubyJmeter.dsl_eval(RubyJmeter::ExtendedDSL.new(params), &block) end