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 +4 -4
- data/.github/workflows/build.yml +3 -3
- data/README.md +9 -4
- data/lib/simplecov-cobertura/version.rb +1 -1
- data/lib/simplecov-cobertura.rb +123 -92
- data/simplecov-cobertura.gemspec +11 -11
- data/test/fixtures/sample/lib/sample.rb +28 -0
- data/test/fixtures/sample/test/simplecov_setup.rb +17 -0
- data/test/fixtures/sample/test/test_merged_a.rb +11 -0
- data/test/fixtures/sample/test/test_merged_b.rb +7 -0
- data/test/fixtures/sample/test/test_sample.rb +12 -0
- data/test/integration_test.rb +175 -0
- data/test/simplecov-cobertura_test.rb +41 -20
- metadata +14 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 64fe70b5fa4a2d1864fd64e6a44798cc2e6530bbef43b601e9b2aaa9e1516951
|
|
4
|
+
data.tar.gz: 419fe1ceee7fec9e4737425d177c5a5e2d58f7a78c95170f75b6f7840f4cccbd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d2d7ddbac9c17c4346372ba6eccc9a73d4ec6c90973555d77c8248fe5fc8e08a9b4e7f11ddb5d418b41e561b96c27580255e3b32dfac9f817e5958eacb372b60
|
|
7
|
+
data.tar.gz: fa33a2992ee7eeb5f72017111996f19169519b9c314b0f0262c19eb8cf838fe8b18c834140cebbc54c781d7b4ec31c767188f87795429db8873b7377ac7ac48c
|
data/.github/workflows/build.yml
CHANGED
|
@@ -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@
|
|
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
|
-
[](https://github.com/jessebs/simplecov-cobertura/actions/workflows/build.yml) [](http://badge.fury.io/rb/simplecov-cobertura)
|
|
1
|
+
# simplecov-cobertura
|
|
3
2
|
|
|
3
|
+
[](https://github.com/jessebs/simplecov-cobertura/actions/workflows/build.yml) [](http://badge.fury.io/rb/simplecov-cobertura)
|
|
4
4
|
|
|
5
|
-
Produces [Cobertura](http://cobertura.sourceforge.net/) formatted XML
|
|
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)
|
|
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");
|
data/lib/simplecov-cobertura.rb
CHANGED
|
@@ -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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
+
set_coverage_attributes(coverage, result)
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
52
|
+
coverage.add_element(packages = REXML::Element.new('packages'))
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
if result.groups.empty?
|
|
55
|
+
groups = { File.basename(SimpleCov.root) => result.files }
|
|
56
|
+
else
|
|
57
|
+
groups = result.groups
|
|
58
|
+
end
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
+
package.add_element(classes = REXML::Element.new('classes'))
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
files.each do |file|
|
|
68
|
+
classes.add_element(class_ = REXML::Element.new('class'))
|
|
69
|
+
set_class_attributes(class_, file)
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
class_.add_element(REXML::Element.new('methods'))
|
|
72
|
+
class_.add_element(lines = REXML::Element.new('lines'))
|
|
71
73
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
branches_by_line = {}
|
|
75
|
+
if SimpleCov.branch_coverage?
|
|
76
|
+
build_branches_by_line(file, branches_by_line)
|
|
77
|
+
end
|
|
74
78
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
coverage.attributes['
|
|
101
|
-
coverage.attributes['
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
109
|
+
def set_package_attributes(package, name, result)
|
|
110
|
+
ls = result.coverage_statistics[:line]
|
|
111
|
+
bs = result.coverage_statistics[:branch]
|
|
108
112
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
def set_class_attributes(class_, file)
|
|
122
|
+
ls = file.coverage_statistics[:line]
|
|
123
|
+
bs = file.coverage_statistics[:branch]
|
|
120
124
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
185
|
+
def resolve_filename(filename)
|
|
186
|
+
Pathname.new(filename).relative_path_from(project_root).to_s
|
|
187
|
+
end
|
|
157
188
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
189
|
+
def extract_rate(percent)
|
|
190
|
+
(percent / 100).round(4).to_s
|
|
191
|
+
end
|
|
161
192
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
193
|
+
def project_root
|
|
194
|
+
@project_root ||= Pathname.new(SimpleCov.root)
|
|
195
|
+
end
|
|
165
196
|
end
|
|
166
197
|
end
|
|
167
198
|
end
|
data/simplecov-cobertura.gemspec
CHANGED
|
@@ -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
|
|
9
|
-
spec.version
|
|
10
|
-
spec.authors
|
|
11
|
-
spec.email
|
|
12
|
-
spec.summary
|
|
13
|
-
spec.description
|
|
14
|
-
spec.homepage
|
|
15
|
-
spec.license
|
|
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
|
|
19
|
-
spec.executables
|
|
20
|
-
spec.test_files
|
|
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,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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 '
|
|
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 '
|
|
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.
|
|
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.
|
|
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
|