Skip to content

Commit 5387cb4

Browse files
committed
Clean up Ruby, tests and CI
1 parent 84e1ea9 commit 5387cb4

File tree

15 files changed

+290
-87
lines changed

15 files changed

+290
-87
lines changed

.buildkite/pipeline.yml

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
steps:
2+
- label: ":bash: Plugin"
3+
plugins:
4+
docker-compose#v2.0.0:
5+
run: plugin
6+
- label: ":ruby: Ruby"
7+
plugins:
8+
docker-compose#v2.0.0:
9+
run: ruby

LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The MIT License (MIT)
22

3-
Copyright (c) 2017 Buildkite
3+
Copyright (c) 2018 Buildkite
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

+25-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ steps:
1414
- wait: ~
1515
continue_on_failure: true
1616
- plugins:
17-
junit-annotate#v0.0.1:
17+
junit-annotate#v1.0.0:
1818
artifacts: tmp/junit-*.xml
1919
```
2020
@@ -26,6 +26,30 @@ The artifact glob path to find the JUnit XML files.
2626

2727
Example: `tmp/junit-*.xml`
2828

29+
### `job-uuid-file-pattern` (optional)
30+
31+
The regular expression (with capture group) that matches the job UUID in the junit file names. This is used to create the job links in the annotation.
32+
33+
To use this, configure your test reporter to embed the `$BUILDKITE_JOB_UUID` environment variable into your junit file names. For example `"junit-buildkite-job-$BUILDKITE_JOB_UUID.xml"`.
34+
35+
Default: `-(.*).xml`
36+
37+
## Developing
38+
39+
To test the junit parser (in Ruby) and plugin hooks (in Bash):
40+
41+
```bash
42+
docker-compose run --rm plugin &&
43+
docker-compose run --rm ruby
44+
```
45+
46+
To test the Ruby parser locally:
47+
48+
```bash
49+
cd ruby
50+
rake
51+
```
52+
2953
## License
3054

3155
MIT (see [LICENSE](LICENSE))

docker-compose.yml

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
version: '2'
22
services:
3-
tests:
3+
plugin:
44
image: buildkite/plugin-tester
55
volumes:
66
- ".:/plugin"
7+
depends_on:
8+
- ruby
9+
ruby:
10+
image: ruby:2.5-alpine
11+
command: rake
12+
working_dir: /src
13+
volumes:
14+
- "./ruby:/src"

hooks/command

+28-16
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,40 @@
22

33
set -euo pipefail
44

5-
junits_dir=$(mktemp -d -p "$(pwd)" "junits-tmp.XXXXXXXXXX")
5+
PLUGIN_DIR="$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)/.."
6+
7+
echo "--- :junit: Download the junits"
8+
9+
artifacts_dir="$(pwd)/$(mktemp -d "junit-annotate-plugin-artifacts-tmp.XXXXXXXXXX")"
10+
annotation_dir="$(pwd)/$(mktemp -d "junit-annotate-plugin-annotation-tmp.XXXXXXXXXX")"
11+
annotation_path="${annotation_dir}/annotation.md"
612

713
function cleanup {
8-
rm -rf "${junits_dir}"
14+
rm -rf "${artifacts_dir}"
15+
rm -rf "${annotation_dir}"
916
}
1017

1118
trap cleanup EXIT
1219

13-
echo "--- :junit: Download the junits"
14-
15-
buildkite-agent artifact download "${BUILDKITE_PLUGIN_JUNIT_ANNOTATE_ARTIFACTS}" junits_dir
20+
buildkite-agent artifact download \
21+
"${BUILDKITE_PLUGIN_JUNIT_ANNOTATE_ARTIFACTS}" \
22+
"$artifacts_dir"
1623

1724
echo "--- :junit: Processing the junits"
1825

19-
docker run \
20-
--rm \
21-
-v "$(pwd)/src:/usr/src/app" \
22-
-v "${junits_dir}:/junits" \
23-
ruby:2.4 /usr/src/app/run.sh > "${junits_dir}/annotation.md"
24-
25-
cat "${junits_dir}/annotation.md"
26-
27-
echo "--- :buildkite: Creating annotation"
28-
29-
buildkite-agent annotate --context junit --style error < "${junits_dir}/annotation.md"
26+
docker \
27+
--log-level "error" \
28+
run \
29+
--rm \
30+
--volume "$artifacts_dir:/junits" \
31+
--volume "$PLUGIN_DIR/ruby:/src" \
32+
--env "BUILDKITE_PLUGIN_JUNIT_ANNOTATE_JOB_UUID_FILE_PATTERN=${BUILDKITE_PLUGIN_JUNIT_ANNOTATE_JOB_UUID_FILE_PATTERN:-}" \
33+
ruby:2.5-alpine /src/bin/annotate /junits \
34+
> "$annotation_path"
35+
36+
cat "$annotation_path"
37+
38+
if ! grep -q "There were no failures" "$annotation_path"; then
39+
echo "--- :buildkite: Creating annotation"
40+
cat "$annotation_path" | buildkite-agent annotate --context junit --style error
41+
fi

ruby/Rakefile

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
require "rake/testtask"
2+
3+
Rake::TestTask.new do |t|
4+
t.test_files = FileList['tests/**/*_test.rb']
5+
end
6+
7+
task default: :test

ruby/bin/annotate

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env ruby
2+
3+
require 'rexml/document'
4+
5+
# Reads a list of junit files and returns a nice Buildkite build annotation on
6+
# STDOUT that summarizes any failures.
7+
8+
junits_dir = ARGV[0]
9+
abort("Usage: annotate <junits-dir>") unless junits_dir
10+
abort("#{junits_dir} does not exist") unless Dir.exist?(junits_dir)
11+
12+
job_pattern = ENV['BUILDKITE_PLUGIN_JUNIT_ANNOTATE_JOB_UUID_FILE_PATTERN']
13+
job_pattern = '-(.*).xml' if !job_pattern || job_pattern.empty?
14+
15+
class Failure < Struct.new(:name, :classname, :body, :job); end
16+
17+
junit_report_files = Dir.glob(File.join(junits_dir, "*"))
18+
all_failures = []
19+
20+
junit_report_files.each do |file|
21+
STDERR.puts "Parsing #{file.sub(junits_dir, '')}"
22+
job = File.basename(file)[/#{job_pattern}/, 1]
23+
xml = File.read(file)
24+
doc = REXML::Document.new(xml)
25+
26+
doc.elements.each('*/testcase') do |testcase|
27+
name = testcase.attributes['name'].to_s
28+
classname = testcase.attributes['classname'].to_s
29+
testcase.elements.each("failure") do |failure|
30+
all_failures << Failure.new(name, classname, failure.text.chomp.strip, job)
31+
end
32+
end
33+
end
34+
35+
STDERR.puts "--- ❓ Checking failures"
36+
37+
if all_failures.empty?
38+
STDERR.puts "There were no failures 🙌"
39+
exit 0
40+
else
41+
STDERR.puts "There are #{all_failures.length} failures 😭"
42+
end
43+
44+
STDERR.puts "--- ✍️ Preparing annotation"
45+
46+
puts "There were #{all_failures.length} failures:\n\n"
47+
48+
all_failures.each do |failure|
49+
puts "<details>"
50+
puts "<summary><code>#{failure.name} in #{failure.classname}</code></summary>\n\n"
51+
puts "<code><pre>#{failure.body}</pre></code>\n\n"
52+
if failure.job
53+
puts "in <a href=\"##{failure.job}\">Job ##{failure.job}</a>"
54+
end
55+
puts "</details>"
56+
puts "" unless failure == all_failures.last
57+
end

ruby/tests/annotate_test.rb

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
require 'minitest/autorun'
2+
require 'open3'
3+
4+
describe "Junit annotate plugin parser" do
5+
it "handles no failures" do
6+
output, status = Open3.capture2e("#{__dir__}/../bin/annotate", "#{__dir__}/no-test-errors/")
7+
8+
assert_equal <<~OUTPUT, output
9+
Parsing junit-1.xml
10+
Parsing junit-2.xml
11+
--- ❓ Checking failures
12+
There were no failures 🙌
13+
OUTPUT
14+
15+
assert_equal 0, status.exitstatus
16+
end
17+
18+
it "handles failures across multiple files" do
19+
output, status = Open3.capture2e("#{__dir__}/../bin/annotate", "#{__dir__}/two-test-errors/")
20+
21+
assert_equal <<~OUTPUT, output
22+
Parsing junit-1.xml
23+
Parsing junit-2.xml
24+
--- ❓ Checking failures
25+
There are 2 failures 😭
26+
--- ✍️ Preparing annotation
27+
There were 2 failures:
28+
29+
<details>
30+
<summary><code>Account#maximum_jobs_added_by_pipeline_changer returns 250 by default in spec.models.account_spec</code></summary>
31+
32+
<code><pre>Failure/Error: expect(account.maximum_jobs_added_by_pipeline_changer).to eql(250)
33+
34+
expected: 250
35+
got: 500
36+
37+
(compared using eql?)
38+
./spec/models/account_spec.rb:78:in `block (3 levels) in <top (required)>'
39+
./spec/support/database.rb:16:in `block (2 levels) in <top (required)>'
40+
./spec/support/log.rb:17:in `run'
41+
./spec/support/log.rb:66:in `block (2 levels) in <top (required)>'</pre></code>
42+
43+
in <a href="#1">Job #1</a>
44+
</details>
45+
46+
<details>
47+
<summary><code>Account#maximum_jobs_added_by_pipeline_changer returns 700 if the account is XYZ in spec.models.account_spec</code></summary>
48+
49+
<code><pre>Failure/Error: expect(account.maximum_jobs_added_by_pipeline_changer).to eql(250)
50+
51+
expected: 700
52+
got: 500
53+
54+
(compared using eql?)
55+
./spec/models/account_spec.rb:78:in `block (3 levels) in <top (required)>'
56+
./spec/support/database.rb:16:in `block (2 levels) in <top (required)>'
57+
./spec/support/log.rb:17:in `run'
58+
./spec/support/log.rb:66:in `block (2 levels) in <top (required)>'</pre></code>
59+
60+
in <a href="#2">Job #2</a>
61+
</details>
62+
OUTPUT
63+
64+
assert_equal 0, status.exitstatus
65+
end
66+
67+
it "accepts custom regex filename patterns for job id" do
68+
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/")
69+
70+
assert_equal <<~OUTPUT, output
71+
Parsing junit-123-456-custom-pattern.xml
72+
--- ❓ Checking failures
73+
There are 1 failures 😭
74+
--- ✍️ Preparing annotation
75+
There were 1 failures:
76+
77+
<details>
78+
<summary><code>Account#maximum_jobs_added_by_pipeline_changer returns 250 by default in spec.models.account_spec</code></summary>
79+
80+
<code><pre>Failure/Error: expect(account.maximum_jobs_added_by_pipeline_changer).to eql(250)
81+
82+
expected: 250
83+
got: 500
84+
85+
(compared using eql?)
86+
./spec/models/account_spec.rb:78:in `block (3 levels) in <top (required)>'
87+
./spec/support/database.rb:16:in `block (2 levels) in <top (required)>'
88+
./spec/support/log.rb:17:in `run'
89+
./spec/support/log.rb:66:in `block (2 levels) in <top (required)>'</pre></code>
90+
91+
in <a href="#123-456">Job #123-456</a>
92+
</details>
93+
OUTPUT
94+
95+
assert_equal 0, status.exitstatus
96+
end
97+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<testsuite name="rspec" tests="2" skipped="0" failures="1" errors="0" time="49.290713" timestamp="2018-04-18T23:29:42+00:00" hostname="626d214475e4">
3+
<testcase classname="spec.models.account_spec" name="Account#maximum_jobs_added_by_pipeline_changer returns 500 if the account is ABC" file="./spec/models/account_spec.rb" time="0.020013"/>
4+
<testcase classname="spec.models.account_spec" name="Account#maximum_jobs_added_by_pipeline_changer returns 250 by default" file="./spec/models/account_spec.rb" time="0.967127">
5+
<failure message=" expected: 250 got: 500 (compared using eql?) " type="RSpec::Expectations::ExpectationNotMetError">Failure/Error: expect(account.maximum_jobs_added_by_pipeline_changer).to eql(250)
6+
7+
expected: 250
8+
got: 500
9+
10+
(compared using eql?)
11+
./spec/models/account_spec.rb:78:in `block (3 levels) in &lt;top (required)&gt;&apos;
12+
./spec/support/database.rb:16:in `block (2 levels) in &lt;top (required)&gt;&apos;
13+
./spec/support/log.rb:17:in `run&apos;
14+
./spec/support/log.rb:66:in `block (2 levels) in &lt;top (required)&gt;&apos;</failure>
15+
</testcase>
16+
</testsuite>

ruby/tests/no-test-errors/junit-1.xml

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<testsuite name="rspec" tests="2" skipped="0" failures="0" errors="0" time="49.290713" timestamp="2018-04-18T23:29:42+00:00" hostname="626d214475e4">
3+
<testcase classname="spec.models.account_spec" name="Account#maximum_jobs_added_by_pipeline_changer returns 700 if the account is XYZ" file="./spec/models/account_spec.rb" time="0.020013"/>
4+
<testcase classname="spec.models.account_spec" name="Account#maximum_jobs_added_by_pipeline_changer returns 900 if the account is F00" file="./spec/models/account_spec.rb" time="0.020013"/>
5+
</testsuite>

ruby/tests/no-test-errors/junit-2.xml

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<testsuite name="rspec" tests="2" skipped="0" failures="0" errors="0" time="49.290713" timestamp="2018-04-18T23:29:42+00:00" hostname="626d214475e4">
3+
<testcase classname="spec.models.account_spec" name="Account#maximum_jobs_added_by_pipeline_changer returns 500 if the account is ABC" file="./spec/models/account_spec.rb" time="0.020013"/>
4+
<testcase classname="spec.models.account_spec" name="Account#maximum_jobs_added_by_pipeline_changer returns 250 by default" file="./spec/models/account_spec.rb" time="0.967127"/>
5+
</testsuite>
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<testsuite name="rspec" tests="2" skipped="0" failures="1" errors="0" time="49.290713" timestamp="2018-04-18T23:29:42+00:00" hostname="626d214475e4">
3+
<testcase classname="spec.models.account_spec" name="Account#maximum_jobs_added_by_pipeline_changer returns 500 if the account is ABC" file="./spec/models/account_spec.rb" time="0.020013"/>
4+
<testcase classname="spec.models.account_spec" name="Account#maximum_jobs_added_by_pipeline_changer returns 250 by default" file="./spec/models/account_spec.rb" time="0.967127">
5+
<failure message=" expected: 250 got: 500 (compared using eql?) " type="RSpec::Expectations::ExpectationNotMetError">Failure/Error: expect(account.maximum_jobs_added_by_pipeline_changer).to eql(250)
6+
7+
expected: 250
8+
got: 500
9+
10+
(compared using eql?)
11+
./spec/models/account_spec.rb:78:in `block (3 levels) in &lt;top (required)&gt;&apos;
12+
./spec/support/database.rb:16:in `block (2 levels) in &lt;top (required)&gt;&apos;
13+
./spec/support/log.rb:17:in `run&apos;
14+
./spec/support/log.rb:66:in `block (2 levels) in &lt;top (required)&gt;&apos;</failure>
15+
</testcase>
16+
</testsuite>
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<testsuite name="rspec" tests="1" skipped="0" failures="1" errors="0" time="49.290713" timestamp="2018-04-18T23:29:42+00:00" hostname="626d214475e4">
3+
<testcase classname="spec.models.account_spec" name="Account#maximum_jobs_added_by_pipeline_changer returns 700 if the account is XYZ" file="./spec/models/account_spec.rb" time="0.967127">
4+
<failure message=" expected: 700 got: 500 (compared using eql?) " type="RSpec::Expectations::ExpectationNotMetError">Failure/Error: expect(account.maximum_jobs_added_by_pipeline_changer).to eql(250)
5+
6+
expected: 700
7+
got: 500
8+
9+
(compared using eql?)
10+
./spec/models/account_spec.rb:78:in `block (3 levels) in &lt;top (required)&gt;&apos;
11+
./spec/support/database.rb:16:in `block (2 levels) in &lt;top (required)&gt;&apos;
12+
./spec/support/log.rb:17:in `run&apos;
13+
./spec/support/log.rb:66:in `block (2 levels) in &lt;top (required)&gt;&apos;</failure>
14+
</testcase>
15+
</testsuite>

0 commit comments

Comments
 (0)