# frozen_string_literal: true require_relative "jmeter_perf/version" lib = File.dirname(File.absolute_path(__FILE__)) Dir.glob(File.join(lib, "jmeter_perf/report/*.rb")).each do |file| require_relative file end Dir.glob(File.join(lib, "jmeter_perf/helpers/*.rb")).each do |file| require_relative file end Dir.glob(File.join(lib, "jmeter_perf/dsl/*.rb")).each do |file| require_relative file end Dir.glob(File.join(lib, "jmeter_perf/extend/**/*.rb")).each do |file| require_relative file end Dir.glob(File.join(lib, "jmeter_perf/plugins/*.rb")).each do |file| require_relative file end # JmeterPerf module for handling performance testing with JMeter. # This module provides methods to define and execute JMeter test plans. module JmeterPerf # Evaluates the test plan with the given parameters and block. # @see https://github.com/jlurena/jmeter_perf/wiki/1.-DSL-Documentation DSL Documentation # @param params [Hash] Parameters for the test plan (default: `{}`). # @yield The block to define the test plan steps. # @return [JmeterPerf::ExtendedDSL] The DSL instance with the configured test plan. def self.test(params = {}, &block) dsl = JmeterPerf::ExtendedDSL.new(params) block_context = eval("self", block.binding, __FILE__, __LINE__) proxy_context = JmeterPerf::Helpers::FallbackContextProxy.new(dsl, block_context) begin block_context.instance_variables.each do |ivar| proxy_context.instance_variable_set(ivar, block_context.instance_variable_get(ivar)) end proxy_context.instance_eval(&block) ensure block_context.instance_variables.each do |ivar| block_context.instance_variable_set(ivar, proxy_context.instance_variable_get(ivar)) end end dsl end # DSL class for defining JMeter test plans. # Provides methods to generate, save, and run JMeter tests. class ExtendedDSL < DSL include JmeterPerf::Helpers::Parser attr_accessor :root # Initializes an ExtendedDSL object with the provided parameters. # @param params [Hash] Parameters for the test plan (default: `{}`). def initialize(params = {}) @root = Nokogiri::XML(JmeterPerf::Helpers::String.strip_heredoc( <<-EOF EOF )) node = JmeterPerf::DSL::TestPlan.new(params) @current_node = @root.at_xpath("//jmeterTestPlan/hashTree") @current_node = attach_to_last(node) end # Saves the test plan as a JMX file. # # @param out_jmx [String] The path for the output JMX file (default: `"ruby-jmeter.jmx"`). def jmx(out_jmx: "ruby-jmeter.jmx") File.write(out_jmx, doc.to_xml(indent: 2)) logger.info "JMX saved to: #{out_jmx}" end # Runs the test plan with the specified configuration. # # @param name [String] The name of the test run (default: `"ruby-jmeter"`). # @param jmeter_path [String] Path to the JMeter executable (default: `"jmeter"`). # @param out_jmx [String] The filename for the output JMX file (default: `"ruby-jmeter.jmx"`). # @param out_jtl [String] The filename for the output JTL file (default: `"jmeter.jtl"`). # @param out_jmeter_log [String] The filename for the JMeter log file (default: `"jmeter.log"`). # @param out_cmd_log [String] The filename for the command log file (default: `"jmeter-cmd.log"`). # @param jtl_read_timeout [Integer] The maximum number of seconds to wait for a line read (default: `3`). # @return [JmeterPerf::Report::Summary] The summary report of the test run. # @raise [RuntimeError] If the test execution fails. def run( name: "ruby-jmeter", jmeter_path: "jmeter", out_jmx: "ruby-jmeter.jmx", out_jtl: "jmeter.jtl", out_jmeter_log: "jmeter.log", out_cmd_log: "jmeter-cmd.log", jtl_read_timeout: 3 ) summary = nil jmx(out_jmx:) logger.warn "Executing #{out_jmx} test plan locally ..." cmd = <<~CMD.strip #{jmeter_path} -n -t #{out_jmx} -j #{out_jmeter_log} -l #{out_jtl} CMD summary = JmeterPerf::Report::Summary.new(file_path: out_jtl, name:, jtl_read_timeout:) summary.stream_jtl_async File.open(out_cmd_log, "w") do |f| pid = Process.spawn(cmd, out: f, err: [:child, :out]) Process.waitpid(pid) end summary.finish! # Notify jtl collection that JTL cmd finished and waits unless $?.exitstatus.zero? logger.error("Failed to run #{cmd}. See #{out_cmd_log} and #{out_jmeter_log} for details.") raise "Exit status: #{$?.exitstatus}" end summary.summarize_data! logger.info "[Test Plan Execution Completed Successfully] JTL saved to: #{out_jtl}\n" summary ensure summary&.finish! end private # Creates a new hash tree node for the JMeter test plan. # # @return [Nokogiri::XML::Node] A new hash tree node. def hash_tree Nokogiri::XML::Node.new("hashTree", @root) end # Attaches a node as the last child of the current node. # # @param node [Object] The node to attach. # @return [Object] The hash tree for the attached node. def attach_to_last(node) ht = hash_tree last_node = @current_node last_node << node.doc.children << ht ht end # Attaches a node and evaluates the block within its context. # # @param node [Object] The node to attach. # @yield The block to be executed in the node's context. def attach_node(node, &block) ht = attach_to_last(node) previous = @current_node @current_node = ht instance_exec(&block) if block @current_node = previous end # Returns the Nokogiri XML document. # # @return [Nokogiri::XML::Document] The XML document for the JMeter test plan. def doc Nokogiri::XML(@root.to_s, &:noblanks) end # Logger instance to log messages to the standard output. # # @return [Logger] The logger instance, initialized to DEBUG level. def logger return @logger if @logger @logger = Logger.new($stdout) @logger.level = Logger::DEBUG @logger end end end