jmeter_perf 1.0.10 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +2 -2
- data/README.md +6 -1
- data/example/Gemfile.lock +1 -1
- data/lib/jmeter_perf/report/comparator.rb +91 -114
- data/lib/jmeter_perf/report/summary.rb +21 -19
- data/lib/jmeter_perf/rspec_matchers/pass_performance_test.rb +4 -10
- data/lib/jmeter_perf/rspec_matchers.rb +1 -1
- data/lib/jmeter_perf/version.rb +1 -1
- data/lib/jmeter_perf/views/report_template.html.erb +9 -9
- data/lib/jmeter_perf.rb +12 -13
- data/sig/jmeter_perf.rbs +47 -174
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 600531edf66d0d72c64c6015327f9b63db03ab11357e28758a7250aead0d9106
|
4
|
+
data.tar.gz: f254970ad87fb7850e4989d25c3f6437cb037614a76cd5c145b6c3bb5db6b4a3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 14c69cb7eac86ce588226a5c69a4b791e7d4f9015034c2314f9c13daab0a5dbeab5feeaaaaf6f58fe9be22c3216ac99e94f9d0e718cc1027ea4aa0d2e43e43a7
|
7
|
+
data.tar.gz: 2608a6d4db406a42a9bbaf2874e2cebfdb0768b62100e5515eb68a098683fc191410632aa9c314c0b17a8884405fc314499473d7335dc4d4177b3c56e8f3abfe
|
data/.rspec
CHANGED
data/README.md
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# JmeterPerf
|
2
2
|
Dynamically generate JMeter test plans, run performance tests, generate reports, compare reports and more using JMeter and Ruby.
|
3
|
+
Inspired by the now archived/defunct [ruby-jmeter](https://github.com/flood-io/ruby-jmeter).
|
3
4
|
|
4
5
|
## Installation
|
5
6
|
|
@@ -17,7 +18,11 @@ Check out the Wiki! https://github.com/jlurena/jmeter_perf/wiki
|
|
17
18
|
|
18
19
|
## Development
|
19
20
|
|
20
|
-
After checking out the repo, run `bin/setup` to install dependencies.
|
21
|
+
After checking out the repo, run `bin/setup` to install dependencies.
|
22
|
+
Run `rake` to run the linter and tests.
|
23
|
+
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
24
|
+
|
25
|
+
Additionally, there is an `example` Rails Application you can play with.
|
21
26
|
|
22
27
|
## Contributing
|
23
28
|
|
data/example/Gemfile.lock
CHANGED
@@ -25,8 +25,21 @@ module JmeterPerf
|
|
25
25
|
huge: 2.0 # huge
|
26
26
|
}
|
27
27
|
|
28
|
-
|
29
|
-
|
28
|
+
COMPARISON_REPORT_HEADER = [
|
29
|
+
"Label",
|
30
|
+
"Total Requests",
|
31
|
+
"Total Elapsed Time",
|
32
|
+
"RPM",
|
33
|
+
"Errors",
|
34
|
+
"Error %",
|
35
|
+
"Min",
|
36
|
+
"Max",
|
37
|
+
"Avg",
|
38
|
+
"SD",
|
39
|
+
"P10",
|
40
|
+
"P50",
|
41
|
+
"P95"
|
42
|
+
]
|
30
43
|
|
31
44
|
# Initializes a Comparator instance to compare two reports.
|
32
45
|
#
|
@@ -40,51 +53,80 @@ module JmeterPerf
|
|
40
53
|
compare_reports!
|
41
54
|
end
|
42
55
|
|
43
|
-
# Checks if the comparison passes based on Cohen's D and effect size.
|
56
|
+
# Checks if the comparison passes based on Cohen's D and effect size threshold.
|
44
57
|
# @note If no Cohen's D limit is provided, the `effect_size` threshold is used.
|
45
|
-
# @
|
58
|
+
# @note Positive effect size indicates an increase in performance and is considered a pass.
|
59
|
+
# @param cohens_d_limit [Float, nil] optional negative limit for Cohen's D
|
46
60
|
# @param effect_size [Symbol] the desired effect size threshold (default: :vsmall).
|
47
61
|
# See {JmeterPerf::Report::Comparator::EFFECT_SIZE_LIMITS} for options.
|
48
|
-
# @
|
49
|
-
# See {JmeterPerf::Report::Comparator::EFFECT_SIZE_DIRECTION} for options.
|
50
|
-
# @raise [ArgumentError] if the effect size or direction is invalid
|
62
|
+
# @raise [ArgumentError] if the effect size is invalid
|
51
63
|
# @return [Boolean] true if comparison meets the criteria
|
52
|
-
def pass?(cohens_d_limit: nil, effect_size: :vsmall
|
64
|
+
def pass?(cohens_d_limit: nil, effect_size: :vsmall)
|
53
65
|
limit = cohens_d_limit || EFFECT_SIZE_LIMITS[effect_size]
|
54
66
|
raise ArgumentError, "Invalid effect size: #{effect_size}" unless limit
|
55
|
-
|
56
|
-
raise ArgumentError, "Invalid direction: #{direction}" unless EFFECT_SIZE_DIRECTION.include?(direction)
|
57
|
-
|
58
|
-
case direction
|
59
|
-
when :positive
|
60
|
-
cohens_d >= limit
|
61
|
-
when :negative
|
62
|
-
cohens_d <= -limit
|
63
|
-
when :both
|
64
|
-
cohens_d >= limit || !(cohens_d <= -limit)
|
65
|
-
end
|
67
|
+
cohens_d >= -limit.abs
|
66
68
|
end
|
67
69
|
|
68
70
|
# Generates comparison reports in specified formats.
|
69
71
|
#
|
70
72
|
# @param output_dir [String] the directory for output files (default: ".")
|
71
|
-
# @param output_format [Symbol] the format for the report, e.g., :html, :csv (default: :all)
|
73
|
+
# @param output_format [Symbol] the format for the report, e.g., :html, :csv, :stdout (default: :all)
|
72
74
|
# @raise [ArgumentError] if the output format is invalid
|
73
75
|
# @return [void]
|
74
76
|
def generate_reports(output_dir: ".", output_format: :all)
|
75
|
-
generator = Generator.new(self, [@base_report, @test_report])
|
76
|
-
|
77
77
|
case output_format
|
78
78
|
when :all
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
79
|
+
generate_html_report(File.join(output_dir, "#{@name}_comparison_report.html"))
|
80
|
+
generate_csv_report(File.join(output_dir, "#{@name}_comparison_report.csv"))
|
81
|
+
print_comparison
|
82
|
+
when :html
|
83
|
+
generate_html_report(File.join(output_dir, "#{@name}_comparison_report.html"))
|
84
|
+
when :csv
|
85
|
+
generate_csv_report(File.join(output_dir, "#{@name}_comparison_report.csv"))
|
86
|
+
when :stdout
|
87
|
+
print_comparison
|
83
88
|
else
|
84
89
|
raise ArgumentError, "Invalid output format: #{output_format}"
|
85
90
|
end
|
86
91
|
end
|
87
92
|
|
93
|
+
def to_s
|
94
|
+
report_text = "Comparison Report\n"
|
95
|
+
report_text << "Cohen's D: #{@cohens_d}\n"
|
96
|
+
report_text << "Human Rating: #{@human_rating}\n"
|
97
|
+
report_text << "-" * 135 + "\n"
|
98
|
+
|
99
|
+
header_format = "%-15s %-17s %-18s %-8s %-8s %-9s %-7s %-7s %-8s %-8s %-8s %-8s %-8s\n"
|
100
|
+
row_format = "%-15s %-17d %-18d %-8.2f %-8d %-9.2f %-7d %-7d %-8.2f %-8.2f %-8.2f %-8.2f %-8.2f\n"
|
101
|
+
|
102
|
+
report_text << sprintf(header_format, *COMPARISON_REPORT_HEADER)
|
103
|
+
report_text << "-" * 135 + "\n"
|
104
|
+
|
105
|
+
[@base_report, @test_report].each_with_index do |report, index|
|
106
|
+
report_text << sprintf(row_format,
|
107
|
+
(index == 0) ? "Base Metric" : "Test Metric",
|
108
|
+
report.total_requests,
|
109
|
+
report.total_run_time,
|
110
|
+
report.rpm,
|
111
|
+
report.total_errors,
|
112
|
+
report.error_percentage,
|
113
|
+
report.min,
|
114
|
+
report.max,
|
115
|
+
report.avg,
|
116
|
+
report.std,
|
117
|
+
report.p10,
|
118
|
+
report.p50,
|
119
|
+
report.p95)
|
120
|
+
end
|
121
|
+
|
122
|
+
report_text << "-" * 135 + "\n"
|
123
|
+
report_text
|
124
|
+
end
|
125
|
+
|
126
|
+
def print_comparison
|
127
|
+
puts self
|
128
|
+
end
|
129
|
+
|
88
130
|
private
|
89
131
|
|
90
132
|
# Calculates Cohen's D and T-statistic between the two reports.
|
@@ -151,108 +193,43 @@ module JmeterPerf
|
|
151
193
|
@human_rating = "#{s_mag} #{s_dir}"
|
152
194
|
end
|
153
195
|
|
154
|
-
#
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
# Generates a report in the specified format at the given path.
|
166
|
-
#
|
167
|
-
# @param output_path [String] the path to save the generated report
|
168
|
-
# @param output_format [Symbol] the format for the report, e.g., :html or :csv
|
169
|
-
# @return [void]
|
170
|
-
def generate_report(output_path, output_format)
|
171
|
-
case output_format
|
172
|
-
when :html
|
173
|
-
generate_html_report(output_path)
|
174
|
-
when :csv
|
175
|
-
generate_csv_report(output_path)
|
176
|
-
else
|
177
|
-
print_report(output_path)
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
|
-
# Generates an HTML report.
|
182
|
-
#
|
183
|
-
# @param output_path [String] the path to save the HTML report
|
184
|
-
# @return [void]
|
185
|
-
def generate_html_report(output_path)
|
186
|
-
template_path = File.join(__dir__, "..", "views", "report_template.html.erb")
|
187
|
-
template = File.read(template_path)
|
188
|
-
result = ERB.new(template).result(binding)
|
189
|
-
File.write(output_path, result)
|
190
|
-
end
|
191
|
-
|
192
|
-
# Generates a CSV report.
|
193
|
-
#
|
194
|
-
# @param output_path [String] the path to save the CSV report
|
195
|
-
# @return [void]
|
196
|
-
def generate_csv_report(output_path)
|
197
|
-
CSV.open(output_path, "wb") do |csv|
|
198
|
-
csv << ["Label", "Total Requests", "Total Elapsed Time", "RPM", "Errors", "Error %", "Min", "Max", "Avg", "SD", "P10", "P50", "P95"]
|
199
|
-
@reports.each_with_index do |report, index|
|
200
|
-
csv << [
|
201
|
-
(index == 0) ? "Base Metric" : "Test Metric",
|
202
|
-
report.total_requests,
|
203
|
-
report.total_elapsed_time,
|
204
|
-
sprintf("%.2f", report.rpm),
|
205
|
-
report.total_errors,
|
206
|
-
sprintf("%.2f", report.error_percentage),
|
207
|
-
report.min,
|
208
|
-
report.max,
|
209
|
-
sprintf("%.2f", report.avg),
|
210
|
-
sprintf("%.2f", report.std),
|
211
|
-
sprintf("%.2f", report.p10),
|
212
|
-
sprintf("%.2f", report.p50),
|
213
|
-
sprintf("%.2f", report.p95)
|
214
|
-
]
|
215
|
-
end
|
216
|
-
end
|
217
|
-
end
|
196
|
+
# Generates an HTML report.
|
197
|
+
#
|
198
|
+
# @param output_path [String] the path to save the HTML report
|
199
|
+
# @return [void]
|
200
|
+
def generate_html_report(output_path)
|
201
|
+
template_path = File.join(__dir__, "..", "views", "report_template.html.erb")
|
202
|
+
template = File.read(template_path)
|
203
|
+
result = ERB.new(template).result(binding)
|
204
|
+
File.write(output_path, result)
|
205
|
+
end
|
218
206
|
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
report_text << format_line([
|
207
|
+
# Generates a CSV report.
|
208
|
+
#
|
209
|
+
# @param output_path [String] the path to save the CSV report
|
210
|
+
# @return [void]
|
211
|
+
def generate_csv_report(output_path)
|
212
|
+
CSV.open(output_path, "wb") do |csv|
|
213
|
+
csv << COMPARISON_REPORT_HEADER
|
214
|
+
[@base_report, @test_report].each_with_index do |report, index|
|
215
|
+
csv << [
|
229
216
|
(index == 0) ? "Base Metric" : "Test Metric",
|
230
217
|
report.total_requests,
|
218
|
+
report.total_run_time,
|
219
|
+
sprintf("%.2f", report.rpm),
|
231
220
|
report.total_errors,
|
232
221
|
sprintf("%.2f", report.error_percentage),
|
233
222
|
report.min,
|
234
|
-
report.median,
|
235
|
-
sprintf("%.2f", report.avg),
|
236
223
|
report.max,
|
224
|
+
sprintf("%.2f", report.avg),
|
237
225
|
sprintf("%.2f", report.std),
|
238
226
|
sprintf("%.2f", report.p10),
|
239
227
|
sprintf("%.2f", report.p50),
|
240
228
|
sprintf("%.2f", report.p95)
|
241
|
-
]
|
229
|
+
]
|
242
230
|
end
|
243
|
-
puts report_text
|
244
|
-
end
|
245
|
-
|
246
|
-
# Formats a line for plain text output.
|
247
|
-
#
|
248
|
-
# @param values [Array<String, Numeric>] the values to format
|
249
|
-
# @return [String] the formatted line
|
250
|
-
def format_line(values)
|
251
|
-
values.map { |v| v.to_s.ljust(10) }.join(" ") + "\n"
|
252
231
|
end
|
253
232
|
end
|
254
|
-
|
255
|
-
private_constant :Generator
|
256
233
|
end
|
257
234
|
end
|
258
235
|
end
|
@@ -32,8 +32,6 @@ module JmeterPerf
|
|
32
32
|
# @return [Float] the standard deviation of response times
|
33
33
|
# @!attribute [rw] total_bytes
|
34
34
|
# @return [Integer] the total number of bytes received
|
35
|
-
# @!attribute [rw] total_elapsed_time
|
36
|
-
# @return [Integer] the total elapsed time in milliseconds
|
37
35
|
# @!attribute [rw] total_errors
|
38
36
|
# @return [Integer] the total number of errors encountered
|
39
37
|
# @!attribute [rw] total_latency
|
@@ -88,7 +86,6 @@ module JmeterPerf
|
|
88
86
|
"Standard Deviation" => :standard_deviation,
|
89
87
|
"Total Run Time" => :total_run_time,
|
90
88
|
"Total Bytes" => :total_bytes,
|
91
|
-
"Total Elapsed Time" => :total_elapsed_time,
|
92
89
|
"Total Errors" => :total_errors,
|
93
90
|
"Total Latency" => :total_latency,
|
94
91
|
"Total Requests" => :total_requests,
|
@@ -135,7 +132,7 @@ module JmeterPerf
|
|
135
132
|
# @param file_path [String] the file path of the performance file to summarize. Either a JTL or CSV file.
|
136
133
|
# @param name [String, nil] an optional name for the summary, derived from the file path if not provided (default: nil)
|
137
134
|
# @param jtl_read_timeout [Integer] the maximum number of seconds to wait for a line read (default: 3)
|
138
|
-
def initialize(file_path:, name: nil, jtl_read_timeout:
|
135
|
+
def initialize(file_path:, name: nil, jtl_read_timeout: 30)
|
139
136
|
@name = name || file_path.to_s.tr("/", "_")
|
140
137
|
@jtl_read_timeout = jtl_read_timeout
|
141
138
|
@finished = false
|
@@ -145,7 +142,6 @@ module JmeterPerf
|
|
145
142
|
@min = 1_000_000
|
146
143
|
@response_codes = Hash.new { |h, k| h[k.to_s] = 0 }
|
147
144
|
@total_bytes = 0
|
148
|
-
@total_elapsed_time = 0
|
149
145
|
@total_errors = 0
|
150
146
|
@total_latency = 0
|
151
147
|
@total_requests = 0
|
@@ -158,12 +154,12 @@ module JmeterPerf
|
|
158
154
|
@end_time = nil
|
159
155
|
end
|
160
156
|
|
161
|
-
# Marks the summary as finished
|
157
|
+
# Marks the summary as finished and joins the processing thread.
|
162
158
|
#
|
163
159
|
# @return [void]
|
164
160
|
def finish!
|
165
161
|
@finished = true
|
166
|
-
@processing_jtl_thread
|
162
|
+
@processing_jtl_thread&.join
|
167
163
|
end
|
168
164
|
|
169
165
|
# Generates a CSV report with the given output file.
|
@@ -191,23 +187,28 @@ module JmeterPerf
|
|
191
187
|
end
|
192
188
|
|
193
189
|
# Starts streaming and processing JTL file content asynchronously.
|
194
|
-
#
|
195
|
-
# @return [
|
190
|
+
# @note Once streaming, in order to finish processing, call `finish!` otherwise it will continue indefinitely.
|
191
|
+
# @return [void]
|
196
192
|
def stream_jtl_async
|
197
193
|
@processing_jtl_thread = Thread.new do
|
198
|
-
|
194
|
+
Timeout.timeout(@jtl_read_timeout, nil, "Timed out attempting to open JTL File #{@file_path}") do
|
195
|
+
sleep 0.1 until File.exist?(@file_path) # Wait for the file to be created
|
196
|
+
end
|
199
197
|
|
200
198
|
File.open(@file_path, "r") do |file|
|
201
|
-
file.seek(0, IO::SEEK_END)
|
202
199
|
until @finished && file.eof?
|
203
200
|
line = file.gets
|
204
|
-
next unless line # Skip if no line was read
|
205
201
|
|
206
|
-
#
|
202
|
+
# Skip if the line is nil. This can happen if not @finished, and we are at EOF
|
203
|
+
next if line.nil?
|
204
|
+
# Process only if the line is complete. JMeter always finishes with a newline
|
207
205
|
read_until_complete_line(file, line)
|
208
206
|
end
|
209
207
|
end
|
210
208
|
end
|
209
|
+
|
210
|
+
@processing_jtl_thread.abort_on_exception = true
|
211
|
+
nil
|
211
212
|
end
|
212
213
|
|
213
214
|
# Summarizes the collected data by calculating statistical metrics and error rates.
|
@@ -225,17 +226,19 @@ module JmeterPerf
|
|
225
226
|
private
|
226
227
|
|
227
228
|
def read_until_complete_line(file, line)
|
228
|
-
|
229
|
-
|
230
|
-
|
229
|
+
lineno = file.lineno
|
230
|
+
return if lineno == 1 # Skip the header row
|
231
|
+
Timeout.timeout(@jtl_read_timeout, nil, "Timed out processing line #{lineno}") do
|
232
|
+
# If finished and eof but no newline: Means processing was interrupted
|
233
|
+
# JMeter always finishes with a new line in the JTL file
|
234
|
+
until line.end_with?("\n") || (file.eof? && @finished)
|
231
235
|
sleep 0.1
|
232
236
|
line += file.gets.to_s
|
233
237
|
end
|
234
238
|
end
|
235
239
|
parse_csv_row(line)
|
236
240
|
rescue Timeout::Error
|
237
|
-
|
238
|
-
raise
|
241
|
+
raise Timeout::Error, "Timed out reading JTL file at line #{lineno}"
|
239
242
|
rescue CSV::MalformedCSVError
|
240
243
|
@csv_error_lines << file.lineno
|
241
244
|
end
|
@@ -253,7 +256,6 @@ module JmeterPerf
|
|
253
256
|
# Continue with processing the row as before...
|
254
257
|
@running_statistics_helper.add_number(elapsed)
|
255
258
|
@total_requests += 1
|
256
|
-
@total_elapsed_time += elapsed
|
257
259
|
@response_codes[line_item.fetch(:responseCode)] += 1
|
258
260
|
@total_errors += (line_item.fetch(:success) == "true") ? 0 : 1
|
259
261
|
@total_bytes += line_item.fetch(:bytes, 0).to_i
|
@@ -13,26 +13,20 @@ RSpec::Matchers.define :pass_performance_test do
|
|
13
13
|
@effect_size = effect_size
|
14
14
|
end
|
15
15
|
|
16
|
-
chain :with_direction do |direction|
|
17
|
-
@direction = direction
|
18
|
-
end
|
19
|
-
|
20
16
|
chain :with_cohen_d_limit do |limit|
|
21
17
|
@cohen_limit = limit
|
22
18
|
end
|
23
19
|
|
24
20
|
chain :with do |options|
|
25
|
-
@effect_size = options[:effect_size]
|
26
|
-
@direction = options[:direction]
|
27
21
|
@cohen_limit = options[:cohen_limit]
|
22
|
+
@effect_size = options[:effect_size]
|
28
23
|
end
|
29
24
|
|
30
25
|
match do |comparator|
|
31
26
|
if comparator.is_a?(JmeterPerf::Report::Comparator)
|
32
27
|
comparator.pass?(
|
33
28
|
cohens_d_limit: @cohen_limit || nil,
|
34
|
-
effect_size: @effect_size || :vsmall
|
35
|
-
direction: @direction || :both
|
29
|
+
effect_size: @effect_size || :vsmall
|
36
30
|
)
|
37
31
|
else
|
38
32
|
false
|
@@ -41,7 +35,7 @@ RSpec::Matchers.define :pass_performance_test do
|
|
41
35
|
|
42
36
|
failure_message do |comparator|
|
43
37
|
if comparator.is_a?(JmeterPerf::Report::Comparator)
|
44
|
-
"
|
38
|
+
"Performance Test Failed\n#{comparator}"
|
45
39
|
else
|
46
40
|
"#{comparator.class.name} is not a valid comparator"
|
47
41
|
end
|
@@ -49,7 +43,7 @@ RSpec::Matchers.define :pass_performance_test do
|
|
49
43
|
|
50
44
|
failure_message_when_negated do |comparator|
|
51
45
|
if comparator.is_a?(JmeterPerf::Report::Comparator)
|
52
|
-
"
|
46
|
+
"Performance Test Passed\n#{comparator}"
|
53
47
|
else
|
54
48
|
"#{comparator.class.name} is not a valid comparator"
|
55
49
|
end
|
@@ -1 +1 @@
|
|
1
|
-
Dir.glob(File.join(__dir__, "rspec_matchers
|
1
|
+
Dir.glob(File.join(__dir__, "rspec_matchers/*.rb")).each { |f| require_relative f }
|
data/lib/jmeter_perf/version.rb
CHANGED
@@ -57,28 +57,28 @@
|
|
57
57
|
</style>
|
58
58
|
</head>
|
59
59
|
<body>
|
60
|
-
<% if @
|
61
|
-
<h2>Comparison Report for <span style="text-decoration: underline;"><%= @
|
60
|
+
<% if @name %>
|
61
|
+
<h2>Comparison Report for <span style="text-decoration: underline;"><%= @name %></span></h2>
|
62
62
|
<% end %>
|
63
63
|
<div class="summary-box" id="summary-box">
|
64
64
|
<b>Report Generated:</b> <%= Time.now.strftime("%Y-%m-%d %H:%M:%S") %><br>
|
65
|
-
<b>Cohen's D:</b> <%= @
|
66
|
-
<b>T
|
67
|
-
<b>Summary:</b> <%= @
|
65
|
+
<b>Cohen's D:</b> <%= @cohens_d %><br>
|
66
|
+
<b>T Statistic:</b> <%= @t_statistic %><br>
|
67
|
+
<b>Summary:</b> <%= @human_rating %> in performance
|
68
68
|
</div>
|
69
69
|
<table>
|
70
70
|
<tr class="header-row">
|
71
|
-
<%
|
71
|
+
<% JmeterPerf::Report::Comparator::COMPARISON_REPORT_HEADER.each do |header| %>
|
72
72
|
<td class="tooltip"><%= header %>
|
73
73
|
<span class="tooltiptext"><%= "Description for #{header}" %></span>
|
74
74
|
</td>
|
75
75
|
<% end %>
|
76
76
|
</tr>
|
77
|
-
<% @
|
77
|
+
<% [@base_report, @test_report].each_with_index do |report, index| %>
|
78
78
|
<tr>
|
79
79
|
<td><%= index == 0 ? "Base Metric" : "Test Metric" %></td>
|
80
80
|
<td><%= report.total_requests %></td>
|
81
|
-
<td><%= report.
|
81
|
+
<td><%= report.total_run_time %></td>
|
82
82
|
<td><%= sprintf('%.2f', report.rpm) %></td>
|
83
83
|
<td class="<%= 'bad' if report.total_errors > 0 %>"><%= report.total_errors %></td>
|
84
84
|
<td class="<%= 'bad' if report.error_percentage > 0.0 %>"><%= sprintf('%.2f', report.error_percentage) %></td>
|
@@ -96,7 +96,7 @@
|
|
96
96
|
import ColorScale from "https://cdn.skypack.dev/color-scales";
|
97
97
|
document.addEventListener('DOMContentLoaded', function () {
|
98
98
|
const summaryBox = document.getElementById('summary-box');
|
99
|
-
const cohenValue = <%= @
|
99
|
+
const cohenValue = <%= @cohens_d %>;
|
100
100
|
let color;
|
101
101
|
|
102
102
|
if (cohenValue === 0) {
|
data/lib/jmeter_perf.rb
CHANGED
@@ -61,7 +61,7 @@ module JmeterPerf
|
|
61
61
|
@root = Nokogiri::XML(JmeterPerf::Helpers::String.strip_heredoc(
|
62
62
|
<<-EOF
|
63
63
|
<?xml version="1.0" encoding="UTF-8"?>
|
64
|
-
<jmeterTestPlan version="1.2" properties="3.1" jmeter="3.1"
|
64
|
+
<jmeterTestPlan version="1.2" properties="3.1" jmeter="3.1" jmeterperf="#{JmeterPerf::VERSION}">
|
65
65
|
<hashTree>
|
66
66
|
</hashTree>
|
67
67
|
</jmeterTestPlan>
|
@@ -74,17 +74,17 @@ module JmeterPerf
|
|
74
74
|
|
75
75
|
# Saves the test plan as a JMX file.
|
76
76
|
#
|
77
|
-
# @param out_jmx [String] The path for the output JMX file (default: `"
|
78
|
-
def jmx(out_jmx: "
|
77
|
+
# @param out_jmx [String] The path for the output JMX file (default: `"jmeter_perf.jmx"`).
|
78
|
+
def jmx(out_jmx: "jmeter_perf.jmx")
|
79
79
|
File.write(out_jmx, doc.to_xml(indent: 2))
|
80
80
|
logger.info "JMX saved to: #{out_jmx}"
|
81
81
|
end
|
82
82
|
|
83
83
|
# Runs the test plan with the specified configuration.
|
84
84
|
#
|
85
|
-
# @param name [String] The name of the test run (default: `"
|
85
|
+
# @param name [String] The name of the test run (default: `"jmeter_perf"`).
|
86
86
|
# @param jmeter_path [String] Path to the JMeter executable (default: `"jmeter"`).
|
87
|
-
# @param out_jmx [String] The filename for the output JMX file (default: `"
|
87
|
+
# @param out_jmx [String] The filename for the output JMX file (default: `"jmeter_perf.jmx"`).
|
88
88
|
# @param out_jtl [String] The filename for the output JTL file (default: `"jmeter.jtl"`).
|
89
89
|
# @param out_jmeter_log [String] The filename for the JMeter log file (default: `"jmeter.log"`).
|
90
90
|
# @param out_cmd_log [String] The filename for the command log file (default: `"jmeter-cmd.log"`).
|
@@ -92,14 +92,15 @@ module JmeterPerf
|
|
92
92
|
# @return [JmeterPerf::Report::Summary] The summary report of the test run.
|
93
93
|
# @raise [RuntimeError] If the test execution fails.
|
94
94
|
def run(
|
95
|
-
name: "
|
95
|
+
name: "jmeter_perf",
|
96
96
|
jmeter_path: "jmeter",
|
97
|
-
out_jmx: "
|
97
|
+
out_jmx: "jmeter_perf.jmx",
|
98
98
|
out_jtl: "jmeter.jtl",
|
99
99
|
out_jmeter_log: "jmeter.log",
|
100
100
|
out_cmd_log: "jmeter-cmd.log",
|
101
101
|
jtl_read_timeout: 3
|
102
102
|
)
|
103
|
+
summary = nil
|
103
104
|
jmx(out_jmx:)
|
104
105
|
logger.warn "Executing #{out_jmx} test plan locally ..."
|
105
106
|
|
@@ -108,15 +109,14 @@ module JmeterPerf
|
|
108
109
|
CMD
|
109
110
|
|
110
111
|
summary = JmeterPerf::Report::Summary.new(file_path: out_jtl, name:, jtl_read_timeout:)
|
111
|
-
|
112
|
+
summary.stream_jtl_async
|
112
113
|
|
113
114
|
File.open(out_cmd_log, "w") do |f|
|
114
115
|
pid = Process.spawn(cmd, out: f, err: [:child, :out])
|
115
116
|
Process.waitpid(pid)
|
116
117
|
end
|
117
118
|
|
118
|
-
summary.finish! # Notify jtl collection that JTL cmd finished
|
119
|
-
jtl_process_thread.join # Join main thread and wait for it to finish
|
119
|
+
summary.finish! # Notify jtl collection that JTL cmd finished and waits
|
120
120
|
|
121
121
|
unless $?.exitstatus.zero?
|
122
122
|
logger.error("Failed to run #{cmd}. See #{out_cmd_log} and #{out_jmeter_log} for details.")
|
@@ -126,9 +126,8 @@ module JmeterPerf
|
|
126
126
|
summary.summarize_data!
|
127
127
|
logger.info "[Test Plan Execution Completed Successfully] JTL saved to: #{out_jtl}\n"
|
128
128
|
summary
|
129
|
-
|
130
|
-
summary
|
131
|
-
raise
|
129
|
+
ensure
|
130
|
+
summary&.finish!
|
132
131
|
end
|
133
132
|
|
134
133
|
private
|
data/sig/jmeter_perf.rbs
CHANGED
@@ -1,195 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JmeterPerf
|
2
|
-
def self.test: (Hash[Symbol, untyped]
|
4
|
+
def self.test: (params: Hash[Symbol, untyped] ?, &block: untyped) -> ExtendedDSL
|
3
5
|
|
4
6
|
class ExtendedDSL < DSL
|
5
|
-
include JmeterPerf::
|
6
|
-
attr_accessor root: Nokogiri::XML::Document
|
7
|
+
include JmeterPerf::Helpers::Parser
|
7
8
|
|
8
|
-
|
9
|
+
attr_accessor root: ::Nokogiri::XML::Document
|
9
10
|
|
10
|
-
def
|
11
|
+
def initialize: (params: Hash[Symbol, untyped] ?) -> void
|
12
|
+
def jmx: (out_jmx: String = "jmeter_perf.jmx") -> void
|
11
13
|
def run: (
|
12
|
-
name: String,
|
13
|
-
jmeter_path: String
|
14
|
-
out_jmx: String,
|
15
|
-
out_jtl: String,
|
16
|
-
out_jmeter_log: String,
|
17
|
-
out_cmd_log: String
|
18
|
-
|
14
|
+
name: String = "jmeter_perf",
|
15
|
+
jmeter_path: String = "jmeter",
|
16
|
+
out_jmx: String = "jmeter_perf.jmx",
|
17
|
+
out_jtl: String = "jmeter.jtl",
|
18
|
+
out_jmeter_log: String = "jmeter.log",
|
19
|
+
out_cmd_log: String = "jmeter-cmd.log",
|
20
|
+
jtl_read_timeout: Integer = 3
|
21
|
+
) -> JmeterPerf::Report::Summary
|
19
22
|
end
|
20
23
|
|
21
24
|
module Report
|
22
|
-
class Summary
|
23
|
-
attr_reader name: String
|
24
|
-
attr_reader avg: Float
|
25
|
-
attr_reader error_percentage: Float
|
26
|
-
attr_reader max: Integer
|
27
|
-
attr_reader min: Integer
|
28
|
-
attr_reader p10: Float
|
29
|
-
attr_reader p50: Float
|
30
|
-
attr_reader p95: Float
|
31
|
-
attr_reader requests_per_minute: Float
|
32
|
-
attr_reader response_codes: Hash[String, Integer]
|
33
|
-
attr_reader standard_deviation: Float
|
34
|
-
attr_reader total_bytes: Integer
|
35
|
-
attr_reader total_elapsed_time: Integer
|
36
|
-
attr_reader total_errors: Integer
|
37
|
-
attr_reader total_latency: Integer
|
38
|
-
attr_reader total_requests: Integer
|
39
|
-
attr_reader total_sent_bytes: Integer
|
40
|
-
|
41
|
-
alias rpm: requests_per_minute
|
42
|
-
alias std: standard_deviation
|
43
|
-
alias median: p50
|
44
|
-
|
45
|
-
def initialize: (String | Pathname, String?) -> void
|
46
|
-
def finish!: () -> void
|
47
|
-
def stream_jtl_async: () -> Thread
|
48
|
-
def summarize_data!: () -> void
|
49
|
-
end
|
50
|
-
|
51
25
|
class Comparator
|
52
26
|
attr_reader cohens_d: Float
|
53
27
|
attr_reader t_statistic: Float
|
54
28
|
attr_reader human_rating: String
|
55
29
|
attr_reader name: String
|
56
30
|
|
57
|
-
def initialize: (Summary, Summary, String?) -> void
|
58
|
-
def pass?: (Float?, Symbol
|
59
|
-
def generate_reports: (output_dir: String, output_format: Symbol) -> void
|
31
|
+
def initialize: (base_report: Summary, test_report: Summary, name: String ?) -> void
|
32
|
+
def pass?: (cohens_d_limit: Float ?, effect_size: Symbol = :vsmall) -> bool
|
33
|
+
def generate_reports: (output_dir: String = ".", output_format: Symbol = :all) -> void
|
34
|
+
def to_s: () -> String
|
35
|
+
def print_comparison: () -> void
|
60
36
|
end
|
61
37
|
end
|
62
38
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
def bsf_preprocessor: (Hash[Symbol, untyped], &block) -> void
|
84
|
-
def bsf_sampler: (Hash[Symbol, untyped], &block) -> void
|
85
|
-
def bsf_timer: (Hash[Symbol, untyped], &block) -> void
|
86
|
-
def bean_shell_assertion: (Hash[Symbol, untyped], &block) -> void
|
87
|
-
def bean_shell_listener: (Hash[Symbol, untyped], &block) -> void
|
88
|
-
def bean_shell_postprocessor: (Hash[Symbol, untyped], &block) -> void
|
89
|
-
def bean_shell_preprocessor: (Hash[Symbol, untyped], &block) -> void
|
90
|
-
def bean_shell_sampler: (Hash[Symbol, untyped], &block) -> void
|
91
|
-
def bean_shell_timer: (Hash[Symbol, untyped], &block) -> void
|
92
|
-
def css_jquery_extractor: (Hash[Symbol, untyped], &block) -> void
|
93
|
-
def csv_data_set_config: (Hash[Symbol, untyped], &block) -> void
|
94
|
-
def compare_assertion: (Hash[Symbol, untyped], &block) -> void
|
95
|
-
def comparison_assertion_visualizer: (Hash[Symbol, untyped], &block) -> void
|
96
|
-
def constant_throughput_timer: (Hash[Symbol, untyped], &block) -> void
|
97
|
-
def constant_timer: (Hash[Symbol, untyped], &block) -> void
|
98
|
-
def counter: (Hash[Symbol, untyped], &block) -> void
|
99
|
-
def debug_postprocessor: (Hash[Symbol, untyped], &block) -> void
|
100
|
-
def debug_sampler: (Hash[Symbol, untyped], &block) -> void
|
101
|
-
def distribution_graphalpha: (Hash[Symbol, untyped], &block) -> void
|
102
|
-
def duration_assertion: (Hash[Symbol, untyped], &block) -> void
|
103
|
-
def ftp_request: (Hash[Symbol, untyped], &block) -> void
|
104
|
-
def ftp_request_defaults: (Hash[Symbol, untyped], &block) -> void
|
105
|
-
def for_each_controller: (Hash[Symbol, untyped], &block) -> void
|
106
|
-
def gaussian_random_timer: (Hash[Symbol, untyped], &block) -> void
|
107
|
-
def generate_summary_results: (Hash[Symbol, untyped], &block) -> void
|
108
|
-
def graph_results: (Hash[Symbol, untyped], &block) -> void
|
109
|
-
def html_assertion: (Hash[Symbol, untyped], &block) -> void
|
110
|
-
def html_link_parser: (Hash[Symbol, untyped], &block) -> void
|
111
|
-
def html_parameter_mask: (Hash[Symbol, untyped], &block) -> void
|
112
|
-
def http_authorization_manager: (Hash[Symbol, untyped], &block) -> void
|
113
|
-
def http_cache_manager: (Hash[Symbol, untyped], &block) -> void
|
114
|
-
def http_cookie_manager: (Hash[Symbol, untyped], &block) -> void
|
115
|
-
def http_header_manager: (Hash[Symbol, untyped], &block) -> void
|
116
|
-
def http_request: (Hash[Symbol, untyped], &block) -> void
|
117
|
-
def http_request_defaults: (Hash[Symbol, untyped], &block) -> void
|
118
|
-
def http_url_rewriting_modifier: (Hash[Symbol, untyped], &block) -> void
|
119
|
-
def if_controller: (Hash[Symbol, untyped], &block) -> void
|
120
|
-
def include_controller: (Hash[Symbol, untyped], &block) -> void
|
121
|
-
def jdbc_connection_configuration: (Hash[Symbol, untyped], &block) -> void
|
122
|
-
def jdbc_postprocessor: (Hash[Symbol, untyped], &block) -> void
|
123
|
-
def jdbc_preprocessor: (Hash[Symbol, untyped], &block) -> void
|
124
|
-
def jdbc_request: (Hash[Symbol, untyped], &block) -> void
|
125
|
-
def jms_pointto_point: (Hash[Symbol, untyped], &block) -> void
|
126
|
-
def jms_publisher: (Hash[Symbol, untyped], &block) -> void
|
127
|
-
def jms_subscriber: (Hash[Symbol, untyped], &block) -> void
|
128
|
-
def json_path_postprocessor: (Hash[Symbol, untyped], &block) -> void
|
129
|
-
def jsr223_assertion: (Hash[Symbol, untyped], &block) -> void
|
130
|
-
def jsr223_listener: (Hash[Symbol, untyped], &block) -> void
|
131
|
-
def jsr223_postprocessor: (Hash[Symbol, untyped], &block) -> void
|
132
|
-
def jsr223_preprocessor: (Hash[Symbol, untyped], &block) -> void
|
133
|
-
def jsr223_sampler: (Hash[Symbol, untyped], &block) -> void
|
134
|
-
def jsr223_timer: (Hash[Symbol, untyped], &block) -> void
|
135
|
-
def j_unit_request: (Hash[Symbol, untyped], &block) -> void
|
136
|
-
def java_request: (Hash[Symbol, untyped], &block) -> void
|
137
|
-
def java_request_defaults: (Hash[Symbol, untyped], &block) -> void
|
138
|
-
def keystore_configuration: (Hash[Symbol, untyped], &block) -> void
|
139
|
-
def ldap_extended_request: (Hash[Symbol, untyped], &block) -> void
|
140
|
-
def ldap_extended_request_defaults: (Hash[Symbol, untyped], &block) -> void
|
141
|
-
def ldap_request: (Hash[Symbol, untyped], &block) -> void
|
142
|
-
def ldap_request_defaults: (Hash[Symbol, untyped], &block) -> void
|
143
|
-
def login_config_element: (Hash[Symbol, untyped], &block) -> void
|
144
|
-
def loop_controller: (Hash[Symbol, untyped], &block) -> void
|
145
|
-
def md5_hex_assertion: (Hash[Symbol, untyped], &block) -> void
|
146
|
-
def mail_reader_sampler: (Hash[Symbol, untyped], &block) -> void
|
147
|
-
def mailer_visualizer: (Hash[Symbol, untyped], &block) -> void
|
148
|
-
def module_controller: (Hash[Symbol, untyped], &block) -> void
|
149
|
-
def monitor_results: (Hash[Symbol, untyped], &block) -> void
|
150
|
-
def os_process_sampler: (Hash[Symbol, untyped], &block) -> void
|
151
|
-
def once_only_controller: (Hash[Symbol, untyped], &block) -> void
|
152
|
-
def poisson_random_timer: (Hash[Symbol, untyped], &block) -> void
|
153
|
-
def random_controller: (Hash[Symbol, untyped], &block) -> void
|
154
|
-
def random_order_controller: (Hash[Symbol, untyped], &block) -> void
|
155
|
-
def random_variable: (Hash[Symbol, untyped], &block) -> void
|
156
|
-
def recording_controller: (Hash[Symbol, untyped], &block) -> void
|
157
|
-
def reg_ex_user_parameters: (Hash[Symbol, untyped], &block) -> void
|
158
|
-
def regular_expression_extractor: (Hash[Symbol, untyped], &block) -> void
|
159
|
-
def response_assertion: (Hash[Symbol, untyped], &block) -> void
|
160
|
-
def response_time_graph: (Hash[Symbol, untyped], &block) -> void
|
161
|
-
def result_status_action_handler: (Hash[Symbol, untyped], &block) -> void
|
162
|
-
def runtime_controller: (Hash[Symbol, untyped], &block) -> void
|
163
|
-
def smime_assertion: (Hash[Symbol, untyped], &block) -> void
|
164
|
-
def smtp_sampler: (Hash[Symbol, untyped], &block) -> void
|
165
|
-
def soap_xml_rpc_request: (Hash[Symbol, untyped], &block) -> void
|
166
|
-
def save_responses_to_a_file: (Hash[Symbol, untyped], &block) -> void
|
167
|
-
def simple_config_element: (Hash[Symbol, untyped], &block) -> void
|
168
|
-
def simple_controller: (Hash[Symbol, untyped], &block) -> void
|
169
|
-
def simple_data_writer: (Hash[Symbol, untyped], &block) -> void
|
170
|
-
def spline_visualizer: (Hash[Symbol, untyped], &block) -> void
|
171
|
-
def summary_report: (Hash[Symbol, untyped], &block) -> void
|
172
|
-
def switch_controller: (Hash[Symbol, untyped], &block) -> void
|
173
|
-
def synchronizing_timer: (Hash[Symbol, untyped], &block) -> void
|
174
|
-
def tcp_sampler: (Hash[Symbol, untyped], &block) -> void
|
175
|
-
def tcp_sampler_config: (Hash[Symbol, untyped], &block) -> void
|
176
|
-
def test_action: (Hash[Symbol, untyped], &block) -> void
|
177
|
-
def test_fragment: (Hash[Symbol, untyped], &block) -> void
|
178
|
-
def test_plan: (Hash[Symbol, untyped], &block) -> void
|
179
|
-
def thread_group: (Hash[Symbol, untyped], &block) -> void
|
180
|
-
def throughput_controller: (Hash[Symbol, untyped], &block) -> void
|
181
|
-
def transaction_controller: (Hash[Symbol, untyped], &block) -> void
|
182
|
-
def uniform_random_timer: (Hash[Symbol, untyped], &block) -> void
|
183
|
-
def user_defined_variables: (Hash[Symbol, untyped], &block) -> void
|
184
|
-
def user_parameters: (Hash[Symbol, untyped], &block) -> void
|
185
|
-
def view_results_in_table: (Hash[Symbol, untyped], &block) -> void
|
186
|
-
def view_results_tree: (Hash[Symbol, untyped], &block) -> void
|
187
|
-
def while_controller: (Hash[Symbol, untyped], &block) -> void
|
188
|
-
def xml_assertion: (Hash[Symbol, untyped], &block) -> void
|
189
|
-
def xml_schema_assertion: (Hash[Symbol, untyped], &block) -> void
|
190
|
-
def x_path_assertion: (Hash[Symbol, untyped], &block) -> void
|
191
|
-
def x_path_extractor: (Hash[Symbol, untyped], &block) -> void
|
39
|
+
module Report
|
40
|
+
class Summary
|
41
|
+
attr_accessor avg: Float
|
42
|
+
attr_accessor error_percentage: Float
|
43
|
+
attr_accessor max: Integer
|
44
|
+
attr_accessor min: Integer
|
45
|
+
attr_accessor p10: Float
|
46
|
+
attr_accessor p50: Float
|
47
|
+
attr_accessor p95: Float
|
48
|
+
attr_accessor requests_per_minute: Float
|
49
|
+
attr_accessor response_codes: Hash[String, Integer]
|
50
|
+
attr_accessor standard_deviation: Float
|
51
|
+
attr_accessor total_bytes: Integer
|
52
|
+
attr_accessor total_errors: Integer
|
53
|
+
attr_accessor total_latency: Integer
|
54
|
+
attr_accessor total_requests: Integer
|
55
|
+
attr_accessor total_sent_bytes: Integer
|
56
|
+
attr_accessor csv_error_lines: Array[Integer]
|
57
|
+
attr_accessor total_run_time: Integer
|
58
|
+
attr_accessor name: String
|
192
59
|
|
193
|
-
|
60
|
+
def self.read: (csv_path: String) -> Summary
|
61
|
+
def initialize: (file_path: String, name: String ? = nil, jtl_read_timeout: Integer = 3) -> void
|
62
|
+
def finish!: () -> void
|
63
|
+
def write_csv: (output_file: String) -> void
|
64
|
+
def stream_jtl_async: () -> Thread
|
65
|
+
def summarize_data!: () -> void
|
66
|
+
end
|
194
67
|
end
|
195
68
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jmeter_perf
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jean Luis Urena
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-11-
|
11
|
+
date: 2024-11-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nokogiri
|