better_coverage 1.0.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 80a077769534b5cd4709f7844887805d72b9f760605363cff95c9fd939eca64e
4
- data.tar.gz: 96fbc52eae5827f8dc8f2625c9834aee87160b3056079be85d8300d7f2456f7c
3
+ metadata.gz: 0a9e8806a618defa8f83d7f9c6dc59b25e7219c92e97e1e92e461dbe5e1ec497
4
+ data.tar.gz: e64c6af95687429b32040397a7b29d4505ae89b302916709183f782510516263
5
5
  SHA512:
6
- metadata.gz: 9b671abdd530d496637d69dfd2c9e1b4b0ab797a31ef47b1e10373896f1a0225d910a0585d33f030a03c049a8988862b410e104132ddee8b333b5240859681c6
7
- data.tar.gz: d43e52ef4a6df7c1210749dbb85ae17862a206883a77a73fa1c0dce9fd5ea80358b9e70647fffabd12669680b566264636452d1f1e0cd3e3ba18491808d43b27
6
+ metadata.gz: 6de3bcf36d678356f42f2d39c71dfcfdde02ee8a59079355d7d858db8cfabb97f74a5e64c7662d1c434e80dfcbffb36b08f5143cd61466fa7d90bb2864b06070
7
+ data.tar.gz: ca4b38d544b726d8581530a63a57287e011eeb086131c83edf77a87aaf0938ab504d509bb590261365e3a45fad11b47ad5a83584de19b6bc2fe5ebdf49c41bc1
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.
@@ -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
 
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MinitestPlus
4
- VERSION = '1.0.1'
4
+ VERSION = '1.1.1'
5
5
  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.1
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mridang Agarwalla
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-11-28 00:00:00.000000000 Z
11
+ date: 2026-04-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest-reporters
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.5'
19
+ version: '1.8'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.5'
26
+ version: '1.8'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: simplecov
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -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: