simplecov-cobertura 3.1.0 → 3.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 04ae07cb7f6bb85ad36640169b7d630c985ca1d3d067ef3485ea531b7b775209
4
- data.tar.gz: 42f5899bf07710880548d48a27d8d59d2eef1b4d10a05374c44e4344f8f0ac54
3
+ metadata.gz: 64fe70b5fa4a2d1864fd64e6a44798cc2e6530bbef43b601e9b2aaa9e1516951
4
+ data.tar.gz: 419fe1ceee7fec9e4737425d177c5a5e2d58f7a78c95170f75b6f7840f4cccbd
5
5
  SHA512:
6
- metadata.gz: e35c59ec78263fe81826f47b83ff5e160e08bf8aa4e01a01b943d1671974c853737e4742e5073997e4ac40d77f54f4f29a1f3d29b17a8e93021432c3f7814fc1
7
- data.tar.gz: b320d8920cfb8a354031628b045077387474fb07511547cc14f277a00362fcd34ef9974dfb0464cda308f5f167e0c3c3c4bbf932b3608e463bc948dbeb0696da
6
+ metadata.gz: d2d7ddbac9c17c4346372ba6eccc9a73d4ec6c90973555d77c8248fe5fc8e08a9b4e7f11ddb5d418b41e561b96c27580255e3b32dfac9f817e5958eacb372b60
7
+ data.tar.gz: fa33a2992ee7eeb5f72017111996f19169519b9c314b0f0262c19eb8cf838fe8b18c834140cebbc54c781d7b4ec31c767188f87795429db8873b7377ac7ac48c
@@ -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,10 +15,10 @@ 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]
18
+ ruby-version: [ 2.5, 2.6, 2.7, '3.0', 3.1, 3.2, 3.3, 3.4, 4.0 ]
19
19
 
20
20
  steps:
21
- - uses: actions/checkout@v2
21
+ - uses: actions/checkout@v6
22
22
  - name: Set up Ruby
23
23
  uses: ruby/setup-ruby@v1
24
24
  with:
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,7 +34,9 @@ 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:
39
+
36
40
  * 3.0 - 3.4
37
41
  * 2.5 - 2.7
38
42
 
@@ -45,6 +49,7 @@ Tested in a CI environment against the following Ruby versions:
45
49
  5. Create a new Pull Request
46
50
 
47
51
  ## License
52
+
48
53
  Copyright 2025 Jesse Bowes
49
54
 
50
55
  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.0'
4
+ VERSION = '3.2.0'
5
5
  end
6
6
  end
7
7
  end
@@ -1,6 +1,8 @@
1
+ require 'stringio'
1
2
  require 'rexml/document'
2
3
  require 'rexml/element'
3
4
  require 'pathname'
5
+ require 'simplecov'
4
6
 
5
7
  require_relative 'simplecov-cobertura/version'
6
8
 
@@ -32,116 +34,145 @@ module SimpleCov
32
34
 
33
35
  private
34
36
 
35
- def result_to_xml(result)
36
- doc = REXML::Document.new
37
- doc.context[:attribute_quote] = :quote
38
- doc << REXML::XMLDecl.new('1.0')
39
- doc << REXML::DocType.new('coverage', "SYSTEM \"#{DTD_URL}\"")
40
- doc << REXML::Comment.new("Generated by simplecov-cobertura version #{VERSION} (https://github.com/jessebs/simplecov-cobertura)")
41
- doc.add_element REXML::Element.new('coverage')
42
- coverage = doc.root
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
43
45
 
44
- set_coverage_attributes(coverage, result)
46
+ set_coverage_attributes(coverage, result)
45
47
 
46
- coverage.add_element(sources = REXML::Element.new('sources'))
47
- sources.add_element(source = REXML::Element.new('source'))
48
- source.text = SimpleCov.root
48
+ coverage.add_element(sources = REXML::Element.new('sources'))
49
+ sources.add_element(source = REXML::Element.new('source'))
50
+ source.text = SimpleCov.root
49
51
 
50
- coverage.add_element(packages = REXML::Element.new('packages'))
52
+ coverage.add_element(packages = REXML::Element.new('packages'))
51
53
 
52
- if result.groups.empty?
53
- groups = {File.basename(SimpleCov.root) => result.files}
54
- else
55
- groups = result.groups
56
- end
54
+ if result.groups.empty?
55
+ groups = { File.basename(SimpleCov.root) => result.files }
56
+ else
57
+ groups = result.groups
58
+ end
57
59
 
58
- groups.each do |name, files|
59
- next if files.empty?
60
- packages.add_element(package = REXML::Element.new('package'))
61
- 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)
62
64
 
63
- package.add_element(classes = REXML::Element.new('classes'))
65
+ package.add_element(classes = REXML::Element.new('classes'))
64
66
 
65
- files.each do |file|
66
- classes.add_element(class_ = REXML::Element.new('class'))
67
- 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)
68
70
 
