getcov 0.3.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6d05ca227c84cbbabe0b4988f9e05c32421c9264de47645135a594abea611be3
4
+ data.tar.gz: e4c472974841b668a70bc39bd73c7496917b8cdf059d8d862fa1fc4623265def
5
+ SHA512:
6
+ metadata.gz: 4fd45e4fd86b3b9deb0c49fa2d8e6a5cbfebefeaa8db230f286ee5f015b3bdcdf012e9781a80fef7b5e69f853e156b658b6ee96e586b6972c0a15dae582e4676
7
+ data.tar.gz: fb6dcd4d57cf7bd706b4ad085ad0652efa27d4731cf31758ee167bf8e4fe3eaadc27a27e5ca3b771df8fbf22016cbf2367436729d861787d4e18dd4e77219481
@@ -0,0 +1,54 @@
1
+ name: Publish Coverage to Pages
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ branches: [ main, master ]
7
+
8
+ permissions:
9
+ contents: read
10
+ pages: write
11
+ id-token: write
12
+
13
+ concurrency:
14
+ group: "pages"
15
+ cancel-in-progress: false
16
+
17
+ jobs:
18
+ build:
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: ruby/setup-ruby@v1
23
+ with:
24
+ ruby-version: '3.2'
25
+ bundler-cache: true
26
+ - run: bundle install --jobs 4 --retry 3
27
+ - name: Configure getcov (HTML only)
28
+ run: |
29
+ cat > getcov.rb <<'RUBY'
30
+ Getcov.configure do |c|
31
+ c.output_dir = 'coverage'
32
+ c.enable_html!
33
+ end
34
+ RUBY
35
+ - name: Run tests with coverage
36
+ env:
37
+ RUBYOPT: -rgetcov/auto
38
+ run: bundle exec rake test
39
+ - name: Setup Pages
40
+ uses: actions/configure-pages@v5
41
+ - name: Upload artifact
42
+ uses: actions/upload-pages-artifact@v3
43
+ with:
44
+ path: coverage
45
+ deploy:
46
+ environment:
47
+ name: github-pages
48
+ url: ${{ steps.deployment.outputs.page_url }}
49
+ runs-on: ubuntu-latest
50
+ needs: build
51
+ steps:
52
+ - name: Deploy to GitHub Pages
53
+ id: deployment
54
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,94 @@
1
+ name: PR Coverage
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [ main, master ]
6
+
7
+ jobs:
8
+ coverage-diff:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ with:
13
+ fetch-depth: 0
14
+
15
+ - name: Set up Ruby
16
+ uses: ruby/setup-ruby@v1
17
+ with:
18
+ ruby-version: '3.2'
19
+ bundler-cache: true
20
+
21
+ - name: Install dependencies
22
+ run: bundle install --jobs 4 --retry 3
23
+
24
+ - name: Configure getcov (HTML + JSON + PR comment)
25
+ run: |
26
+ cat > getcov.rb <<'RUBY'
27
+ require 'getcov/formatter/summary_json'
28
+ require 'getcov/formatter/pr_comment'
29
+ Getcov.configure do |c|
30
+ c.output_dir = 'coverage'
31
+ c.enable_html!
32
+ c.add_formatter(Getcov::Formatter::SummaryJSON)
33
+ c.add_formatter(Getcov::Formatter::PRComment)
34
+ end
35
+ RUBY
36
+
37
+ - name: Run tests on HEAD with coverage
38
+ env:
39
+ RUBYOPT: -rgetcov/auto
40
+ run: bundle exec rake test
41
+
42
+ - name: Move HEAD coverage aside
43
+ run: |
44
+ mkdir -p .cov/head
45
+ cp coverage/summary.json .cov/head/summary.json
46
+ cp -r coverage .cov/head/html || true
47
+
48
+ - name: Compute BASE ref
49
+ id: base
50
+ run: |
51
+ echo "ref=${{ github.base_ref }}" >> "$GITHUB_OUTPUT"
52
+
53
+ - name: Checkout BASE
54
+ run: |
55
+ git checkout ${{ steps.base.outputs.ref }}
56
+
57
+ - name: Install deps for BASE
58
+ run: bundle install --jobs 4 --retry 3
59
+
60
+ - name: Configure getcov (JSON only for baseline)
61
+ run: |
62
+ cat > getcov.rb <<'RUBY'
63
+ require 'getcov/formatter/summary_json'
64
+ Getcov.configure do |c|
65
+ c.output_dir = 'coverage'
66
+ c.add_formatter(Getcov::Formatter::SummaryJSON)
67
+ end
68
+ RUBY
69
+
70
+ - name: Run tests on BASE with coverage
71
+ env:
72
+ RUBYOPT: -rgetcov/auto
73
+ run: bundle exec rake test
74
+
75
+ - name: Move BASE coverage aside
76
+ run: |
77
+ mkdir -p .cov/base
78
+ cp coverage/summary.json .cov/base/summary.json || echo '{}' > .cov/base/summary.json
79
+
80
+ - name: Coverage diff
81
+ id: diff
82
+ run: |
83
+ ruby .github/scripts/coverage_diff.rb --base ./.cov/base/summary.json --head ./.cov/head/summary.json --fail-drop ${FAIL_DROP:-1.0} > coverage_diff.md
84
+ env:
85
+ FAIL_DROP: "1.0" # fail if total coverage drops by >= 1.0%
86
+
87
+ - name: Append to step summary
88
+ run: cat coverage_diff.md >> "$GITHUB_STEP_SUMMARY"
89
+
90
+ - name: Upload coverage HTML (HEAD) as artifact
91
+ uses: actions/upload-artifact@v4
92
+ with:
93
+ name: coverage-html
94
+ path: .cov/head/html
@@ -0,0 +1,84 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ jobs:
9
+ test-and-build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - name: Set up Ruby
15
+ uses: ruby/setup-ruby@v1
16
+ with:
17
+ ruby-version: '3.2'
18
+ bundler-cache: true
19
+
20
+ - name: Install dependencies
21
+ run: bundle install --jobs 4 --retry 3
22
+
23
+ - name: Configure getcov (HTML, Cobertura, LCOV, Badge)
24
+ run: |
25
+ cat > getcov.rb <<'RUBY'
26
+ Getcov.configure do |c|
27
+ c.output_dir = 'coverage'
28
+ c.enable_html!
29
+ c.enable_cobertura!
30
+ c.enable_lcov!
31
+ c.enable_badge!
32
+ end
33
+ RUBY
34
+
35
+ - name: Run tests with coverage
36
+ env:
37
+ RUBYOPT: -rgetcov/auto
38
+ run: bundle exec rake test
39
+
40
+ - name: Archive coverage artifacts
41
+ run: |
42
+ mkdir -p pkg
43
+ (cd coverage && zip -r ../pkg/coverage.zip .) || true
44
+
45
+ - name: Build gem
46
+ run: |
47
+ gem build getcov.gemspec
48
+ mkdir -p pkg
49
+ mv getcov-*.gem pkg/
50
+
51
+ - name: Upload artifacts
52
+ uses: actions/upload-artifact@v4
53
+ with:
54
+ name: build-artifacts
55
+ path: |
56
+ pkg/*.gem
57
+ pkg/coverage.zip
58
+
59
+ publish:
60
+ runs-on: ubuntu-latest
61
+ needs: test-and-build
62
+ permissions:
63
+ contents: write
64
+ steps:
65
+ - uses: actions/download-artifact@v4
66
+ with:
67
+ name: build-artifacts
68
+ path: pkg
69
+
70
+ - name: Publish to RubyGems
71
+ env:
72
+ GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
73
+ run: |
74
+ ls -l pkg
75
+ gem push pkg/getcov-*.gem
76
+
77
+ - name: Create GitHub Release
78
+ uses: softprops/action-gh-release@v2
79
+ with:
80
+ files: |
81
+ pkg/getcov-*.gem
82
+ pkg/coverage.zip
83
+ env:
84
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,24 @@
1
+ name: Ruby CI
2
+ on:
3
+ push:
4
+ branches: [ main, master ]
5
+ pull_request:
6
+ branches: [ main, master ]
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ ruby-version: [ '3.1', '3.2', '3.3' ]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - name: Set up Ruby
17
+ uses: ruby/setup-ruby@v1
18
+ with:
19
+ ruby-version: ${{ matrix.ruby-version }}
20
+ bundler-cache: true
21
+ - name: Install dependencies
22
+ run: bundle install --jobs 4 --retry 3
23
+ - name: Run tests
24
+ run: bundle exec rake test
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :development, :test do
6
+ gem 'rake'
7
+ gem 'minitest'
8
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Mridul
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # Getcov
2
+
3
+ Getcov is a tiny, dependency‑free Ruby code‑coverage tool inspired by SimpleCov.
4
+ It uses Ruby’s built‑in `Coverage` module to measure which lines are executed during
5
+ your test run and prints concise reports (console + HTML), with exporters and CI helpers.
6
+
7
+ ## Features
8
+
9
+ - CLI wrapper: `getcov <command>` auto-starts coverage
10
+ - Auto loader: `-r getcov/auto`
11
+ - Filters & only/ignore globs
12
+ - Groups + per-group minimum thresholds
13
+ - Track files not loaded (so 0% appears)
14
+ - Minimum coverage gate (exit code 2)
15
+ - Console summary
16
+ - HTML report (index + per-file with **per-line highlighting**), repo links
17
+ - Cobertura XML (`coverage/coverage.xml`) and LCOV (`coverage/lcov.info`)
18
+ - SVG coverage badge (`coverage/coverage.svg`)
19
+ - Branch coverage summary (if Ruby supports branches)
20
+ - Parallel test merging helpers
21
+ - GitHub Actions CI (Ruby 3.1/3.2/3.3 matrix)
22
+
23
+ ## Quick start
24
+
25
+ ```bash
26
+ gem build getcov.gemspec
27
+ gem install getcov-0.2.0.gem
28
+
29
+ # recommended: wrap your test command
30
+ getcov bundle exec rspec
31
+ # or
32
+ getcov bundle exec rake test
33
+
34
+ # alternative: just require auto starter
35
+ ruby -r getcov/auto -S rspec
36
+ ```
37
+
38
+ ## Configuration
39
+
40
+ Create `getcov.rb` in your project root, or call `Getcov.configure` programmatically.
41
+
42
+ ```ruby
43
+ Getcov.configure do |c|
44
+ c.output_dir = 'coverage'
45
+ c.minimum_coverage = 90.0
46
+
47
+ # Groups + thresholds
48
+ c.add_group 'Models', %r{^app/models/}
49
+ c.add_group 'Controllers', %r{^app/controllers/}
50
+ c.add_group_minimum 'Models', 95
51
+ c.add_group_minimum 'Controllers', 90
52
+
53
+ # Selection helpers
54
+ c.only_files 'app/**/*.rb'
55
+ c.ignore_files 'app/admin/**/*', 'vendor/**/*'
56
+ c.add_filter %r{(^|/)spec/}
57
+
58
+ # Track files even if not loaded
59
+ c.track_files 'app/**/*.rb'
60
+
61
+ # Repo links in HTML
62
+ c.repo_url = 'https://github.com/your/repo'
63
+ c.repo_branch = 'main'
64
+
65
+ # Formatters
66
+ c.enable_html!
67
+ c.enable_cobertura!
68
+ c.enable_lcov!
69
+ c.enable_badge!
70
+
71
+ # Parallel merge directory (see below)
72
+ # c.parallel_merge_dir = 'tmp/getcov'
73
+ end
74
+ ```
75
+
76
+ ## Parallel runs (simple workflow)
77
+
78
+ Workers:
79
+ ```bash
80
+ export GETCOV_ROLE=worker
81
+ # run tests with -r getcov/auto; finalize writes partial JSONs to c.parallel_merge_dir
82
+ ```
83
+
84
+ Master (after all workers finish):
85
+ ```bash
86
+ export GETCOV_ROLE=master
87
+ # run a no-op to trigger finalize which merges partials + writes reports
88
+ ruby -r getcov/auto -e ""
89
+ ```
90
+
91
+ Alternatively, call `Getcov.merge_raw_results([...])` to combine raw result hashes yourself,
92
+ then build a `Getcov::Result` and run formatters.
93
+
94
+ ## Badges
95
+
96
+ After a run, publish `coverage/coverage.svg` wherever you like (README image, docs, etc).
97
+
98
+ ## CI
99
+
100
+ GitHub Actions workflow is included in `.github/workflows/ruby.yml` (Ruby 3.1/3.2/3.3).
101
+
102
+ ## License
103
+
104
+ MIT © 2025 Mridul Shukla
105
+
106
+
107
+ ## Extras
108
+
109
+ ### Per-file thresholds
110
+ ```ruby
111
+ Getcov.configure do |c|
112
+ c.per_file_minimum = 85.0
113
+ end
114
+ ```
115
+
116
+ ### PR comment / CI summary
117
+ Enable the PR comment formatter to generate `coverage/pr_comment.md` and also write to
118
+ `$GITHUB_STEP_SUMMARY` when running in GitHub Actions:
119
+
120
+ ```ruby
121
+ require 'getcov/formatter/pr_comment'
122
+ Getcov.configure { |c| c.add_formatter(Getcov::Formatter::PRComment) }
123
+ ```
124
+
125
+ ### JSON summary
126
+ ```ruby
127
+ require 'getcov/formatter/summary_json'
128
+ Getcov.configure { |c| c.add_formatter(Getcov::Formatter::SummaryJSON) }
129
+ # => writes coverage/summary.json
130
+ ```
131
+
132
+
133
+ ## Release (RubyGems + GitHub)
134
+ - Set repo secret `RUBYGEMS_API_KEY` (from rubygems.org → API Keys).
135
+ - Bump version in `lib/getcov/version.rb`, commit, then:
136
+ ```bash
137
+ git tag vX.Y.Z
138
+ git push origin vX.Y.Z
139
+ ```
140
+ - CI will test, generate coverage, publish the gem, and create a Release with artifacts.
141
+ See `RELEASE.md` for details.
142
+
143
+
144
+ ## PR coverage diffs
145
+ A workflow `.github/workflows/pr_coverage.yml` runs tests on both **HEAD** and **BASE**, then
146
+ generates a Markdown diff (`coverage_diff.md`) and appends it to the GitHub Actions step summary.
147
+ It fails the PR if total coverage drops by ≥ `FAIL_DROP` (default **1.0%**).
148
+
149
+ To tweak the threshold, set an env var on the `Coverage diff` step:
150
+ ```yaml
151
+ env:
152
+ FAIL_DROP: "0.5"
153
+ ```
154
+
155
+ ## GitHub Pages docs
156
+ Workflow `.github/workflows/pages.yml` runs HTML coverage on pushes to default branch and publishes
157
+ the `coverage/` folder to GitHub Pages.
158
+ Enable Pages in repo settings to serve `index.html`.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << 'lib'
6
+ t.libs << 'test'
7
+ t.pattern = 'test/**/*_test.rb'
8
+ end
9
+
10
+ task default: :test
data/exe/getcov ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ if ARGV.empty?
5
+ warn 'Usage: getcov <command> [args...]'
6
+ exit 1
7
+ end
8
+
9
+ ENV['RUBYOPT'] = [ENV['RUBYOPT'], '-rgetcov/auto'].compact.join(' ')
10
+ exec(*ARGV)
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+ require 'getcov'
3
+
4
+ config_file = File.expand_path('getcov.rb', Dir.pwd)
5
+ if File.exist?(config_file)
6
+ require config_file
7
+ end
8
+
9
+ Getcov.start
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+ module Getcov
3
+ class Configuration
4
+ attr_accessor :root, :minimum_coverage, :per_file_minimum, :output_dir,
5
+ :repo_url, :repo_branch, :parallel_merge_dir
6
+ attr_reader :filters, :groups, :tracked_globs, :formatters,
7
+ :group_minimums, :ignore_globs, :only_globs
8
+
9
+ def initialize
10
+ @root = Dir.pwd
11
+ @filters = []
12
+ @groups = {}
13
+ @tracked_globs = []
14
+ @formatters = []
15
+ @minimum_coverage = nil
16
+ @per_file_minimum = nil
17
+ @group_minimums = {}
18
+ @output_dir = File.expand_path('coverage', @root)
19
+ @repo_url = nil
20
+ @repo_branch = 'main'
21
+ @ignore_globs = []
22
+ @only_globs = []
23
+ @parallel_merge_dir = nil
24
+ end
25
+
26
+ # Selection & filtering
27
+ def add_filter(callable_or_pattern = nil, &block)
28
+ predicate =
29
+ if callable_or_pattern.respond_to?(:call)
30
+ callable_or_pattern
31
+ elsif callable_or_pattern
32
+ pattern = callable_or_pattern
33
+ ->(path) { path.match?(pattern) }
34
+ elsif block
35
+ block
36
+ else
37
+ raise ArgumentError, 'add_filter requires a callable, pattern, or block'
38
+ end
39
+ @filters << predicate
40
+ end
41
+
42
+ def ignore_files(*globs) # convenience
43
+ @ignore_globs.concat(globs.flatten)
44
+ end
45
+ alias add_ignore_glob ignore_files
46
+
47
+ def only_files(*globs) # convenience
48
+ @only_globs.concat(globs.flatten)
49
+ end
50
+
51
+ def filtered?(path)
52
+ unless @only_globs.empty?
53
+ return true unless @only_globs.any? { |g| File.fnmatch?(g, path, File::FNM_PATHNAME | File::FNM_EXTGLOB) }
54
+ end
55
+ if @ignore_globs.any? { |g| File.fnmatch?(g, path, File::FNM_PATHNAME | File::FNM_EXTGLOB) }
56
+ return true
57
+ end
58
+ @filters.any? { |f| f.call(path) }
59
+ end
60
+
61
+ def add_group(name, *patterns)
62
+ @groups[name] ||= []
63
+ @groups[name].concat(patterns.flatten)
64
+ end
65
+
66
+ def track_files(*globs)
67
+ @tracked_globs.concat(globs.flatten)
68
+ end
69
+
70
+ def add_formatter(formatter)
71
+ @formatters << formatter
72
+ end
73
+ def enable_html!; require 'getcov/formatter/html'; add_formatter(Getcov::Formatter::HTML); end
74
+ def enable_cobertura!; require 'getcov/formatter/cobertura'; add_formatter(Getcov::Formatter::Cobertura); end
75
+ def enable_lcov!; require 'getcov/formatter/lcov'; add_formatter(Getcov::Formatter::LCOV); end
76
+ def enable_badge!; require 'getcov/formatter/badge'; add_formatter(Getcov::Formatter::Badge); end
77
+
78
+ def add_group_minimum(name, percent)
79
+ @group_minimums[name] = percent.to_f
80
+ end
81
+
82
+ def group_minimum(name)
83
+ @group_minimums[name]
84
+ end
85
+
86
+ def group_for(path)
87
+ @groups.each do |name, patterns|
88
+ return name if patterns.any? { |p| path.match?(p.is_a?(String) ? Regexp.new(Regexp.escape(p)) : p) }
89
+ end
90
+ nil
91
+ end
92
+
93
+ def tracked_paths
94
+ return [] if @tracked_globs.empty?
95
+ @tracked_globs.flat_map { |g| Dir.glob(File.join(@root, g)) }.uniq
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+ require 'fileutils'
3
+
4
+ module Getcov
5
+ module Formatter
6
+ class Badge
7
+ def initialize(config) @config = config end
8
+
9
+ def write(result)
10
+ pct = result.total_coverage.round(0)
11
+ color = case pct
12
+ when 90..100 then '#4c1'
13
+ when 75..89 then '#97CA00'
14
+ when 60..74 then '#dfb317'
15
+ else '#e05d44'
16
+ end
17
+ svg = <<~SVG
18
+ <svg xmlns="http://www.w3.org/2000/svg" width="110" height="20" role="img" aria-label="coverage: #{pct}%">
19
+ <linearGradient id="s" x2="0" y2="100%">
20
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
21
+ <stop offset="1" stop-opacity=".1"/>
22
+ </linearGradient>
23
+ <rect rx="3" width="110" height="20" fill="#555"/>
24
+ <rect rx="3" x="60" width="50" height="20" fill="#{color}"/>
25
+ <path fill="#{color}" d="M60 0h4v20h-4z"/>
26
+ <rect rx="3" width="110" height="20" fill="url(#s)"/>
27
+ <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
28
+ <text x="30" y="14">coverage</text>
29
+ <text x="85" y="14">#{pct}%</text>
30
+ </g>
31
+ </svg>
32
+ SVG
33
+ FileUtils.mkdir_p(@config.output_dir)
34
+ File.write(File.join(@config.output_dir, 'coverage.svg'), svg)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+ require 'fileutils'
3
+ require 'time'
4
+
5
+ module Getcov
6
+ module Formatter
7
+ class Cobertura
8
+ def initialize(config) @config = config end
9
+
10
+ def write(result)
11
+ out_dir = @config.output_dir
12
+ FileUtils.mkdir_p(out_dir)
13
+ File.write(File.join(out_dir, 'coverage.xml'), render(result))
14
+ end
15
+
16
+ def render(result)
17
+ timestamp = (Time.now.to_f * 1000).to_i
18
+ <<~XML
19
+ <?xml version="1.0"?>
20
+ <coverage lines-valid="#{result.files.sum(&:relevant)}" lines-covered="#{result.files.sum(&:covered)}" line-rate="#{(result.total_coverage/100.0).round(4)}" timestamp="#{timestamp}" version="getcov">
21
+ <packages>
22
+ #{packages_xml(result)} </packages>
23
+ </coverage>
24
+ XML
25
+ end
26
+
27
+ private
28
+
29
+ def packages_xml(result)
30
+ groups = result.files.group_by { |fr| fr.group || 'Ungrouped' }
31
+ groups.map do |gname, files|
32
+ <<~PKG
33
+ <package name="#{xml_escape(gname)}">
34
+ <classes>
35
+ #{files.map { |fr| class_xml(fr) }.join}
36
+ </classes>
37
+ </package>
38
+ PKG
39
+ end.join
40
+ end
41
+
42
+ def class_xml(fr)
43
+ rate = fr.relevant.zero? ? 1.0 : (fr.covered.to_f / fr.relevant)
44
+ <<~CLS
45
+ <class name="#{xml_escape(fr.path)}" filename="#{xml_escape(fr.path)}" line-rate="#{rate.round(4)}">
46
+ <lines>
47
+ #{lines_xml(fr)} </lines>
48
+ </class>
49
+ CLS
50
+ end
51
+
52
+ def lines_xml(fr)
53
+ return "" unless fr.hits
54
+ out = String.new
55
+ fr.hits.each_with_index do |hit, idx|
56
+ next if hit.nil?
57
+ ln = idx + 1
58
+ out << %Q{ <line number="#{ln}" hits="#{hit.to_i}"/>
59
+ }
60
+ end
61
+ out
62
+ end
63
+
64
+ def xml_escape(s)
65
+ s.to_s.gsub('&','&amp;').gsub('<','&lt;').gsub('>','&gt;').gsub('"','&quot;').gsub("'", '&apos;')
66
+ end
67
+ end
68
+ end
69
+ end