jmeter_perf 1.0.10 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|