simplecov-cobertura 3.1.2 → 4.0.0.rc1

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: d9b68c5aa76db30753de21b0db6f942c58e0ccc41c7f77d73881215faf7bbf31
4
- data.tar.gz: 19b8626568d444dc368bd9a4fad5747c78c0cb4583c0d44d074994e671d4ceab
3
+ metadata.gz: 416ae5fdd5d22add79d7d67afe18217d19f923c93384442ad3690259a29b8d68
4
+ data.tar.gz: f260ecd884fed84037e6de727fc141e8862df1afe497b6454cdbb3618e7dffe7
5
5
  SHA512:
6
- metadata.gz: 9cd6c9bf87b3da4df600bd3c3754ed66581802eb5d46b691119f605af7707f23a14af37fba5a67f524bf36380540f259ca2f3479ee0d651d5fd8cd0c6e0f3ffd
7
- data.tar.gz: 4308ed2698f5bd7c3afc7c319a6a231aa1ea501f312282fd73f9650062d34d7492e26e8cb86d127a779f11c2fbb89a80c06dc6638e063e3c7bdf219a2356f6f8
6
+ metadata.gz: 524aad2db646cee50a4588ee35e9c9e94db3196a08d8bd831d035293d954f60095412d5e787f03a22cd79061163ae173a28fb5b18d17af95f3c100416cfcccdb
7
+ data.tar.gz: a91b43c093b8e112aa76c1d901f8409f11f283915c04daa5d39dce42ecaf328a9b3503c0610b2ba5286db82d5ab0f61ff6596fa5f8a778728a308c2c9e1215ea
@@ -7,7 +7,7 @@
7
7
 
8
8
  name: Build
9
9
 
10
- on: [push, pull_request]
10
+ on: [ push, pull_request ]
11
11
 
12
12
  jobs:
13
13
  test:
@@ -15,7 +15,7 @@ jobs:
15
15
  runs-on: ubuntu-latest
16
16
  strategy:
17
17
  matrix:
18
- ruby-version: [2.5, 2.6, 2.7, '3.0', 3.1, 3.2, 3.3, 3.4, 4.0]
18
+ ruby-version: [ 3.1, 3.2, 3.3, 3.4, 4.0 ]
19
19
 
20
20
  steps:
21
21
  - uses: actions/checkout@v6