69
- class_.add_element(REXML::Element.new('methods'))
70
- 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'))
71
73
 
72
- branched_lines = file.branches.map(&:start_line)
73
- branched_lines_covered = file.covered_branches.map(&:start_line)
74
+ branches_by_line = {}
75
+ if SimpleCov.branch_coverage?
76
+ build_branches_by_line(file, branches_by_line)
77
+ end
74
78
 
75
- file.lines.each do |file_line|
76
- if file_line.covered? || file_line.missed?
77
- lines.add_element(line = REXML::Element.new('line'))
78
- set_line_attributes(line, file_line)
79
- set_branch_attributes(line, file_line, branched_lines, branched_lines_covered) if SimpleCov.branch_coverage?
80
- 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?
81
84
  end
82
85
  end
83
86
  end
84
-
85
- doc
86
87
  end
87
88
 
88
- def set_coverage_attributes(coverage, result)
89
- ls = result.coverage_statistics[:line]
90
- bs = result.coverage_statistics[:branch]
91
-
92
- coverage.attributes['line-rate'] = extract_rate(ls.percent)
93
- coverage.attributes['lines-covered'] = ls.covered.to_s.to_s
94
- coverage.attributes['lines-valid'] = ls.total.to_s.to_s
95
- if SimpleCov.branch_coverage?
96
- coverage.attributes['branches-covered'] = bs.covered.to_s
97
- coverage.attributes['branches-valid'] = bs.total.to_s
98
- coverage.attributes['branch-rate'] = extract_rate(bs.percent)
99
- end
100
- coverage.attributes['complexity'] = '0'
101
- coverage.attributes['version'] = '0'
102
- 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)
103
103
  end
104
+ coverage.attributes['complexity'] = '0'
105
+ coverage.attributes['version'] = '0'
106
+ coverage.attributes['timestamp'] = Time.now.to_i.to_s
107
+ end
104
108
 
105
- def set_package_attributes(package, name, result)
106
- ls = result.coverage_statistics[:line]
107
- 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]
108
112
 
109
- package.attributes['name'] = name
110
- package.attributes['line-rate'] = extract_rate(ls.percent)
111
- if SimpleCov.branch_coverage?
112
- package.attributes['branch-rate'] = extract_rate(bs.percent)
113
- end
114
- 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)
115
117
  end
118
+ package.attributes['complexity'] = '0'
119
+ end
116
120
 
117
- def set_class_attributes(class_, file)
118
- ls = file.coverage_statistics[:line]
119
- bs = file.coverage_statistics[:branch]
121
+ def set_class_attributes(class_, file)
122
+ ls = file.coverage_statistics[:line]
123
+ bs = file.coverage_statistics[:branch]
120
124
 
121
- filename = file.filename
122
- class_.attributes['name'] = resolve_filename(filename)
123
- class_.attributes['filename'] = resolve_filename(filename)
124
- class_.attributes['line-rate'] = extract_rate(ls.percent)
125
- if SimpleCov.branch_coverage?
126
- class_.attributes['branch-rate'] = extract_rate(bs.percent)
127
- end
128
- 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)
129
131
  end
132
+ class_.attributes['complexity'] = '0'
133
+ end
134
+
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
130
139
 
131
- def set_line_attributes(line, file_line)
132
- line.attributes['number'] = file_line.line_number.to_s
133
- line.attributes['hits'] = file_line.coverage.to_s
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'
134
150
  end
151
+ end
135
152
 
136
- def set_branch_attributes(line, file_line, branched_lines, branched_lines_covered)
137
- if branched_lines.include? file_line.number
138
- pct_coverage, branches_covered = branched_lines_covered.include?(file_line.number) ? [100, '1/1'] : [0, '0/1']
139
- line.attributes['branch'] = 'true'
140
- line.attributes['condition-coverage'] = "#{pct_coverage}% (#{branches_covered})"
141
- else
142
- 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
143
162
  end
144
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
145
176
 
146
177
  # Roughly mirrors private method SimpleCov::Formatter::HTMLFormatter#output_coverage
147
178
  def output_message(result, output_path)
@@ -151,17 +182,17 @@ module SimpleCov
151
182
  output
152
183
  end
153
184
 
154
- def resolve_filename(filename)
155
- Pathname.new(filename).relative_path_from(project_root).to_s
156
- end
185
+ def resolve_filename(filename)
186
+ Pathname.new(filename).relative_path_from(project_root).to_s
187
+ end
157
188
 
158
- def extract_rate(percent)
159
- (percent / 100).round(4).to_s
160
- end
189
+ def extract_rate(percent)
190
+ (percent / 100).round(4).to_s
191
+ end
161
192
 
162
- def project_root
163
- @project_root ||= Pathname.new(SimpleCov.root)
164
- end
193
+ def project_root
194
+ @project_root ||= Pathname.new(SimpleCov.root)
195
+ end
165
196
  end
166
197
  end
167
198
  end
