better_coverage 1.0.1 → 1.1.0
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/README.md +53 -0
- data/lib/better_coverage.rb +4 -0
- data/lib/better_junit.rb +159 -0
- data/lib/minitest_plus/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a7f5a4ce847abf8e093c1e2ec3362653d8ec1f0126ac5d8915b08895e3cea9b8
|
|
4
|
+
data.tar.gz: c952539e5001d5acbf41f60e926095d33cd266dc4afff4de93743d3fd2ed4d1c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6b95e4b3a47df45691dd33570d869cebb6e13eafcb137c3205780144e5e32e5aadd4c707f691735f0b19e7aefd58c174aa7b249adc0cc4a21880932060e6fe43
|
|
7
|
+
data.tar.gz: 8181b23f8e230550880ae6f18045dc580c7d289a4a268900be9fc091b5eb42cc2c010975ef502b03cc6c15ec6fc419894dc09fba39dddc92e989183cfc467fa9
|
data/README.md
CHANGED
|
@@ -44,6 +44,59 @@ MinitestPlus::BetterCoverage.new(
|
|
|
44
44
|
)
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
## BetterJUnit
|
|
48
|
+
|
|
49
|
+
**BetterJUnit** is a Minitest reporter that aggregates **all** test suites into a single JUnit XML file.
|
|
50
|
+
|
|
51
|
+
##### Why?
|
|
52
|
+
|
|
53
|
+
The built-in `Minitest::Reporters::JUnitReporter` writes one XML file per test class (e.g. `TEST-CalculatorTest.xml`, `TEST-StringTest.xml`, ...). Most CI test-result consumers — GitHub Actions test annotators, Buildkite test analytics, CircleCI insights, etc. — expect a **single** combined artefact. `BetterJUnit` emits exactly that: one `<testsuites>` document containing every `<testsuite>`, with the same field set as the upstream reporter (so it stays interoperable with every standard JUnit consumer).
|
|
54
|
+
|
|
55
|
+
In your `test/test_helper.rb`:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
require 'minitest/reporters'
|
|
59
|
+
require 'better_junit'
|
|
60
|
+
|
|
61
|
+
Minitest::Reporters.use! [
|
|
62
|
+
MinitestPlus::BetterJUnit.new
|
|
63
|
+
]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The combined report is written to `test/reports/junit.xml` by default.
|
|
67
|
+
|
|
68
|
+
#### Options
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
MinitestPlus::BetterJUnit.new(
|
|
72
|
+
path: 'build/reports/junit.xml', # Output file path (default: 'test/reports/junit.xml')
|
|
73
|
+
base_path: Dir.pwd, # Base for relative file paths in XML (default: Dir.pwd)
|
|
74
|
+
include_timestamp: true # Add ISO8601 timestamp per suite (default: false)
|
|
75
|
+
)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The `path` option accepts any path — nested directories are created automatically:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
MinitestPlus::BetterJUnit.new(path: 'out/deeply/nested/results/my-custom-junit.xml')
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
#### Sample output
|
|
85
|
+
|
|
86
|
+
```xml
|
|
87
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
88
|
+
<testsuites>
|
|
89
|
+
<testsuite name="CalculatorTest" filepath="test/calculator_test.rb" skipped="0" failures="0" errors="0" tests="2" assertions="2" time="0.000021">
|
|
90
|
+
<testcase name="test_addition" lineno="6" classname="CalculatorTest" assertions="1" time="0.000018" file="test/calculator_test.rb"/>
|
|
91
|
+
<testcase name="test_subtraction" lineno="10" classname="CalculatorTest" assertions="1" time="0.000003" file="test/calculator_test.rb"/>
|
|
92
|
+
</testsuite>
|
|
93
|
+
<testsuite name="StringTest" filepath="test/string_test.rb" skipped="0" failures="0" errors="0" tests="2" assertions="2" time="0.000010">
|
|
94
|
+
<testcase name="test_reverse" lineno="10" classname="StringTest" assertions="1" time="0.000007" file="test/string_test.rb"/>
|
|
95
|
+
<testcase name="test_upcase" lineno="6" classname="StringTest" assertions="1" time="0.000003" file="test/string_test.rb"/>
|
|
96
|
+
</testsuite>
|
|
97
|
+
</testsuites>
|
|
98
|
+
```
|
|
99
|
+
|
|
47
100
|
## Contributing
|
|
48
101
|
|
|
49
102
|
Contributions are welcome! If you find a bug or have suggestions for improvement, please open an issue or submit a pull request.
|
data/lib/better_coverage.rb
CHANGED
|
@@ -26,6 +26,10 @@ module MinitestPlus
|
|
|
26
26
|
result = SimpleCov.result
|
|
27
27
|
return unless result
|
|
28
28
|
|
|
29
|
+
# Calling SimpleCov.result stops its at_exit hook from running; flip it back
|
|
30
|
+
# on so the configured formatters still write their reports.
|
|
31
|
+
SimpleCov.running = true if SimpleCov.respond_to?(:running=) && !SimpleCov.running
|
|
32
|
+
|
|
29
33
|
print_coverage_table(result)
|
|
30
34
|
end
|
|
31
35
|
|
data/lib/better_junit.rb
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/reporters'
|
|
4
|
+
require 'builder'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
require 'pathname'
|
|
7
|
+
require 'time'
|
|
8
|
+
|
|
9
|
+
module MinitestPlus
|
|
10
|
+
class BetterJUnit < Minitest::Reporters::BaseReporter # rubocop:disable Metrics/ClassLength,Style/Documentation
|
|
11
|
+
DEFAULT_PATH = 'test/reports/junit.xml'
|
|
12
|
+
|
|
13
|
+
def initialize(options = {})
|
|
14
|
+
super({})
|
|
15
|
+
@path = options[:path] || DEFAULT_PATH
|
|
16
|
+
@base_path = options[:base_path] || Dir.pwd
|
|
17
|
+
@include_timestamp = options[:include_timestamp] || false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def report
|
|
21
|
+
super
|
|
22
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
23
|
+
File.write(@path, build_xml)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def build_xml
|
|
29
|
+
xml = Builder::XmlMarkup.new(indent: 2)
|
|
30
|
+
xml.instruct!
|
|
31
|
+
xml.testsuites do
|
|
32
|
+
tests.group_by { |test| test_class(test) }.each do |suite, suite_tests|
|
|
33
|
+
parse_xml_for(xml, suite, suite_tests) if suite
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
xml.target!
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def get_source_location(result)
|
|
40
|
+
result.source_location
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def get_relative_path(result)
|
|
44
|
+
file_path = Pathname.new(get_source_location(result).first)
|
|
45
|
+
base_path = Pathname.new(@base_path)
|
|
46
|
+
|
|
47
|
+
if file_path.absolute?
|
|
48
|
+
file_path.relative_path_from(base_path)
|
|
49
|
+
else
|
|
50
|
+
file_path
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# rubocop:disable Metrics/MethodLength
|
|
55
|
+
def parse_xml_for(xml, suite, tests)
|
|
56
|
+
stats = analyze_suite(tests)
|
|
57
|
+
file_path = get_relative_path(tests.first)
|
|
58
|
+
attrs = testsuite_attributes(suite, file_path, stats)
|
|
59
|
+
|
|
60
|
+
xml.testsuite(attrs) do
|
|
61
|
+
tests.each do |test|
|
|
62
|
+
xml.testcase(testcase_attributes(test, suite, file_path)) do
|
|
63
|
+
message = xml_message_for(test)
|
|
64
|
+
xml << message if message
|
|
65
|
+
xml << xml_attachment_for(test) if test.respond_to?('metadata') && test.metadata[:failure_screenshot_path]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
# rubocop:enable Metrics/MethodLength
|
|
71
|
+
|
|
72
|
+
def testsuite_attributes(suite, file_path, stats)
|
|
73
|
+
attrs = {
|
|
74
|
+
name: suite.name, filepath: file_path.to_s, skipped: stats[:skip_count],
|
|
75
|
+
failures: stats[:fail_count], errors: stats[:error_count],
|
|
76
|
+
tests: stats[:test_count], assertions: stats[:assertion_count],
|
|
77
|
+
time: stats[:time]
|
|
78
|
+
}
|
|
79
|
+
attrs[:timestamp] = stats[:timestamp] if @include_timestamp && stats[:timestamp]
|
|
80
|
+
attrs
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def testcase_attributes(test, suite, file_path)
|
|
84
|
+
{
|
|
85
|
+
name: test.name, lineno: get_source_location(test).last, classname: suite.name,
|
|
86
|
+
assertions: test.assertions, time: test.time, file: file_path.to_s
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def xml_attachment_for(test)
|
|
91
|
+
xml = Builder::XmlMarkup.new(indent: 2, margin: 2)
|
|
92
|
+
xml.tag!('system-out', "[[ATTACHMENT|#{test.metadata[:failure_screenshot_path]}]]")
|
|
93
|
+
xml.target!
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
97
|
+
def xml_message_for(test)
|
|
98
|
+
failure = test.failure
|
|
99
|
+
return nil unless failure
|
|
100
|
+
|
|
101
|
+
xml = Builder::XmlMarkup.new(indent: 2, margin: 2)
|
|
102
|
+
|
|
103
|
+
if test.skipped?
|
|
104
|
+
xml.skipped(type: failure.error.class.name)
|
|
105
|
+
elsif test.error?
|
|
106
|
+
xml.error(type: failure.error.class.name, message: trunc(failure.message)) do
|
|
107
|
+
xml.text!(message_for(test, failure) || '')
|
|
108
|
+
end
|
|
109
|
+
else
|
|
110
|
+
xml.failure(type: failure.error.class.name, message: trunc(failure.message)) do
|
|
111
|
+
xml.text!(message_for(test, failure) || '')
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
xml.target!
|
|
116
|
+
end
|
|
117
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
118
|
+
|
|
119
|
+
def trunc(text)
|
|
120
|
+
text.sub(/\n.*/m, '...')
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def message_for(test, failure)
|
|
124
|
+
suite = test.class
|
|
125
|
+
name = test.name
|
|
126
|
+
|
|
127
|
+
if test.skipped?
|
|
128
|
+
"Skipped:\n#{name}(#{suite}) [#{location(failure)}]:\n#{failure.message}\n"
|
|
129
|
+
elsif test.error?
|
|
130
|
+
"Error:\n#{name}(#{suite}):\n#{failure.message}"
|
|
131
|
+
else
|
|
132
|
+
"Failure:\n#{name}(#{suite}) [#{location(failure)}]:\n#{failure.message}\n"
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def location(failure)
|
|
137
|
+
last_before_assertion = ''
|
|
138
|
+
failure.backtrace.reverse_each do |s|
|
|
139
|
+
break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/
|
|
140
|
+
|
|
141
|
+
last_before_assertion = s
|
|
142
|
+
end
|
|
143
|
+
last_before_assertion.sub(/:in .*$/, '')
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def analyze_suite(tests)
|
|
147
|
+
stats = Hash.new(0)
|
|
148
|
+
stats[:time] = 0.0
|
|
149
|
+
tests.each do |test|
|
|
150
|
+
stats[:"#{result(test)}_count"] += 1
|
|
151
|
+
stats[:assertion_count] += test.assertions
|
|
152
|
+
stats[:test_count] += 1
|
|
153
|
+
stats[:time] += test.time
|
|
154
|
+
end
|
|
155
|
+
stats[:timestamp] = Time.now.iso8601 if @include_timestamp
|
|
156
|
+
stats
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: better_coverage
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mridang Agarwalla
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-04-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: minitest-reporters
|
|
@@ -48,6 +48,7 @@ extra_rdoc_files: []
|
|
|
48
48
|
files:
|
|
49
49
|
- README.md
|
|
50
50
|
- lib/better_coverage.rb
|
|
51
|
+
- lib/better_junit.rb
|
|
51
52
|
- lib/minitest_plus/version.rb
|
|
52
53
|
homepage: https://github.com/mridang/minitest-reporters
|
|
53
54
|
licenses:
|