data/README.md CHANGED
@@ -1,10 +1,12 @@
1
- # simplecov-cobertura
2
- [![Build](https://github.com/jessebs/simplecov-cobertura/actions/workflows/build.yml/badge.svg)](https://github.com/jessebs/simplecov-cobertura/actions/workflows/build.yml) [![Gem Version](https://badge.fury.io/rb/simplecov-cobertura.svg)](http://badge.fury.io/rb/simplecov-cobertura)
1
+ # simplecov-cobertura
3
2
 
3
+ [![Build](https://github.com/jessebs/simplecov-cobertura/actions/workflows/build.yml/badge.svg)](https://github.com/jessebs/simplecov-cobertura/actions/workflows/build.yml) [![Gem Version](https://badge.fury.io/rb/simplecov-cobertura.svg)](http://badge.fury.io/rb/simplecov-cobertura)
4
4
 
5
- Produces [Cobertura](http://cobertura.sourceforge.net/) formatted XML from [SimpleCov](https://github.com/colszowka/simplecov).
5
+ Produces [Cobertura](http://cobertura.sourceforge.net/) formatted XML
6
+ from [SimpleCov](https://github.com/colszowka/simplecov).
6
7
 
7
- Output can be consumed by the [Jenkins Cobertura Plugin](https://wiki.jenkins-ci.org/display/JENKINS/Cobertura+Plugin) for easy
8
+ Output can be consumed by the [Jenkins Cobertura Plugin](https://wiki.jenkins-ci.org/display/JENKINS/Cobertura+Plugin)
9
+ for easy
8
10
  coverage visualization.
9
11
 
10
12
  ## Installation
@@ -32,9 +34,10 @@ SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter
32
34
  ```
33
35
 
34
36
  ## Continuous Integration
37
+
35
38
  Tested in a CI environment against the following Ruby versions:
36
- * 3.0 - 3.4
37
- * 2.5 - 2.7
39
+
40
+ * 3.1 - 4.0
38
41
 
39
42
  ## Contributing
40
43
 
@@ -45,6 +48,7 @@ Tested in a CI environment against the following Ruby versions:
45
48
  5. Create a new Pull Request
46
49
 
47
50
  ## License
51
+
48
52
  Copyright 2025 Jesse Bowes
49
53
 
50
54
  Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,7 +1,7 @@
1
1
  module SimpleCov
2
2
  module Formatter
3
3
  class CoberturaFormatter
4
- VERSION = '3.1.2'
4
+ VERSION = '4.0.0.rc1'
5
5
  end
6
6
  end
7
7
  end
@@ -34,126 +34,145 @@ module SimpleCov
34
34
 
35
35
  private
36
36
 
37
- def result_to_xml(result)
38
- doc = REXML::Document.new
39
- doc.context[:attribute_quote] = :quote
40
- doc << REXML::XMLDecl.new('1.0')
41
- doc << REXML::DocType.new('coverage', "SYSTEM \"#{DTD_URL}\"")
42
- doc << REXML::Comment.new("Generated by simplecov-cobertura version #{VERSION} (https://github.com/jessebs/simplecov-cobertura)")
43
- doc.add_element REXML::Element.new('coverage')
44
- coverage = doc.root
45
-
46
- set_coverage_attributes(coverage, result)
47
-
48
- coverage.add_element(sources = REXML::Element.new('sources'))
49
- sources.add_element(source = REXML::Element.new('source'))
50
- source.text = SimpleCov.root
51
-
52
- coverage.add_element(packages = REXML::Element.new('packages'))
53
-
54
- if result.groups.empty?
55
- groups = {File.basename(SimpleCov.root) => result.files}
56
- else
57
- groups = result.groups
58
- end
37
+ def result_to_xml(result)
38
+ doc = REXML::Document.new
39
+ doc.context[:attribute_quote] = :quote
40
+ doc << REXML::XMLDecl.new('1.0')
41
+ doc << REXML::DocType.new('coverage', "SYSTEM \"#{DTD_URL}\"")
42
+ doc << REXML::Comment.new("Generated by simplecov-cobertura version #{VERSION} (https://github.com/jessebs/simplecov-cobertura)")
43
+ doc.add_element REXML::Element.new('coverage')
44
+ coverage = doc.root
45
+
46
+ set_coverage_attributes(coverage, result)
47
+
48
+ coverage.add_element(sources = REXML::Element.new('sources'))
49
+ sources.add_element(source = REXML::Element.new('source'))
50
+ source.text = SimpleCov.root
51
+
52
+ coverage.add_element(packages = REXML::Element.new('packages'))
53
+
54
+ if result.groups.empty?
55
+ groups = { File.basename(SimpleCov.root) => result.files }
56
+ else
57
+ groups = result.groups
58
+ end
59
59
 
60
- groups.each do |name, files|
61
- next if files.empty?
62
- packages.add_element(package = REXML::Element.new('package'))
63
- set_package_attributes(package, name, files)
60
+ groups.each do |name, files|
61
+ next if files.empty?
62
+ packages.add_element(package = REXML::Element.new('package'))
63
+ set_package_attributes(package, name, files)
64
64
 
65
- package.add_element(classes = REXML::Element.new('classes'))
65
+ package.add_element(classes = REXML::Element.new('classes'))
66
66
 
67
- files.each do |file|
68
- classes.add_element(class_ = REXML::Element.new('class'))
69
- set_class_attributes(class_, file)
67
+ files.each do |file|
68
+ classes.add_element(class_ = REXML::Element.new('class'))
69
+ set_class_attributes(class_, file)
70
70
 
71
- class_.add_element(REXML::Element.new('methods'))
72
- class_.add_element(lines = REXML::Element.new('lines'))
71
+ class_.add_element(REXML::Element.new('methods'))
72
+ class_.add_element(lines = REXML::Element.new('lines'))
73
73
 
74
- branches_by_line = {}
75
- if SimpleCov.branch_coverage?
76
- file.branches.each do |branch|
77
- line_no = branch.start_line
78
- branches_by_line[line_no] ||= { total: 0, covered: 0 }
79
- branches_by_line[line_no][:total] += 1
80
- branches_by_line[line_no][:covered] += 1 if branch.covered?
81
- end
82
- end
74
+ branches_by_line = {}
75
+ if SimpleCov.branch_coverage?
76
+ build_branches_by_line(file, branches_by_line)
77
+ end
83
78
 
84
- file.lines.each do |file_line|
85
- if file_line.covered? || file_line.missed?
86
- lines.add_element(line = REXML::Element.new('line'))
87
- set_line_attributes(line, file_line)
88
- set_branch_attributes(line, file_line, branches_by_line) if SimpleCov.branch_coverage?
89
- end
79
+ file.lines.each do |file_line|
80
+ if file_line.covered? || file_line.missed?
81
+ lines.add_element(line = REXML::Element.new('line'))
82
+ set_line_attributes(line, file_line)
83
+ set_branch_attributes(line, file_line, branches_by_line) if SimpleCov.branch_coverage?
90
84
  end
91
85
  end
92
86
  end
93
-
94
- doc
95
87
  end
96
88
 
97
- def set_coverage_attributes(coverage, result)
98
- ls = result.coverage_statistics[:line]
99
- bs = result.coverage_statistics[:branch]
100
-
101
- coverage.attributes['line-rate'] = extract_rate(ls.percent)
102
- coverage.attributes['lines-covered'] = ls.covered.to_s.to_s
103
- coverage.attributes['lines-valid'] = ls.total.to_s.to_s
104
- if SimpleCov.branch_coverage?
105
- coverage.attributes['branches-covered'] = bs.covered.to_s
106
- coverage.attributes['branches-valid'] = bs.total.to_s
107
- coverage.attributes['branch-rate'] = extract_rate(bs.percent)
108
- end
109
- coverage.attributes['complexity'] = '0'
110
- coverage.attributes['version'] = '0'
111
- coverage.attributes['timestamp'] = Time.now.to_i.to_s
89
+ doc
90
+ end
91
+
92
+ def set_coverage_attributes(coverage, result)
93
+ ls = result.coverage_statistics[:line]
94
+ bs = result.coverage_statistics[:branch]
95
+
96
+ coverage.attributes['line-rate'] = extract_rate(ls.percent)
97
+ coverage.attributes['lines-covered'] = ls.covered.to_s.to_s
98
+ coverage.attributes['lines-valid'] = ls.total.to_s.to_s
99
+ if SimpleCov.branch_coverage?
100
+ coverage.attributes['branches-covered'] = bs.covered.to_s
101
+ coverage.attributes['branches-valid'] = bs.total.to_s
102
+ coverage.attributes['branch-rate'] = extract_rate(bs.percent)
112
103
  end
104
+ coverage.attributes['complexity'] = '0'
105
+ coverage.attributes['version'] = '0'
106
+ coverage.attributes['timestamp'] = Time.now.to_i.to_s
107
+ end
113
108
 
114
- def set_package_attributes(package, name, result)
115
- ls = result.coverage_statistics[:line]
116
- bs = result.coverage_statistics[:branch]
109
+ def set_package_attributes(package, name, result)
110
+ ls = result.coverage_statistics[:line]
111
+ bs = result.coverage_statistics[:branch]
117
112
 
118
- package.attributes['name'] = name
119
- package.attributes['line-rate'] = extract_rate(ls.percent)
120
- if SimpleCov.branch_coverage?
121
- package.attributes['branch-rate'] = extract_rate(bs.percent)
122
- end
123
- package.attributes['complexity'] = '0'
113
+ package.attributes['name'] = name
114
+ package.attributes['line-rate'] = extract_rate(ls.percent)
115
+ if SimpleCov.branch_coverage?
116
+ package.attributes['branch-rate'] = extract_rate(bs.percent)
124
117
  end
118
+ package.attributes['complexity'] = '0'
119
+ end
125
120
 
126
- def set_class_attributes(class_, file)
127
- ls = file.coverage_statistics[:line]
128
- bs = file.coverage_statistics[:branch]
121
+ def set_class_attributes(class_, file)
122
+ ls = file.coverage_statistics[:line]
123
+ bs = file.coverage_statistics[:branch]
129
124
 
130
- filename = file.filename
131
- class_.attributes['name'] = resolve_filename(filename)
132
- class_.attributes['filename'] = resolve_filename(filename)
133
- class_.attributes['line-rate'] = extract_rate(ls.percent)
134
- if SimpleCov.branch_coverage?
135
- class_.attributes['branch-rate'] = extract_rate(bs.percent)
136
- end
137
- class_.attributes['complexity'] = '0'
125
+ filename = file.filename
126
+ class_.attributes['name'] = resolve_filename(filename)
127
+ class_.attributes['filename'] = resolve_filename(filename)
128
+ class_.attributes['line-rate'] = extract_rate(ls.percent)
129
+ if SimpleCov.branch_coverage?
130
+ class_.attributes['branch-rate'] = extract_rate(bs.percent)
138
131
  end
132
+ class_.attributes['complexity'] = '0'
133
+ end
139
134
 
140
- def set_line_attributes(line, file_line)
141
- line.attributes['number'] = file_line.line_number.to_s
142
- line.attributes['hits'] = file_line.coverage.to_s
135
+ def set_line_attributes(line, file_line)
136
+ line.attributes['number'] = file_line.line_number.to_s
137
+ line.attributes['hits'] = file_line.coverage.to_s
138
+ end
139
+
140
+ def set_branch_attributes(line, file_line, branches_by_line)
141
+ branch_info = branches_by_line[file_line.line_number]
142
+ if branch_info
143
+ total = branch_info[:total]
144
+ covered = branch_info[:covered]
145
+ pct_coverage = total > 0 ? (covered * 100 / total) : 0
146
+ line.attributes['branch'] = 'true'
147
+ line.attributes['condition-coverage'] = "#{pct_coverage}% (#{covered}/#{total})"
148
+ else
149
+ line.attributes['branch'] = 'false'
143
150
  end
151
+ end
144
152
 
145
- def set_branch_attributes(line, file_line, branches_by_line)
146
- branch_info = branches_by_line[file_line.number]
147
- if branch_info
148
- total = branch_info[:total]
149
- covered = branch_info[:covered]
150
- pct_coverage = total > 0 ? (covered * 100 / total) : 0
151
- line.attributes['branch'] = 'true'
152
- line.attributes['condition-coverage'] = "#{pct_coverage}% (#{covered}/#{total})"
153
- else
154
- line.attributes['branch'] = 'false'
153
+ def build_branches_by_line(file, branches_by_line)
154
+ file.coverage_data.fetch("branches", {}).each do |condition, branches|
155
+ line = condition_start_line(condition)
156
+ next unless line
157
+
158
+ info = branches_by_line[line] ||= { total: 0, covered: 0 }
159
+ branches.each_value do |hit_count|
160
+ info[:total] += 1
161
+ info[:covered] += 1 if hit_count.to_i > 0
155
162
  end
156
163
  end
164
+ rescue StandardError => e
165
+ warn "simplecov-cobertura: couldn't extract per-line branch detail " \
166
+ "(#{e.class}: #{e.message}); lines will report branch=\"false\""
167
+ branches_by_line.clear
168
+ end
169
+
170
+ def condition_start_line(condition)
171
+ return condition[2] if condition.is_a?(Array)
172
+ return nil unless condition.is_a?(String)
173
+
174
+ condition.scan(/-?\d+/)[1]&.to_i # [id, start_line, ...]
175
+ end
157
176
 
158
177
  # Roughly mirrors private method SimpleCov::Formatter::HTMLFormatter#output_coverage
159
178
  def output_message(result, output_path)
@@ -163,17 +182,17 @@ module SimpleCov
163
182
  output
164
183
  end
165
184
 
166
- def resolve_filename(filename)
167
- Pathname.new(filename).relative_path_from(project_root).to_s
168
- end
185
+ def resolve_filename(filename)
186
+ Pathname.new(filename).relative_path_from(project_root).to_s
187
+ end
169
188
 
170
- def extract_rate(percent)
171
- (percent / 100).round(4).to_s
172
- end
189
+ def extract_rate(percent)
190
+ (percent / 100).round(4).to_s
191
+ end
173
192
 
174
- def project_root
175
- @project_root ||= Pathname.new(SimpleCov.root)
176
- end
193
+ def project_root
194
+ @project_root ||= Pathname.new(SimpleCov.root)
195
+ end
177
196
  end
178
197
  end
179
198
  end
@@ -5,25 +5,25 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require 'simplecov-cobertura/version'
6
6
 
7
7
  Gem::Specification.new do |spec|
8
- spec.name = 'simplecov-cobertura'
9
- spec.version = SimpleCov::Formatter::CoberturaFormatter::VERSION
10
- spec.authors = ['Jesse Bowes']
11
- spec.email = ['jessebowes@acm.org']
12
- spec.summary = 'SimpleCov Cobertura Formatter'
13
- spec.description = 'Produces Cobertura XML formatted output from SimpleCov'
14
- spec.homepage = 'https://github.com/jessebs/simplecov-cobertura'
15
- spec.license = 'Apache-2.0'
16
- spec.required_ruby_version = '>= 2.5.0'
8
+ spec.name = 'simplecov-cobertura'
9
+ spec.version = SimpleCov::Formatter::CoberturaFormatter::VERSION
10
+ spec.authors = ['Jesse Bowes']
11
+ spec.email = ['jessebowes@acm.org']
12
+ spec.summary = 'SimpleCov Cobertura Formatter'
13
+ spec.description = 'Produces Cobertura XML formatted output from SimpleCov'
14
+ spec.homepage = 'https://github.com/jessebs/simplecov-cobertura'
15
+ spec.license = 'Apache-2.0'
16
+ spec.required_ruby_version = '>= 3.1'
17
17
 
18
- spec.files = `git ls-files -z`.split("\x0")
19
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.files = `git ls-files -z`.split("\x0")
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
21
  spec.require_paths = ['lib']
22
22
 
23
23
  spec.add_development_dependency 'test-unit', '~> 3.2'
24
24
  spec.add_development_dependency 'nokogiri', '~> 1.0'
25
25
  spec.add_development_dependency 'rake', '~> 13.0'
26
26
 
27
- spec.add_dependency 'simplecov', '~> 0.19'
27
+ spec.add_dependency 'simplecov', '>= 1.0.0.rc2', '< 2.0'
28
28
  spec.add_dependency 'rexml'
29
29
  end
@@ -0,0 +1,28 @@
1
+ class Sample
2
+ def greet(name)
3
+ if name.nil?
4
+ "Hello, stranger!"
5
+ else
6
+ "Hello, #{name}!"
7
+ end
8
+ end
9
+
10
+ def absolute(n)
11
+ if n < 0
12
+ -n
13
+ else
14
+ n
15
+ end
16
+ end
17
+
18
+ def unused_method
19
+ x = 1
20
+ y = 2
21
+ z = x + y
22
+ if z > 0
23
+ "positive"
24
+ else
25
+ "non-positive"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,17 @@
1
+ $LOAD_PATH.unshift(File.join(ENV.fetch('GEM_ROOT'), 'lib'))
2
+
3
+ require 'simplecov'
4
+ require 'simplecov-cobertura'
5
+
6
+ SimpleCov.command_name ENV.fetch('COMMAND_NAME', 'Unit Tests')
7
+ SimpleCov.start do
8
+ enable_coverage :branch
9
+ use_merging true
10
+ merge_timeout 3600 # generous so slow CI can't expire run A's result
11
+ root ENV.fetch('PROJECT_ROOT')
12
+ coverage_dir 'coverage'
13
+ formatter SimpleCov::Formatter::CoberturaFormatter
14
+ end
15
+
16
+ require File.join(ENV.fetch('PROJECT_ROOT'), 'lib', 'sample')
17
+ require 'test/unit'
@@ -0,0 +1,11 @@
1
+ require_relative 'simplecov_setup'
2
+
3
+ class MergedATest < Test::Unit::TestCase
4
+ def test_greet_with_name
5
+ assert_equal 'Hello, World!', Sample.new.greet('World') # greet: else branch
6
+ end
7
+
8
+ def test_absolute_positive
9
+ assert_equal 5, Sample.new.absolute(5) # absolute: else branch
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ require_relative 'simplecov_setup'
2
+
3
+ class MergedBTest < Test::Unit::TestCase
4
+ def test_greet_without_name
5
+ assert_equal 'Hello, stranger!', Sample.new.greet(nil) # greet: then branch
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ require_relative 'simplecov_setup'
2
+
3
+ class SampleTest < Test::Unit::TestCase
4
+ def test_greet_with_name
5
+ assert_equal "Hello, World!", Sample.new.greet("World")
6
+ end
7
+
8
+ def test_absolute_positive
9
+ # Only exercises the else branch (n >= 0), never the then branch (n < 0)
10
+ assert_equal 5, Sample.new.absolute(5)
11
+ end
12
+ end
@@ -0,0 +1,175 @@
1
+ require 'test/unit'
2
+ require 'tmpdir'
3
+ require 'open3'
4
+ require 'nokogiri'
5
+ require 'fileutils'
6
+
7
+ class IntegrationTest < Test::Unit::TestCase
8
+ FIXTURES_DIR = File.join(__dir__, 'fixtures')
9
+ GEM_ROOT = File.expand_path('..', __dir__)
10
+
11
+ def setup
12
+ @tmpdir = Dir.mktmpdir('simplecov-cobertura-integration')
13
+ end
14
+
15
+ def teardown
16
+ FileUtils.remove_entry(@tmpdir) if @tmpdir && Dir.exist?(@tmpdir)
17
+ end
18
+
19
+ def test_clean_load_require_and_assign_formatter
20
+ # In a fresh subprocess, require simplecov-cobertura and assign the formatter.
21
+ # This catches missing requires (e.g. simplecov, stringio).
22
+ code = <<~RUBY
23
+ require 'simplecov-cobertura'
24
+ SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter
25
+ RUBY
26
+ stdout, stderr, status = Open3.capture3('ruby', '-e', code)
27
+ assert status.success?, "Clean-load subprocess failed.\nstdout: #{stdout}\nstderr: #{stderr}"
28
+ end
29
+
30
+ def test_real_coverage_run_produces_valid_cobertura_xml
31
+ install_fixture('sample')
32
+
33
+ # Run the sample test in a subprocess.
34
+ stdout, stderr, status = run_fixture_test('test/test_sample.rb')
35
+ assert status.success?, "Sample test subprocess failed.\nstdout: #{stdout}\nstderr: #{stderr}"
36
+
37
+ # --- Assert on the generated coverage.xml ---
38
+ coverage_xml_path = File.join(@tmpdir, 'coverage', 'coverage.xml')
39
+ assert File.exist?(coverage_xml_path), "coverage.xml was not generated"
40
+ xml_content = File.read(coverage_xml_path)
41
+ assert !xml_content.empty?, "coverage.xml is empty"
42
+
43
+ doc = Nokogiri::XML(xml_content) { |config| config.strict }
44
+ assert doc.errors.empty?, "XML parse errors: #{doc.errors.join(', ')}"
45
+
46
+ # Root element
47
+ coverage = doc.at_xpath('/coverage')
48
+ assert_not_nil coverage, "Missing /coverage root element"
49
+
50
+ # Line coverage attributes — partial, not 100% and not 0%
51
+ line_rate = coverage['line-rate'].to_f
52
+ assert line_rate > 0.0, "line-rate should be > 0, got #{line_rate}"
53
+ assert line_rate < 1.0, "line-rate should be < 1.0 (partial coverage), got #{line_rate}"
54
+
55
+ lines_covered = coverage['lines-covered'].to_i
56
+ lines_valid = coverage['lines-valid'].to_i
57
+ assert lines_covered > 0, "lines-covered should be > 0"
58
+ assert lines_valid > lines_covered, "lines-valid (#{lines_valid}) should be > lines-covered (#{lines_covered})"
59
+
60
+ # Branch coverage attributes — partial
61
+ branch_rate = coverage['branch-rate'].to_f
62
+ assert branch_rate > 0.0, "branch-rate should be > 0, got #{branch_rate}"
63
+ assert branch_rate < 1.0, "branch-rate should be < 1.0 (partial coverage), got #{branch_rate}"
64
+
65
+ branches_covered = coverage['branches-covered'].to_i
66
+ branches_valid = coverage['branches-valid'].to_i
67
+ assert branches_covered > 0, "branches-covered should be > 0"
68
+ assert branches_valid > branches_covered, "branches-valid (#{branches_valid}) should be > branches-covered (#{branches_covered})"
69
+
70
+ # The sample library file appears as a class with the expected relative filename
71
+ classes = doc.xpath('//class')
72
+ filenames = classes.map { |c| c['filename'] }
73
+ assert filenames.any? { |f| f.include?('lib/sample.rb') },
74
+ "Expected a class with filename containing 'lib/sample.rb', got: #{filenames}"
75
+
76
+ # Line-level <line> elements exist with hits and branch attributes
77
+ lines = doc.xpath('//line')
78
+ assert lines.length > 0, "Expected <line> elements in the output"
79
+ lines.each do |line|
80
+ assert_not_nil line['number'], "Line element missing 'number' attribute"
81
+ assert_not_nil line['hits'], "Line element missing 'hits' attribute"
82
+ assert_not_nil line['branch'], "Line element missing 'branch' attribute"
83
+ end
84
+
85
+ # At least one line should have branch='true' (from the if/else)
86
+ branch_lines = lines.select { |l| l['branch'] == 'true' }
87
+ assert branch_lines.length > 0, "Expected at least one line with branch='true'"
88
+
89
+ # Validate condition-coverage on branch lines
90
+ branch_lines.each do |bl|
91
+ cc = bl['condition-coverage']
92
+ assert_not_nil cc, "Branch line missing 'condition-coverage' attribute"
93
+ assert_match(/\d+% \(\d+\/\d+\)/, cc, "condition-coverage format unexpected: #{cc}")
94
+ end
95
+ end
96
+
97
+ def test_condition_coverage_values_are_accurate
98
+ install_fixture('sample')
99
+
100
+ stdout, stderr, status = run_fixture_test('test/test_sample.rb')
101
+ assert status.success?, "Sample test subprocess failed.\nstdout: #{stdout}\nstderr: #{stderr}"
102
+
103
+ coverage_xml_path = File.join(@tmpdir, 'coverage', 'coverage.xml')
104
+ assert File.exist?(coverage_xml_path), "coverage.xml was not generated"
105
+ doc = Nokogiri::XML(File.read(coverage_xml_path)) { |config| config.strict }
106
+
107
+ # Find the branch line for the if statement in sample.rb's absolute method
108
+ sample_class = doc.xpath('//class').find { |c| c['filename'].include?('sample.rb') }
109
+ assert_not_nil sample_class, "Expected sample.rb class in output"
110
+
111
+ branch_lines = sample_class.xpath('.//line[@branch="true"]')
112
+ assert branch_lines.length > 0, "Expected branch lines in sample.rb"
113
+
114
+ # Each if/else has 2 total branches. greet and absolute each have 1/2 covered,
115
+ # unused_method has 0/2 covered. Verify exact values by line number.
116
+ condition_coverages = branch_lines.map { |bl| [bl['number'].to_i, bl['condition-coverage']] }.to_h
117
+
118
+ # greet: line 3 — only else branch taken => 50% (1/2)
119
+ assert_equal '50% (1/2)', condition_coverages[3], "greet condition-coverage mismatch"
120
+ # absolute: line 11 — only else branch taken => 50% (1/2)
121
+ assert_equal '50% (1/2)', condition_coverages[11], "absolute condition-coverage mismatch"
122
+ # unused_method: line 22 — neither branch taken => 0% (0/2)
123
+ assert_equal '0% (0/2)', condition_coverages[22], "unused_method condition-coverage mismatch"
124
+ end
125
+
126
+ def test_merged_results_exercise_string_condition_keys
127
+ install_fixture('sample')
128
+
129
+ stdout, stderr, status = run_fixture_test('test/test_merged_a.rb',
130
+ 'COMMAND_NAME' => 'Run A')
131
+ assert status.success?, "Run A failed.\nstdout: #{stdout}\nstderr: #{stderr}"
132
+
133
+ # Tripwire: confirm the stored resultset actually contains stringified
134
+ # condition keys. If a future simplecov changes its serialization, this
135
+ # assertion fails and tells you this test no longer covers the string path.
136
+ resultset = File.read(File.join(@tmpdir, 'coverage', '.resultset.json'))
137
+ assert_match(/\[:if, \d+, \d+/, resultset,
138
+ 'Expected stringified branch condition keys in .resultset.json')
139
+
140
+ stdout, stderr, status = run_fixture_test('test/test_merged_b.rb',
141
+ 'COMMAND_NAME' => 'Run B')
142
+ assert status.success?, "Run B failed.\nstdout: #{stdout}\nstderr: #{stderr}"
143
+
144
+ doc = Nokogiri::XML(File.read(File.join(@tmpdir, 'coverage', 'coverage.xml'))) { |c| c.strict }
145
+ sample_class = doc.xpath('//class').find { |c| c['filename'].include?('sample.rb') }
146
+ assert_not_nil sample_class
147
+
148
+ cc = sample_class.xpath('.//line[@branch="true"]')
149
+ .map { |l| [l['number'].to_i, l['condition-coverage']] }
150
+ .to_h
151
+
152
+ # greet (line 3): else in Run A + then in Run B => merged 2/2.
153
+ # Proves string keys were parsed AND hit counts summed across runs.
154
+ assert_equal '100% (2/2)', cc[3], 'greet should be fully covered after merge'
155
+ # absolute (line 11): only Run A touched it => still 1/2.
156
+ assert_equal '50% (1/2)', cc[11]
157
+ # unused_method (line 22): never called in either run => 0/2.
158
+ assert_equal '0% (0/2)', cc[22]
159
+ end
160
+
161
+ private
162
+
163
+ def install_fixture(name)
164
+ FileUtils.cp_r(File.join(FIXTURES_DIR, name, '.'), @tmpdir)
165
+ end
166
+
167
+ def run_fixture_test(test_path, extra_env = {})
168
+ Open3.capture3(
169
+ { 'GEM_ROOT' => GEM_ROOT, 'PROJECT_ROOT' => @tmpdir }.merge(extra_env),
170
+ 'ruby', File.join(@tmpdir, test_path),
171
+ chdir: @tmpdir
172
+ )
173
+ end
174
+
175
+ end
@@ -1,4 +1,6 @@
1
1
  require 'test/unit'
2
+ require 'fileutils'
3
+ require 'tmpdir'
2
4
  require 'nokogiri'
3
5
  require 'open-uri'
4
6
  require 'simplecov'
@@ -6,32 +8,35 @@ require 'simplecov-cobertura'
6
8
 
7
9
  class CoberturaFormatterTest < Test::Unit::TestCase
8
10
  def setup
11
+ @tmpdir = Dir.mktmpdir('simplecov-cobertura-test')
9
12
  SimpleCov.enable_coverage :branch
10
- SimpleCov.coverage_dir "tmp"
13
+ SimpleCov.coverage_dir @tmpdir
14
+ @no_filter = SimpleCov::Result::FilterConfig.new(filters: [], cover_filters: [], groups: {})
11
15
  @result = SimpleCov::Result.new({
12
- "#{__FILE__}" => {
13
- "lines" => [1, 1, 1, nil, 1, nil, 1, 0, nil, 1, nil, nil, nil],
14
- "branches" => {
15
- [:if, 0, 3, 4, 3, 21] =>
16
- {[:then, 1, 3, 4, 3, 10] => 0, [:else, 2, 3, 4, 3, 21] => 1},
17
- [:if, 3, 5, 4, 5, 26] =>
18
- {[:then, 4, 5, 16, 5, 20] => 1, [:else, 5, 5, 23, 5, 26] => 0},
19
- [:if, 6, 7, 4, 11, 7] =>
20
- {[:then, 7, 8, 6, 8, 10] => 0, [:else, 8, 10, 6, 10, 9] => 1},
21
- [:if, 9, 12, 4, 12, 15] =>
22
- {[:then, 10, 12, 6, 12, 10] => 1, [:else, 11, 12, 13, 12, 15] => 0},
23
- [:if, 12, 13, 4, 13, 20] =>
24
- {[:then, 13, 13, 6, 13, 15] => 1, [:else, 14, 13, 18, 13, 20] => 0},
25
- [:if, 15, 15, 4, 15, 25] =>
26
- {[:then, 16, 15, 6, 15, 20] => 0, [:else, 17, 15, 23, 15, 25] => 0}
27
- }
28
- }
29
- })
16
+ "#{__FILE__}" => {
17
+ "lines" => [1, 1, 1, nil, 1, nil, 1, 0, nil, 1, nil, nil, nil],
18
+ "branches" => {
19
+ [:if, 0, 3, 4, 3, 21] =>
20
+ { [:then, 1, 3, 4, 3, 10] => 0, [:else, 2, 3, 4, 3, 21] => 1 },
21
+ [:if, 3, 5, 4, 5, 26] =>
22
+ { [:then, 4, 5, 16, 5, 20] => 1, [:else, 5, 5, 23, 5, 26] => 0 },
23
+ [:if, 6, 7, 4, 11, 7] =>
24
+ { [:then, 7, 8, 6, 8, 10] => 0, [:else, 8, 10, 6, 10, 9] => 1 },
25
+ [:if, 9, 12, 4, 12, 15] =>
26
+ { [:then, 10, 12, 6, 12, 10] => 1, [:else, 11, 12, 13, 12, 15] => 0 },
27
+ [:if, 12, 13, 4, 13, 20] =>
28
+ { [:then, 13, 13, 6, 13, 15] => 1, [:else, 14, 13, 18, 13, 20] => 0 },
29
+ [:if, 15, 15, 4, 15, 25] =>
30
+ { [:then, 16, 15, 6, 15, 20] => 0, [:else, 17, 15, 23, 15, 25] => 0 }
31
+ }
32
+ }
33
+ }, filter_config: @no_filter)
30
34
  @formatter = SimpleCov::Formatter::CoberturaFormatter.new
31
35
  end
32
36
 
33
37
  def teardown
34
38
  SimpleCov.groups.clear
39
+ FileUtils.remove_entry(@tmpdir) if @tmpdir && Dir.exist?(@tmpdir)
35
40
  end
36
41
 
37
42
  def test_format_save_file
@@ -110,26 +115,29 @@ class CoberturaFormatterTest < Test::Unit::TestCase
110
115
  assert_equal '1', first_line.attribute('hits').value
111
116
  last_line = lines.last
112
117
  assert_equal '10', last_line.attribute('number').value
113
- assert_equal 'true', last_line.attribute('branch').value
118
+ assert_equal 'false', last_line.attribute('branch').value
114
119
  assert_equal '1', last_line.attribute('hits').value
115
120
 
116
- # Verify condition-coverage accurately reflects branch counts per line
121
+ # Verify condition-coverage accurately reflects branch counts per condition line
117
122
  branched_lines = lines.select { |l| l.attribute('branch').value == 'true' }
118
123
  condition_coverages = branched_lines.map { |l| [l.attribute('number').value, l.attribute('condition-coverage').value] }
119
- # Line 3: 2 branches (then=>0, else=>1) => 50% (1/2)
124
+ # Line 3: condition [:if, 0, 3, ...] with 2 branches (then=>0, else=>1) => 50% (1/2)
120
125
  assert_include condition_coverages, ['3', '50% (1/2)']
121
- # Line 5: 2 branches (then=>1, else=>0) => 50% (1/2)
126
+ # Line 5: condition [:if, 3, 5, ...] with 2 branches (then=>1, else=>0) => 50% (1/2)
122
127
  assert_include condition_coverages, ['5', '50% (1/2)']
123
- # Line 8: 1 branch (then=>0) => 0% (0/1)
124
- assert_include condition_coverages, ['8', '0% (0/1)']
125
- # Line 10: 1 branch (else=>1) => 100% (1/1)
126
- assert_include condition_coverages, ['10', '100% (1/1)']
128
+ # Line 7: condition [:if, 6, 7, ...] with 2 branches (then=>0, else=>1) => 50% (1/2)
129
+ assert_include condition_coverages, ['7', '50% (1/2)']
130
+ # Lines 12, 13, 15 have nil line coverage so they don't get <line> elements,
131
+ # but their conditions are still correctly grouped by condition start line.
132
+ assert_equal 3, branched_lines.length
127
133
  end
128
134
 
129
135
  def test_groups
130
- SimpleCov.add_group('test_group', 'test/')
136
+ SimpleCov.group('test_group', 'test/')
137
+ group_filter = SimpleCov::Result::FilterConfig.new(filters: [], cover_filters: [], groups: SimpleCov.groups)
138
+ result = SimpleCov::Result.new(@result.original_result, filter_config: group_filter)
131
139
 
132
- xml = @formatter.format(@result)
140
+ xml = @formatter.format(result)
133
141
  doc = Nokogiri::XML::Document.parse(xml)
134
142
 
135
143
  coverage = doc.xpath '/coverage'
@@ -172,24 +180,34 @@ class CoberturaFormatterTest < Test::Unit::TestCase
172
180
  assert_equal '1', first_line.attribute('hits').value
173
181
  last_line = lines.last
174
182
  assert_equal '10', last_line.attribute('number').value
175
- assert_equal 'true', last_line.attribute('branch').value
183
+ assert_equal 'false', last_line.attribute('branch').value
176
184
  assert_equal '1', last_line.attribute('hits').value
177
185
  end
178
186
 
179
187
  def test_supports_root_project_path
180
188
  old_root = SimpleCov.root
181
- SimpleCov.root("/tmp")
182
- expected_base = old_root[1..-1] # Remove leading "/"
183
-
184
- xml = @formatter.format(@result)
189
+ @alt_root = Dir.mktmpdir('simplecov-cobertura-root')
190
+ SimpleCov.root(@alt_root)
191
+ expected_prefix = Pathname.new(old_root).relative_path_from(Pathname.new(@alt_root)).to_s
192
+ result = SimpleCov::Result.new(@result.original_result, filter_config: @no_filter)
193
+ xml = @formatter.format(result)
185
194
  doc = Nokogiri::XML::Document.parse(xml)
186
195
 
187
196
  classes = doc.xpath '/coverage/packages/package/classes/class'
188
197
  assert_equal 1, classes.length
189
198
  clazz = classes.first
190
- assert_equal "../#{expected_base}/test/simplecov-cobertura_test.rb", clazz.attribute('name').value
191
- assert_equal "../#{expected_base}/test/simplecov-cobertura_test.rb", clazz.attribute('filename').value
199
+ assert_equal "#{expected_prefix}/test/simplecov-cobertura_test.rb", clazz.attribute('name').value
200
+ assert_equal "#{expected_prefix}/test/simplecov-cobertura_test.rb", clazz.attribute('filename').value
192
201
  ensure
193
202
  SimpleCov.root(old_root)
203
+ FileUtils.remove_entry(@alt_root) if @alt_root && Dir.exist?(@alt_root)
204
+ end
205
+
206
+ def test_condition_start_line_handles_both_key_forms
207
+ formatter = SimpleCov::Formatter::CoberturaFormatter.new
208
+ assert_equal 3, formatter.send(:condition_start_line, [:if, 0, 3, 4, 5, 10])
209
+ assert_equal 3, formatter.send(:condition_start_line, '[:if, 0, 3, 4, 5, 10]')
210
+ assert_equal 7, formatter.send(:condition_start_line, '[:case, 12, 7, 0, 9, 3]')
211
+ assert_nil formatter.send(:condition_start_line, 42)
194
212
  end
195
213
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simplecov-cobertura
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.2
4
+ version: 4.0.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jesse Bowes
@@ -55,16 +55,22 @@ dependencies:
55
55
  name: simplecov
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
- - - "~>"
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 1.0.0.rc2
61
+ - - "<"
59
62
  - !ruby/object:Gem::Version
60
- version: '0.19'
63
+ version: '2.0'
61
64
  type: :runtime
62
65
  prerelease: false
63
66
  version_requirements: !ruby/object:Gem::Requirement
64
67
  requirements:
65
- - - "~>"
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: 1.0.0.rc2
71
+ - - "<"
66
72
  - !ruby/object:Gem::Version
67
- version: '0.19'
73
+ version: '2.0'
68
74
  - !ruby/object:Gem::Dependency
69
75
  name: rexml
70
76
  requirement: !ruby/object:Gem::Requirement
@@ -95,6 +101,12 @@ files:
95
101
  - lib/simplecov-cobertura.rb
96
102
  - lib/simplecov-cobertura/version.rb
97
103
  - simplecov-cobertura.gemspec
104
+ - test/fixtures/sample/lib/sample.rb
105
+ - test/fixtures/sample/test/simplecov_setup.rb
106
+ - test/fixtures/sample/test/test_merged_a.rb
107
+ - test/fixtures/sample/test/test_merged_b.rb
108
+ - test/fixtures/sample/test/test_sample.rb
109
+ - test/integration_test.rb
98
110
  - test/simplecov-cobertura_test.rb
99
111
  homepage: https://github.com/jessebs/simplecov-cobertura
100
112
  licenses:
@@ -107,7 +119,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
107
119
  requirements:
108
120
  - - ">="
109
121
  - !ruby/object:Gem::Version
110
- version: 2.5.0
122
+ version: '3.1'
111
123
  required_rubygems_version: !ruby/object:Gem::Requirement
112
124
  requirements:
113
125
  - - ">="
@@ -118,4 +130,10 @@ rubygems_version: 3.6.9
118
130
  specification_version: 4
119
131
  summary: SimpleCov Cobertura Formatter
120
132
  test_files:
133
+ - test/fixtures/sample/lib/sample.rb
134
+ - test/fixtures/sample/test/simplecov_setup.rb
135
+ - test/fixtures/sample/test/test_merged_a.rb
136
+ - test/fixtures/sample/test/test_merged_b.rb
137
+ - test/fixtures/sample/test/test_sample.rb
138
+ - test/integration_test.rb
121
139
  - test/simplecov-cobertura_test.rb