@@ -5,19 +5,19 @@ $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'
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
16
  spec.required_ruby_version = '>= 2.5.0'
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'
@@ -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
@@ -9,24 +9,24 @@ class CoberturaFormatterTest < Test::Unit::TestCase
9
9
  SimpleCov.enable_coverage :branch
10
10
  SimpleCov.coverage_dir "tmp"
11
11
  @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
- })
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
+ })
30
30
  @formatter = SimpleCov::Formatter::CoberturaFormatter.new
31
31
  end
32
32
 
@@ -110,8 +110,21 @@ class CoberturaFormatterTest < Test::Unit::TestCase
110
110
  assert_equal '1', first_line.attribute('hits').value
111
111
  last_line = lines.last
112
112
  assert_equal '10', last_line.attribute('number').value
113
- assert_equal 'true', last_line.attribute('branch').value
113
+ assert_equal 'false', last_line.attribute('branch').value
114
114
  assert_equal '1', last_line.attribute('hits').value
115
+
116
+ # Verify condition-coverage accurately reflects branch counts per condition line
117
+ branched_lines = lines.select { |l| l.attribute('branch').value == 'true' }
118
+ condition_coverages = branched_lines.map { |l| [l.attribute('number').value, l.attribute('condition-coverage').value] }
119
+ # Line 3: condition [:if, 0, 3, ...] with 2 branches (then=>0, else=>1) => 50% (1/2)
120
+ assert_include condition_coverages, ['3', '50% (1/2)']
121
+ # Line 5: condition [:if, 3, 5, ...] with 2 branches (then=>1, else=>0) => 50% (1/2)
122
+ assert_include condition_coverages, ['5', '50% (1/2)']
123
+ # Line 7: condition [:if, 6, 7, ...] with 2 branches (then=>0, else=>1) => 50% (1/2)
124
+ assert_include condition_coverages, ['7', '50% (1/2)']
125
+ # Lines 12, 13, 15 have nil line coverage so they don't get <line> elements,
126
+ # but their conditions are still correctly grouped by condition start line.
127
+ assert_equal 3, branched_lines.length
115
128
  end
116
129
 
117
130
  def test_groups
@@ -160,7 +173,7 @@ class CoberturaFormatterTest < Test::Unit::TestCase
160
173
  assert_equal '1', first_line.attribute('hits').value
161
174
  last_line = lines.last
162
175
  assert_equal '10', last_line.attribute('number').value
163
- assert_equal 'true', last_line.attribute('branch').value
176
+ assert_equal 'false', last_line.attribute('branch').value
164
177
  assert_equal '1', last_line.attribute('hits').value
165
178
  end
166
179
 
@@ -180,4 +193,12 @@ class CoberturaFormatterTest < Test::Unit::TestCase
180
193
  ensure
181
194
  SimpleCov.root(old_root)
182
195
  end
196
+
197
+ def test_condition_start_line_handles_both_key_forms
198
+ formatter = SimpleCov::Formatter::CoberturaFormatter.new
199
+ assert_equal 3, formatter.send(:condition_start_line, [:if, 0, 3, 4, 5, 10])
200
+ assert_equal 3, formatter.send(:condition_start_line, '[:if, 0, 3, 4, 5, 10]')
201
+ assert_equal 7, formatter.send(:condition_start_line, '[:case, 12, 7, 0, 9, 3]')
202
+ assert_nil formatter.send(:condition_start_line, 42)
203
+ end
183
204
  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.0
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jesse Bowes
@@ -95,6 +95,12 @@ files:
95
95
  - lib/simplecov-cobertura.rb
96
96
  - lib/simplecov-cobertura/version.rb
97
97
  - simplecov-cobertura.gemspec
98
+ - test/fixtures/sample/lib/sample.rb
99
+ - test/fixtures/sample/test/simplecov_setup.rb
100
+ - test/fixtures/sample/test/test_merged_a.rb
101
+ - test/fixtures/sample/test/test_merged_b.rb
102
+ - test/fixtures/sample/test/test_sample.rb
103
+ - test/integration_test.rb
98
104
  - test/simplecov-cobertura_test.rb
99
105
  homepage: https://github.com/jessebs/simplecov-cobertura
100
106
  licenses:
@@ -114,8 +120,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
114
120
  - !ruby/object:Gem::Version
115
121
  version: '0'
116
122
  requirements: []
117
- rubygems_version: 3.6.7
123
+ rubygems_version: 3.6.9
118
124
  specification_version: 4
119
125
  summary: SimpleCov Cobertura Formatter
120
126
  test_files:
127
+ - test/fixtures/sample/lib/sample.rb
128
+ - test/fixtures/sample/test/simplecov_setup.rb
129
+ - test/fixtures/sample/test/test_merged_a.rb
130
+ - test/fixtures/sample/test/test_merged_b.rb
131
+ - test/fixtures/sample/test/test_sample.rb
132
+ - test/integration_test.rb
121
133
  - test/simplecov-cobertura_test.rb