Skip to content

Commit d35eb85

Browse files
authored
Merge pull request #144 from jvdp/report-slowest
Report specified amount of slowest tests (0 by default)
2 parents 2bd625d + 717b2cb commit d35eb85

File tree

9 files changed

+137
-72
lines changed

9 files changed

+137
-72
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ Default: `junit`
5656

5757
The buildkite annotation context to use. Useful to differentiate multiple runs of this plugin in a single pipeline.
5858

59+
### `report-slowest` (optional)
60+
Default: `0`
61+
62+
Include the specified number of slowest tests in the annotation. The annotation will always be shown.
63+
5964
## Developing
6065

6166
To test the plugin hooks (in Bash) and the junit parser (in Ruby):

hooks/command

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ echo "--- :junit: Download the junits"
1010
artifacts_dir="$(pwd)/$(mktemp -d "junit-annotate-plugin-artifacts-tmp.XXXXXXXXXX")"
1111
annotation_dir="$(pwd)/$(mktemp -d "junit-annotate-plugin-annotation-tmp.XXXXXXXXXX")"
1212
annotation_path="${annotation_dir}/annotation.md"
13+
annotation_style="info"
14+
fail_build=0
1315

1416
function cleanup {
1517
rm -rf "${artifacts_dir}"
@@ -30,6 +32,7 @@ buildkite-agent artifact download \
3032

3133
echo "--- :junit: Processing the junits"
3234

35+
set +e
3336
docker \
3437
--log-level "error" \
3538
run \
@@ -38,25 +41,35 @@ docker \
3841
--volume "$PLUGIN_DIR/ruby:/src" \
3942
--env "BUILDKITE_PLUGIN_JUNIT_ANNOTATE_JOB_UUID_FILE_PATTERN=${BUILDKITE_PLUGIN_JUNIT_ANNOTATE_JOB_UUID_FILE_PATTERN:-}" \
4043
--env "BUILDKITE_PLUGIN_JUNIT_ANNOTATE_FAILURE_FORMAT=${BUILDKITE_PLUGIN_JUNIT_ANNOTATE_FAILURE_FORMAT:-}" \
44+
--env "BUILDKITE_PLUGIN_JUNIT_ANNOTATE_REPORT_SLOWEST=${BUILDKITE_PLUGIN_JUNIT_ANNOTATE_REPORT_SLOWEST:-}" \
4145
ruby:2.7-alpine ruby /src/bin/annotate /junits \
4246
> "$annotation_path"
4347

48+
if [[ $? -eq 64 ]]; then # special exit code to signal test failures
49+
annotation_style="error"
50+
if [[ "${BUILDKITE_PLUGIN_JUNIT_ANNOTATE_FAIL_BUILD_ON_ERROR:-false}" =~ (true|on|1) ]]; then
51+
fail_build=1
52+
fi
53+
fi
54+
55+
set -e
56+
4457
cat "$annotation_path"
4558

4659
if grep -q "<details>" "$annotation_path"; then
47-
4860
if ! check_size; then
4961
echo "--- :warning: Failures too large to annotate"
5062
echo "The failures are too large to create a build annotation. Please inspect the failed JUnit artifacts manually."
5163
else
5264
echo "--- :buildkite: Creating annotation"
5365
# shellcheck disable=SC2002
54-
cat "$annotation_path" | buildkite-agent annotate --context "${BUILDKITE_PLUGIN_JUNIT_ANNOTATE_CONTEXT:-junit}" --style error
66+
cat "$annotation_path" | buildkite-agent annotate --context "${BUILDKITE_PLUGIN_JUNIT_ANNOTATE_CONTEXT:-junit}" --style "$annotation_style"
5567
fi
68+
fi
5669

57-
if [[ "${BUILDKITE_PLUGIN_JUNIT_ANNOTATE_FAIL_BUILD_ON_ERROR:-false}" =~ (true|on|1) ]]
58-
then
59-
echo "--- :boom: Failing build due to error"
60-
exit 1
61-
fi
70+
if ((fail_build)); then
71+
echo "--- :boom: Failing build due to error"
72+
exit 1
73+
else
74+
exit 0
6275
fi

plugin.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ configuration:
1818
type: boolean
1919
context:
2020
type: string
21+
report-slowest:
22+
type: integer
2123
required:
2224
- artifacts
2325
additionalProperties: false

ruby/bin/annotate

Lines changed: 51 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,18 @@ job_pattern = '-(.*).xml' if !job_pattern || job_pattern.empty?
1717
failure_format = ENV['BUILDKITE_PLUGIN_JUNIT_ANNOTATE_FAILURE_FORMAT']
1818
failure_format = 'classname' if !failure_format || failure_format.empty?
1919

20-
class Failure < Struct.new(:name, :failed_test, :body, :job, :type, :message)
20+
report_slowest = ENV['BUILDKITE_PLUGIN_JUNIT_ANNOTATE_REPORT_SLOWEST'].to_i
21+
22+
class Failure < Struct.new(:name, :unit_name, :body, :job, :type, :message)
23+
end
24+
25+
class Timing < Struct.new(:name, :unit_name, :time)
2126
end
2227

2328
junit_report_files = Dir.glob(File.join(junits_dir, "**", "*"))
2429
testcases = 0
2530
failures = []
31+
timings = []
2632

2733
def text_content(element)
2834
# Handle mulptiple CDATA/text children elements
@@ -55,48 +61,65 @@ junit_report_files.sort.each do |file|
5561
REXML::XPath.each(doc, '//testsuite/testcase') do |testcase|
5662
testcases += 1
5763
name = testcase.attributes['name'].to_s
58-
failed_test = testcase.attributes[failure_format].to_s
64+
unit_name = testcase.attributes[failure_format].to_s
65+
time = testcase.attributes['time'].to_f
66+
timings << Timing.new(name, unit_name, time)
5967
testcase.elements.each("failure") do |failure|
60-
failures << Failure.new(name, failed_test, text_content(failure), job, :failure, message_content(failure))
68+
failures << Failure.new(name, unit_name, text_content(failure), job, :failure, message_content(failure))
6169
end
6270
testcase.elements.each("error") do |error|
63-
failures << Failure.new(name, failed_test, text_content(error), job, :error, message_content(error))
71+
failures << Failure.new(name, unit_name, text_content(error), job, :error, message_content(error))
6472
end
6573
end
6674
end
6775

68-
STDERR.puts "--- ❓ Checking failures"
76+
STDERR.puts "--- ✍️ Preparing annotation"
6977
STDERR.puts "#{testcases} testcases found"
7078

71-
if failures.empty?
72-
STDERR.puts "There were no failures/errors 🙌"
73-
exit 0
74-
else
79+
if failures.any?
7580
STDERR.puts "There #{failures.length == 1 ? "is 1 failure/error" : "are #{failures.length} failures/errors" } 😭"
76-
end
7781

78-
STDERR.puts "--- ✍️ Preparing annotation"
82+
failures_count = failures.select {|f| f.type == :failure }.length
83+
errors_count = failures.select {|f| f.type == :error }.length
84+
puts [
85+
failures_count == 0 ? nil : (failures_count == 1 ? "1 failure" : "#{failures_count} failures"),
86+
errors_count === 0 ? nil : (errors_count == 1 ? "1 error" : "#{errors_count} errors"),
87+
].compact.join(" and ") + ":\n\n"
88+
89+
failures.each do |failure|
90+
puts "<details>"
91+
puts "<summary><code>#{failure.name} in #{failure.unit_name}</code></summary>\n\n"
92+
if failure.message
93+
puts "<p>#{failure.message.chomp.strip}</p>\n\n"
94+
end
95+
if failure.body
96+
puts "<pre><code>#{CGI.escapeHTML(failure.body.chomp.strip)}</code></pre>\n\n"
97+
end
98+
if failure.job
99+
puts "in <a href=\"##{failure.job}\">Job ##{failure.job}</a>"
100+
end
101+
puts "</details>"
102+
puts "" unless failure == failures.last
103+
end
79104

80-
failures_count = failures.select {|f| f.type == :failure }.length
81-
errors_count = failures.select {|f| f.type == :error }.length
105+
else
106+
STDERR.puts "There were no failures/errors 🙌"
107+
end
82108

83-
puts [
84-
failures_count == 0 ? nil : (failures_count == 1 ? "1 failure" : "#{failures_count} failures"),
85-
errors_count === 0 ? nil : (errors_count == 1 ? "1 error" : "#{errors_count} errors"),
86-
].compact.join(" and ") + ":\n\n"
109+
if report_slowest > 0
110+
STDERR.puts "Reporting slowest tests ⏱"
87111

88-
failures.each do |failure|
89112
puts "<details>"
90-
puts "<summary><code>#{failure.name} in #{failure.failed_test}</code></summary>\n\n"
91-
if failure.message
92-
puts "<p>#{failure.message.chomp.strip}</p>\n\n"
93-
end
94-
if failure.body
95-
puts "<pre><code>#{CGI.escapeHTML(failure.body.chomp.strip)}</code></pre>\n\n"
96-
end
97-
if failure.job
98-
puts "in <a href=\"##{failure.job}\">Job ##{failure.job}</a>"
113+
puts "<summary>#{report_slowest} slowest tests</summary>\n\n"
114+
puts "<table>"
115+
puts "<thead><tr><th>Unit</th><th>Test</th><th>Time</th></tr></thead>"
116+
puts "<tbody>"
117+
timings.sort_by(&:time).reverse.take(report_slowest).each do |timing|
118+
puts "<tr><td>#{timing.unit_name}</td><td>#{timing.name}</td><td>#{timing.time}</td></tr>"
99119
end
120+
puts "</tbody>"
121+
puts "</table>"
100122
puts "</details>"
101-
puts "" unless failure == failures.last
102123
end
124+
125+
exit 64 if failures.any? # special exit code to signal test failures

ruby/tests/annotate_test.rb

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
Parsing junit-1.xml
1010
Parsing junit-2.xml
1111
Parsing junit-3.xml
12-
--- ❓ Checking failures
12+
--- ✍️ Preparing annotation
1313
8 testcases found
1414
There were no failures/errors 🙌
1515
OUTPUT
@@ -24,10 +24,9 @@
2424
Parsing junit-1.xml
2525
Parsing junit-2.xml
2626
Parsing junit-3.xml
27-
--- ❓ Checking failures
27+
--- ✍️ Preparing annotation
2828
6 testcases found
2929
There are 4 failures/errors 😭
30-
--- ✍️ Preparing annotation
3130
4 failures:
3231
3332
<details>
@@ -107,7 +106,7 @@
107106
</details>
108107
OUTPUT
109108

110-
assert_equal 0, status.exitstatus
109+
assert_equal 64, status.exitstatus
111110
end
112111

113112
it "handles failures and errors across multiple files" do
@@ -117,10 +116,9 @@
117116
Parsing junit-1.xml
118117
Parsing junit-2.xml
119118
Parsing junit-3.xml
120-
--- ❓ Checking failures
119+
--- ✍️ Preparing annotation
121120
6 testcases found
122121
There are 4 failures/errors 😭
123-
--- ✍️ Preparing annotation
124122
2 failures and 2 errors:
125123
126124
<details>
@@ -200,18 +198,17 @@
200198
</details>
201199
OUTPUT
202200

203-
assert_equal 0, status.exitstatus
201+
assert_equal 64, status.exitstatus
204202
end
205203

206204
it "accepts custom regex filename patterns for job id" do
207205
output, status = Open3.capture2e("env", "BUILDKITE_PLUGIN_JUNIT_ANNOTATE_JOB_UUID_FILE_PATTERN=junit-(.*)-custom-pattern.xml", "#{__dir__}/../bin/annotate", "#{__dir__}/custom-job-uuid-pattern/")
208206

209207
assert_equal <<~OUTPUT, output
210208
Parsing junit-123-456-custom-pattern.xml
211-
--- ❓ Checking failures
209+
--- ✍️ Preparing annotation
212210
2 testcases found
213211
There is 1 failure/error 😭
214-
--- ✍️ Preparing annotation
215212
1 failure:
216213
217214
<details>
@@ -234,7 +231,7 @@
234231
</details>
235232
OUTPUT
236233

237-
assert_equal 0, status.exitstatus
234+
assert_equal 64, status.exitstatus
238235
end
239236

240237
it "uses the file path instead of classname for annotation content when specified" do
@@ -244,10 +241,9 @@
244241
Parsing junit-1.xml
245242
Parsing junit-2.xml
246243
Parsing junit-3.xml
247-
--- ❓ Checking failures
244+
--- ✍️ Preparing annotation
248245
6 testcases found
249246
There are 4 failures/errors 😭
250-
--- ✍️ Preparing annotation
251247
2 failures and 2 errors:
252248
253249
<details>
@@ -327,7 +323,7 @@
327323
</details>
328324
OUTPUT
329325

330-
assert_equal 0, status.exitstatus
326+
assert_equal 64, status.exitstatus
331327
end
332328

333329
it "handles failures across multiple files in sub dirs" do
@@ -337,10 +333,9 @@
337333
Parsing sub-dir/junit-1.xml
338334
Parsing sub-dir/junit-2.xml
339335
Parsing sub-dir/junit-3.xml
340-
--- ❓ Checking failures
336+
--- ✍️ Preparing annotation
341337
6 testcases found
342338
There are 4 failures/errors 😭
343-
--- ✍️ Preparing annotation
344339
4 failures:
345340
346341
<details>
@@ -420,18 +415,17 @@
420415
</details>
421416
OUTPUT
422417

423-
assert_equal 0, status.exitstatus
418+
assert_equal 64, status.exitstatus
424419
end
425420

426421
it "handles empty failure bodies" do
427422
output, status = Open3.capture2e("#{__dir__}/../bin/annotate", "#{__dir__}/empty-failure-body/")
428423

429424
assert_equal <<~OUTPUT, output
430425
Parsing junit.xml
431-
--- ❓ Checking failures
426+
--- ✍️ Preparing annotation
432427
2 testcases found
433428
There is 1 failure/error 😭
434-
--- ✍️ Preparing annotation
435429
1 failure:
436430
437431
<details>
@@ -442,18 +436,17 @@
442436
</details>
443437
OUTPUT
444438

445-
assert_equal 0, status.exitstatus
439+
assert_equal 64, status.exitstatus
446440
end
447441

448442
it "handles missing message attributes" do
449443
output, status = Open3.capture2e("#{__dir__}/../bin/annotate", "#{__dir__}/missing-message-attribute/")
450444

451445
assert_equal <<~OUTPUT, output
452446
Parsing junit.xml
453-
--- ❓ Checking failures
447+
--- ✍️ Preparing annotation
454448
4 testcases found
455449
There are 3 failures/errors 😭
456-
--- ✍️ Preparing annotation
457450
1 failure and 2 errors:
458451
459452
<details>
@@ -472,18 +465,17 @@
472465
</details>
473466
OUTPUT
474467

475-
assert_equal 0, status.exitstatus
468+
assert_equal 64, status.exitstatus
476469
end
477470

478471
it "handles cdata formatted XML files" do
479472
output, status = Open3.capture2e("#{__dir__}/../bin/annotate", "#{__dir__}/failure-with-cdata/")
480473

481474
assert_equal <<~OUTPUT, output
482475
Parsing junit.xml
483-
--- ❓ Checking failures
476+
--- ✍️ Preparing annotation
484477
2 testcases found
485478
There is 1 failure/error 😭
486-
--- ✍️ Preparing annotation
487479
1 error:
488480
489481
<details>
@@ -497,6 +489,36 @@
497489
</details>
498490
OUTPUT
499491

492+
assert_equal 64, status.exitstatus
493+
end
494+
495+
it "reports specified amount of slowest tests" do
496+
output, status = Open3.capture2e("env", "BUILDKITE_PLUGIN_JUNIT_ANNOTATE_REPORT_SLOWEST=5", "#{__dir__}/../bin/annotate", "#{__dir__}/no-test-failures/")
497+
498+
assert_equal <<~OUTPUT, output
499+
Parsing junit-1.xml
500+
Parsing junit-2.xml
501+
Parsing junit-3.xml
502+
--- ✍️ Preparing annotation
503+
8 testcases found
504+
There were no failures/errors 🙌
505+
Reporting slowest tests ⏱
506+
<details>
507+
<summary>5 slowest tests</summary>
508+
509+
<table>
510+
<thead><tr><th>Unit</th><th>Test</th><th>Time</th></tr></thead>
511+
<tbody>
512+
<tr><td>spec.models.account_spec</td><td>Account#maximum_jobs_added_by_pipeline_changer returns 250 by default</td><td>0.977127</td></tr>
513+
<tr><td>spec.models.account_spec</td><td>Account#maximum_jobs_added_by_pipeline_changer returns 250 by default</td><td>0.967127</td></tr>
514+
<tr><td>spec.models.account_spec</td><td>Account#maximum_jobs_added_by_pipeline_changer returns 500 if the account is ABC</td><td>0.620013</td></tr>
515+
<tr><td>spec.models.account_spec</td><td>Account#maximum_jobs_added_by_pipeline_changer returns 900 if the account is F00</td><td>0.520013</td></tr>
516+
<tr><td>spec.models.account_spec</td><td>Account#maximum_jobs_added_by_pipeline_changer returns 700 if the account is XYZ</td><td>0.420013</td></tr>
517+
</tbody>
518+
</table>
519+
</details>
520+
OUTPUT
521+
500522
assert_equal 0, status.exitstatus
501523
end
502524
end

0 commit comments

Comments
 (0)