simplecov-cobertura 3.1.2 → 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: d9b68c5aa76db30753de21b0db6f942c58e0ccc41c7f77d73881215faf7bbf31
4
- data.tar.gz: 19b8626568d444dc368bd9a4fad5747c78c0cb4583c0d44d074994e671d4ceab
3
+ metadata.gz: 64fe70b5fa4a2d1864fd64e6a44798cc2e6530bbef43b601e9b2aaa9e1516951
4
+ data.tar.gz: 419fe1ceee7fec9e4737425d177c5a5e2d58f7a78c95170f75b6f7840f4cccbd
5
5
  SHA512:
6
- metadata.gz: 9cd6c9bf87b3da4df600bd3c3754ed66581802eb5d46b691119f605af7707f23a14af37fba5a67f524bf36380540f259ca2f3479ee0d651d5fd8cd0c6e0f3ffd
7
- data.tar.gz: 4308ed2698f5bd7c3afc7c319a6a231aa1ea501f312282fd73f9650062d34d7492e26e8cb86d127a779f11c2fbb89a80c06dc6638e063e3c7bdf219a2356f6f8
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,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: [ 2.5, 2.6, 2.7, '3.0', 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,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.2'
4
+ VERSION = '3.2.0'
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,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,20 +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
115
 
116
- # Verify condition-coverage accurately reflects branch counts per line
116
+ # Verify condition-coverage accurately reflects branch counts per condition line
117
117
  branched_lines = lines.select { |l| l.attribute('branch').value == 'true' }
118
118
  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)
119
+ # Line 3: condition [:if, 0, 3, ...] with 2 branches (then=>0, else=>1) => 50% (1/2)
120
120
  assert_include condition_coverages, ['3', '50% (1/2)']
121
- # Line 5: 2 branches (then=>1, else=>0) => 50% (1/2)
121
+ # Line 5: condition [:if, 3, 5, ...] with 2 branches (then=>1, else=>0) => 50% (1/2)
122
122
  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)']
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
127
128
  end
128
129
 
129
130
  def test_groups
@@ -172,7 +173,7 @@ class CoberturaFormatterTest < Test::Unit::TestCase
172
173
  assert_equal '1', first_line.attribute('hits').value
173
174
  last_line = lines.last
174
175
  assert_equal '10', last_line.attribute('number').value
175
- assert_equal 'true', last_line.attribute('branch').value
176
+ assert_equal 'false', last_line.attribute('branch').value
176
177
  assert_equal '1', last_line.attribute('hits').value
177
178
  end
178
179
 
@@ -192,4 +193,12 @@ class CoberturaFormatterTest < Test::Unit::TestCase
192
193
  ensure
193
194
  SimpleCov.root(old_root)
194
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
195
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.2
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:
@@ -118,4 +124,10 @@ 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