undercover 0.6.4 → 0.7.1
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/ruby.yml +4 -4
- data/.tool-versions +1 -0
- data/CHANGELOG.md +26 -3
- data/Gemfile +2 -1
- data/README.md +38 -14
- data/docs/actions.yml +19 -0
- data/docs/circleci_advanced.yml +3 -3
- data/docs/circleci_config.yml +1 -1
- data/lib/undercover/changeset.rb +15 -28
- data/lib/undercover/cli.rb +3 -2
- data/lib/undercover/filter_set.rb +17 -0
- data/lib/undercover/lcov_parser.rb +14 -4
- data/lib/undercover/options.rb +29 -1
- data/lib/undercover/result.rb +17 -4
- data/lib/undercover/root_to_relative_paths.rb +24 -0
- data/lib/undercover/simplecov_formatter.rb +59 -0
- data/lib/undercover/simplecov_result_adapter.rb +63 -0
- data/lib/undercover/version.rb +1 -1
- data/lib/undercover.rb +26 -13
- data/undercover.gemspec +2 -0
- metadata +35 -4
- data/docs/codeship.md +0 -19
- data/docs/travis.yml +0 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cb507959b33f47004438d9cba6c7cf02dfa8d439e20e34caf341e2f2375fcc04
|
4
|
+
data.tar.gz: 3f801011026a04205318a607b303199aa940c01c4421d06ff2ec99a9edba5886
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0540c422fa1729247cd1136402c1246c7cec3e4276077be48368397d26947c2ca41c8247aa6488f5e1aa8d0690f239c0ef9360da8c9ba912bb3d00b92bfbafde
|
7
|
+
data.tar.gz: ced783b94a0f156d89bb8fba42c737282e879320a62cca94e00021776683873eca03d0acf216c6bdb5c56b2b9c7f52ba3e313eed378c230dace2746e0ec5ee8e
|
data/.github/workflows/ruby.yml
CHANGED
@@ -25,18 +25,18 @@ jobs:
|
|
25
25
|
undercover --compare master
|
26
26
|
- uses: actions/upload-artifact@v4
|
27
27
|
with:
|
28
|
-
name: undercover-${{ matrix.ruby }}
|
29
|
-
path: coverage/
|
28
|
+
name: undercover-${{ matrix.ruby }}-coverage
|
29
|
+
path: coverage/undercover_coverage.json
|
30
30
|
coverage:
|
31
31
|
runs-on: ubuntu-latest
|
32
32
|
needs: build
|
33
33
|
steps:
|
34
34
|
- uses: actions/download-artifact@v4
|
35
35
|
with:
|
36
|
-
name: undercover-3.4
|
36
|
+
name: undercover-3.4-coverage
|
37
37
|
- name: Upload coverage
|
38
38
|
run: |
|
39
39
|
ruby -e "$(curl -s https://undercover-ci.com/uploader.rb)" -- \
|
40
40
|
--repo grodowski/undercover \
|
41
41
|
--commit ${{ github.event.pull_request.head.sha || github.sha }} \
|
42
|
-
--
|
42
|
+
--simplecov /home/runner/work/undercover/undercover/undercover_coverage.json
|
data/.tool-versions
CHANGED
data/CHANGELOG.md
CHANGED
@@ -6,6 +6,25 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|
6
6
|
|
7
7
|
## [Unreleased]
|
8
8
|
|
9
|
+
# [0.7.0] - 2025-07-03
|
10
|
+
|
11
|
+
### Added
|
12
|
+
- New native SimpleCov formatter that generates coverage data directly without requiring LCOV conversion ([#223](https://github.com/grodowski/undercover/pull/223))
|
13
|
+
- `--simplecov -s` CLI option to specify coverage JSON file path as an alternative to LCOV and future default ([#223](https://github.com/grodowski/undercover/pull/223))
|
14
|
+
- Improved path handling for projects running in nested subdirectories or monorepo setups ([#223](https://github.com/grodowski/undercover/pull/223))
|
15
|
+
|
16
|
+
### Fixed
|
17
|
+
- `:nocov:` support to skip coverage analysis for specific code blocks works again after a regression in `0.6+` ([#223](https://github.com/grodowski/undercover/pull/223))
|
18
|
+
|
19
|
+
# [0.6.6] - 2025-07-01
|
20
|
+
|
21
|
+
- Bugfix in `max_warnings_limit` following ([#229](https://github.com/grodowski/undercover/pull/229))
|
22
|
+
|
23
|
+
# [0.6.5] - 2025-07-01
|
24
|
+
|
25
|
+
### Fixed
|
26
|
+
- Improved performance for large PRs with lazy diff enumeration ([#229](https://github.com/grodowski/undercover/pull/229))
|
27
|
+
|
9
28
|
# [0.6.4] - 2025-03-29
|
10
29
|
|
11
30
|
### Fixed
|
@@ -148,9 +167,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|
148
167
|
### Added
|
149
168
|
- First release of `undercover` 🎉
|
150
169
|
|
151
|
-
[Unreleased]: https://github.com/grodowski/undercover/compare/v0.
|
152
|
-
[0.
|
153
|
-
[0.6.
|
170
|
+
[Unreleased]: https://github.com/grodowski/undercover/compare/v0.7.0...HEAD
|
171
|
+
[0.7.0]: https://github.com/grodowski/undercover/compare/v0.6.6...v0.7.0
|
172
|
+
[0.6.6]: https://github.com/grodowski/undercover/compare/v0.6.5...0.6.6
|
173
|
+
[0.6.5]: https://github.com/grodowski/undercover/compare/v0.6.4...0.6.5
|
174
|
+
[0.6.4]: https://github.com/grodowski/undercover/compare/v0.6.3...v0.6.4
|
175
|
+
[0.6.3]: https://github.com/grodowski/undercover/compare/v0.6.0...v0.6.3
|
176
|
+
[0.6.0]: https://github.com/grodowski/undercover/compare/v0.5.0...v0.6.0
|
154
177
|
[0.5.0]: https://github.com/grodowski/undercover/compare/v0.4.7...v0.5.0
|
155
178
|
[0.4.7]: https://github.com/grodowski/undercover/compare/v0.4.6...v0.4.7
|
156
179
|
[0.4.6]: https://github.com/grodowski/undercover/compare/v0.4.5...v0.4.6
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -9,7 +9,6 @@ Works with any Ruby CI pipeline as well as locally as a CLI.
|
|
9
9
|
|
10
10
|
|
11
11
|
[](https://github.com/grodowski/undercover/actions)
|
12
|
-
[](https://codeclimate.com/github/grodowski/undercover/maintainability)
|
13
12
|

|
14
13
|
|
15
14
|
A sample output of `undercover` ran before a commit may look like this:
|
@@ -36,34 +35,58 @@ Or install it yourself as:
|
|
36
35
|
|
37
36
|
$ gem install undercover
|
38
37
|
|
39
|
-
## Setting up
|
38
|
+
## Setting up coverage reporting
|
40
39
|
|
41
|
-
To make your specs or tests compatible with `undercover
|
40
|
+
To make your specs or tests compatible with `undercover`, please add `undercover` to your gemfile to use the undercover formatter the test helper.
|
42
41
|
|
43
42
|
```ruby
|
44
43
|
# Gemfile
|
45
44
|
group :test do
|
46
|
-
gem '
|
47
|
-
gem 'simplecov-lcov'
|
45
|
+
gem 'undercover'
|
48
46
|
end
|
49
47
|
|
50
48
|
# the very top of spec_helper.rb
|
51
49
|
require 'simplecov'
|
52
|
-
require '
|
53
|
-
|
54
|
-
|
50
|
+
require 'undercover/simplecov_formatter'
|
51
|
+
|
52
|
+
# optional, will default to coverage.json
|
53
|
+
SimpleCov::Formatter::Undercover.output_filename = 'my_project_coverage.json'
|
54
|
+
SimpleCov.formatter = SimpleCov::Formatter::Undercover
|
55
|
+
|
55
56
|
SimpleCov.start do
|
56
57
|
add_filter(/^\/spec\//) # For RSpec
|
57
58
|
add_filter(/^\/test\//) # For Minitest
|
58
59
|
enable_coverage(:branch) # Report branch coverage to trigger branch-level undercover warnings
|
59
60
|
end
|
61
|
+
# ...
|
62
|
+
```
|
60
63
|
|
61
|
-
|
64
|
+
Then run your test suite once through to generate the initial coverage file before you can run the `undercover` command.
|
62
65
|
|
63
|
-
|
66
|
+
## Upgrading from pre-0.7.0
|
67
|
+
|
68
|
+
If you're upgrading from an older version of undercover that used LCOV, you can migrate to the new SimpleCov formatter:
|
69
|
+
|
70
|
+
1. Add `gem 'undercover'` to your test group
|
71
|
+
2. Replace the LCOV formatter setup with the new SimpleCov formatter
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
# Gemfile
|
75
|
+
group :test do
|
76
|
+
gem 'simplecov'
|
77
|
+
gem 'simplecov_json_formatter'
|
78
|
+
gem 'undercover'
|
79
|
+
end
|
80
|
+
|
81
|
+
# spec_helper.rb
|
82
|
+
require 'simplecov'
|
83
|
+
require 'undercover/simplecov_formatter'
|
84
|
+
SimpleCov.formatter = SimpleCov::Formatter::Undercover
|
64
85
|
```
|
65
86
|
|
66
|
-
|
87
|
+
3. Update CLI usage: Use `--simplecov` flag instead of `--lcov`, or rely on auto-detection of `coverage/coverage.json`
|
88
|
+
|
89
|
+
Note: LCOV support will be deprecated in a future release, but remains fully functional for existing projects.
|
67
90
|
|
68
91
|
## Usage
|
69
92
|
|
@@ -80,11 +103,10 @@ undercover --compare origin/master
|
|
80
103
|
```
|
81
104
|
|
82
105
|
Check out `docs/` for CI configuration examples:
|
83
|
-
- [
|
106
|
+
- [GitHub Actions](docs/actions.yml)
|
84
107
|
- [CircleCI - simple](docs/circleci_config.yml)
|
85
108
|
- [CircleCI - advanced](docs/circleci_advanced.yml)
|
86
109
|
- [Semaphore](docs/semaphore.yml)
|
87
|
-
- [Codeship](docs/codeship.md)
|
88
110
|
|
89
111
|
Merging coverage results ([sample gist](https://gist.github.com/grodowski/9744ff91034dce8df20c2a8210409fb0)) is required for parallel tests before processing with `undercover`.
|
90
112
|
|
@@ -105,11 +127,13 @@ Options can be passed when running the command from the command line:
|
|
105
127
|
|
106
128
|
```sh
|
107
129
|
Usage: undercover [options]
|
108
|
-
-
|
130
|
+
-s, --simplecov path SimpleCov JSON report file
|
131
|
+
-l, --lcov path LCOV report file path (to be deprecated)
|
109
132
|
-p, --path path Project directory
|
110
133
|
-g, --git-dir dir Override `.git` with a custom directory
|
111
134
|
-c, --compare ref Generate coverage warnings for all changes after `ref`
|
112
135
|
-r, --ruby-syntax ver Ruby syntax version, one of: current, ruby18, ruby19, ruby20, ruby21, ruby22, ruby23, ruby24, ruby25, ruby26, ruby30, ruby31, ruby32, ruby33
|
136
|
+
-w, --max-warnings limit Maximum number of warnings to generate before stopping analysis. Useful as a performance improvement for large diffs.
|
113
137
|
-f, --include-files globs Include files matching specified glob patterns (comma separated). Defaults to '*.rb,*.rake,*.ru,Rakefile'
|
114
138
|
-x, --exclude-files globs Skip files matching specified glob patterns (comma separated). Empty by default.
|
115
139
|
-h, --help Prints this help
|
data/docs/actions.yml
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
name: Tests & Undercover
|
2
|
+
on: [push, pull_request]
|
3
|
+
jobs:
|
4
|
+
build:
|
5
|
+
runs-on: ubuntu-latest
|
6
|
+
steps:
|
7
|
+
- uses: actions/checkout@v4
|
8
|
+
- name: Set up Ruby 3.4
|
9
|
+
uses: ruby/setup-ruby@v1
|
10
|
+
with:
|
11
|
+
ruby-version: 3.4
|
12
|
+
- name: Build and test
|
13
|
+
env:
|
14
|
+
RAILS_ENV: test
|
15
|
+
run: |
|
16
|
+
gem install bundler
|
17
|
+
bundle install --jobs 4 --retry 3
|
18
|
+
bundle exec rake test
|
19
|
+
undercover --simplecov coverage/coverage.json --compare origin/master
|
data/docs/circleci_advanced.yml
CHANGED
@@ -20,7 +20,7 @@ jobs:
|
|
20
20
|
bundle exec rspec
|
21
21
|
- run:
|
22
22
|
name: Store coverage report
|
23
|
-
command: mv coverage/
|
23
|
+
command: mv coverage/coverage.json /tmp/coverage/
|
24
24
|
- persist_to_workspace:
|
25
25
|
root: /tmp/coverage
|
26
26
|
paths: .
|
@@ -33,13 +33,13 @@ jobs:
|
|
33
33
|
steps:
|
34
34
|
- checkout
|
35
35
|
- attach_workspace:
|
36
|
-
at: /tmp/coverage # gives access to project's
|
36
|
+
at: /tmp/coverage # gives access to project's coverage report
|
37
37
|
- run:
|
38
38
|
name: Check coverage
|
39
39
|
command: |
|
40
40
|
sudo apt-get install cmake
|
41
41
|
gem install undercover
|
42
|
-
undercover --
|
42
|
+
undercover --simplecov /tmp/coverage/coverage.json \
|
43
43
|
--compare origin/master
|
44
44
|
|
45
45
|
workflows:
|
data/docs/circleci_config.yml
CHANGED
data/lib/undercover/changeset.rb
CHANGED
@@ -8,31 +8,12 @@ module Undercover
|
|
8
8
|
class Changeset
|
9
9
|
T_ZERO = Time.strptime('0', '%s').freeze
|
10
10
|
|
11
|
-
|
12
|
-
include Enumerable
|
13
|
-
|
14
|
-
attr_reader :files
|
15
|
-
|
16
|
-
def_delegators :files, :each, :<=>
|
17
|
-
|
18
|
-
def initialize(dir, compare_base = nil)
|
11
|
+
def initialize(dir, compare_base = nil, filter_set = nil)
|
19
12
|
@dir = dir
|
20
13
|
@repo = Rugged::Repository.new(dir)
|
21
14
|
@repo.workdir = Pathname.new(dir).dirname.to_s # TODO: can replace?
|
22
15
|
@compare_base = compare_base
|
23
|
-
@
|
24
|
-
end
|
25
|
-
|
26
|
-
def update
|
27
|
-
full_diff.each_patch do |patch|
|
28
|
-
filepath = patch.delta.new_file[:path]
|
29
|
-
line_nums = patch.each_hunk.map do |hunk|
|
30
|
-
# TODO: optimise this to use line ranges!
|
31
|
-
hunk.lines.select(&:addition?).map(&:new_lineno)
|
32
|
-
end.flatten
|
33
|
-
@files[filepath] = line_nums if line_nums.any?
|
34
|
-
end
|
35
|
-
self
|
16
|
+
@filter_set = filter_set
|
36
17
|
end
|
37
18
|
|
38
19
|
def last_modified
|
@@ -46,18 +27,24 @@ module Undercover
|
|
46
27
|
end
|
47
28
|
|
48
29
|
def file_paths
|
49
|
-
|
30
|
+
full_diff.deltas.map { |d| d.new_file[:path] }.sort
|
50
31
|
end
|
51
32
|
|
52
33
|
def each_changed_line
|
53
|
-
|
54
|
-
|
34
|
+
full_diff.each_patch do |patch|
|
35
|
+
filepath = patch.delta.new_file[:path]
|
36
|
+
next if filter_set && !filter_set.include?(filepath)
|
37
|
+
|
38
|
+
patch.each_hunk do |hunk|
|
39
|
+
hunk.lines.select(&:addition?).each do |line|
|
40
|
+
yield filepath, line.new_lineno
|
41
|
+
end
|
42
|
+
end
|
55
43
|
end
|
56
44
|
end
|
57
45
|
|
58
|
-
# TODO: refactor to a standalone validator (depending on changeset AND lcov)
|
59
46
|
def validate(lcov_report_path)
|
60
|
-
return :no_changes if
|
47
|
+
return :no_changes if full_diff.deltas.empty?
|
61
48
|
|
62
49
|
:stale_coverage if last_modified > File.mtime(lcov_report_path)
|
63
50
|
end
|
@@ -68,7 +55,7 @@ module Undercover
|
|
68
55
|
# as it makes sense to run Undercover with the most recent file versions
|
69
56
|
def full_diff
|
70
57
|
base = compare_base_obj || head
|
71
|
-
base.diff(repo.index).merge!(repo.diff_workdir(head))
|
58
|
+
@full_diff ||= base.diff(repo.index).merge!(repo.diff_workdir(head))
|
72
59
|
end
|
73
60
|
|
74
61
|
def compare_base_obj
|
@@ -83,6 +70,6 @@ module Undercover
|
|
83
70
|
repo.head.target
|
84
71
|
end
|
85
72
|
|
86
|
-
attr_reader :repo, :compare_base
|
73
|
+
attr_reader :repo, :compare_base, :filter_set
|
87
74
|
end
|
88
75
|
end
|
data/lib/undercover/cli.rb
CHANGED
@@ -24,7 +24,7 @@ module Undercover
|
|
24
24
|
def self.run_report(opts)
|
25
25
|
report = Undercover::Report.new(changeset(opts), opts).build
|
26
26
|
|
27
|
-
error = report.validate(opts.lcov)
|
27
|
+
error = report.validate(opts.simplecov_resultset || opts.lcov)
|
28
28
|
if error
|
29
29
|
puts(WARNINGS_TO_S[error])
|
30
30
|
return 0 if error == :no_changes
|
@@ -43,7 +43,8 @@ module Undercover
|
|
43
43
|
|
44
44
|
def self.changeset(opts)
|
45
45
|
git_dir = File.join(opts.path, opts.git_dir)
|
46
|
-
Undercover::
|
46
|
+
filter_set = Undercover::FilterSet.new(opts.glob_allow_filters, opts.glob_reject_filters)
|
47
|
+
Undercover::Changeset.new(git_dir, opts.compare, filter_set)
|
47
48
|
end
|
48
49
|
end
|
49
50
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Undercover
|
4
|
+
class FilterSet
|
5
|
+
attr_reader :allow_filters, :reject_filters
|
6
|
+
|
7
|
+
def initialize(allow_filters, reject_filters)
|
8
|
+
@allow_filters = allow_filters || []
|
9
|
+
@reject_filters = reject_filters || []
|
10
|
+
end
|
11
|
+
|
12
|
+
def include?(filepath)
|
13
|
+
fnmatch = proc { |glob| File.fnmatch(glob, filepath) }
|
14
|
+
allow_filters.any?(fnmatch) && reject_filters.none?(fnmatch)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -1,19 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'undercover/root_to_relative_paths'
|
4
|
+
|
3
5
|
module Undercover
|
4
6
|
LcovParseError = Class.new(StandardError)
|
5
7
|
|
6
8
|
class LcovParser
|
9
|
+
include RootToRelativePaths
|
10
|
+
|
7
11
|
attr_reader :io, :source_files
|
8
12
|
|
9
|
-
def initialize(lcov_io)
|
13
|
+
def initialize(lcov_io, opts)
|
10
14
|
@io = lcov_io
|
11
15
|
@source_files = {}
|
16
|
+
@code_dir = opts&.path
|
12
17
|
end
|
13
18
|
|
14
|
-
def self.parse(lcov_report_path)
|
19
|
+
def self.parse(lcov_report_path, opts = nil)
|
15
20
|
lcov_io = File.open(lcov_report_path)
|
16
|
-
new(lcov_io).parse
|
21
|
+
new(lcov_io, opts).parse
|
17
22
|
end
|
18
23
|
|
19
24
|
def parse
|
@@ -24,7 +29,7 @@ module Undercover
|
|
24
29
|
|
25
30
|
def coverage(filepath)
|
26
31
|
_filename, coverage = source_files.find do |relative_path, _|
|
27
|
-
relative_path == filepath
|
32
|
+
relative_path == fix_relative_filepath(filepath)
|
28
33
|
end
|
29
34
|
coverage || []
|
30
35
|
end
|
@@ -47,6 +52,11 @@ module Undercover
|
|
47
52
|
total_f.round(3)
|
48
53
|
end
|
49
54
|
|
55
|
+
def skipped?(_filepath, _line_no)
|
56
|
+
# this is why lcov parser will be deprecated
|
57
|
+
false
|
58
|
+
end
|
59
|
+
|
50
60
|
private
|
51
61
|
|
52
62
|
# rubocop:disable Metrics/MethodLength, Style/SpecialGlobalVars, Metrics/AbcSize
|
data/lib/undercover/options.rb
CHANGED
@@ -34,6 +34,7 @@ module Undercover
|
|
34
34
|
DEFAULT_FILE_EXCLUDE_GLOBS = %w[test/* spec/* db/* config/* *_test.rb *_spec.rb].freeze
|
35
35
|
|
36
36
|
attr_accessor :lcov,
|
37
|
+
:simplecov_resultset,
|
37
38
|
:path,
|
38
39
|
:git_dir,
|
39
40
|
:compare,
|
@@ -41,7 +42,8 @@ module Undercover
|
|
41
42
|
:run_mode,
|
42
43
|
:file_scope,
|
43
44
|
:glob_allow_filters,
|
44
|
-
:glob_reject_filters
|
45
|
+
:glob_reject_filters,
|
46
|
+
:max_warnings_limit
|
45
47
|
|
46
48
|
def initialize
|
47
49
|
@run_mode = DIFF_TRIGGER_LINE
|
@@ -51,6 +53,7 @@ module Undercover
|
|
51
53
|
self.git_dir = '.git'
|
52
54
|
self.glob_allow_filters = DEFAULT_FILE_INCLUDE_GLOBS
|
53
55
|
self.glob_reject_filters = DEFAULT_FILE_EXCLUDE_GLOBS
|
56
|
+
self.max_warnings_limit = nil
|
54
57
|
end
|
55
58
|
|
56
59
|
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
@@ -66,18 +69,23 @@ module Undercover
|
|
66
69
|
end
|
67
70
|
|
68
71
|
opts.on_tail('--version', 'Show version') do
|
72
|
+
# :nocov:
|
69
73
|
puts VERSION
|
70
74
|
exit
|
75
|
+
# :nocov:
|
71
76
|
end
|
72
77
|
|
73
78
|
lcov_path_option(opts)
|
79
|
+
resultset_path_option(opts)
|
74
80
|
project_path_option(opts)
|
75
81
|
git_dir_option(opts)
|
76
82
|
compare_option(opts)
|
77
83
|
ruby_syntax_option(opts)
|
84
|
+
max_warnings_limit_option(opts)
|
78
85
|
file_filters(opts)
|
79
86
|
end.parse(args)
|
80
87
|
|
88
|
+
guess_resultset_path unless simplecov_resultset
|
81
89
|
guess_lcov_path unless lcov
|
82
90
|
self
|
83
91
|
end
|
@@ -109,6 +117,13 @@ module Undercover
|
|
109
117
|
end
|
110
118
|
end
|
111
119
|
|
120
|
+
def resultset_path_option(parser)
|
121
|
+
desc = 'SimpleCov::Formatter::Undercover output file path (alternative to LCOV that will become default)'
|
122
|
+
parser.on('-s', '--simplecov path', desc) do |path|
|
123
|
+
self.simplecov_resultset = path
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
112
127
|
def project_path_option(parser)
|
113
128
|
parser.on('-p', '--path path', 'Project directory') do |path|
|
114
129
|
self.path = path
|
@@ -137,6 +152,19 @@ module Undercover
|
|
137
152
|
end
|
138
153
|
end
|
139
154
|
|
155
|
+
def max_warnings_limit_option(parser)
|
156
|
+
desc = 'Maximum number of warnings to generate before stopping analysis'
|
157
|
+
parser.on('-w', '--max-warnings limit', Integer, desc) do |limit|
|
158
|
+
self.max_warnings_limit = limit
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def guess_resultset_path
|
163
|
+
cwd = Pathname.new(File.expand_path(path))
|
164
|
+
try_path = File.join(cwd, 'coverage', 'coverage.json')
|
165
|
+
self.simplecov_resultset = try_path if File.exist?(try_path)
|
166
|
+
end
|
167
|
+
|
140
168
|
def guess_lcov_path
|
141
169
|
cwd = Pathname.new(File.expand_path(path))
|
142
170
|
self.lcov = File.join(cwd, 'coverage', 'lcov', "#{cwd.split.last}.lcov")
|
data/lib/undercover/result.rb
CHANGED
@@ -3,15 +3,18 @@
|
|
3
3
|
require 'forwardable'
|
4
4
|
|
5
5
|
module Undercover
|
6
|
-
class Result
|
6
|
+
class Result # rubocop:disable Metrics/ClassLength
|
7
7
|
extend Forwardable
|
8
8
|
|
9
|
-
attr_reader :node, :coverage, :file_path
|
9
|
+
attr_reader :node, :coverage, :coverage_adapter, :file_path
|
10
10
|
|
11
11
|
def_delegators :node, :first_line, :last_line, :name
|
12
|
+
def_delegators :coverage_adapter, :skipped?
|
12
13
|
|
13
|
-
def initialize(node,
|
14
|
+
def initialize(node, coverage_adapter, file_path) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
14
15
|
@node = node
|
16
|
+
@coverage_adapter = coverage_adapter
|
17
|
+
file_cov = coverage_adapter.coverage(file_path)
|
15
18
|
@coverage = file_cov.select do |ln, _|
|
16
19
|
if first_line == last_line
|
17
20
|
ln == first_line
|
@@ -21,6 +24,7 @@ module Undercover
|
|
21
24
|
ln > first_line && ln < last_line
|
22
25
|
end
|
23
26
|
end
|
27
|
+
|
24
28
|
@file_path = file_path
|
25
29
|
@flagged = false
|
26
30
|
end
|
@@ -33,7 +37,8 @@ module Undercover
|
|
33
37
|
@flagged
|
34
38
|
end
|
35
39
|
|
36
|
-
def uncovered?(line_no)
|
40
|
+
def uncovered?(line_no) # rubocop:disable Metrics/AbcSize
|
41
|
+
return false if skipped?(file_path, line_no)
|
37
42
|
return true if coverage.empty?
|
38
43
|
|
39
44
|
# check branch coverage for line_no
|
@@ -55,6 +60,11 @@ module Undercover
|
|
55
60
|
|
56
61
|
lines = {}
|
57
62
|
coverage.each do |ln, block_or_line_cov, _, branch_cov|
|
63
|
+
if skipped?(file_path, ln)
|
64
|
+
lines[ln] = 1
|
65
|
+
next
|
66
|
+
end
|
67
|
+
|
58
68
|
lines[ln] = 1 unless lines.key?(ln)
|
59
69
|
if branch_cov
|
60
70
|
lines[ln] = 0 if branch_cov.zero?
|
@@ -93,6 +103,9 @@ module Undercover
|
|
93
103
|
formatted_line = "#{num.to_s.rjust(pad)}: #{line}"
|
94
104
|
if line.strip.empty?
|
95
105
|
Rainbow(formatted_line).darkgray.dark
|
106
|
+
elsif skipped?(file_path, num)
|
107
|
+
Rainbow(formatted_line).darkgray.dark +
|
108
|
+
Rainbow(' skipped with :nocov:').italic.darkgray.dark
|
96
109
|
elsif covered.nil?
|
97
110
|
Rainbow(formatted_line).darkgray.dark +
|
98
111
|
Rainbow(' hits: n/a').italic.darkgray.dark
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Undercover
|
4
|
+
module RootToRelativePaths
|
5
|
+
# Needed if undercover is running inside nested subdirectories (e.g. in a monorepo app), where
|
6
|
+
# the git paths are rooted deeper than the paths in the coverage report.
|
7
|
+
# If that is the case, trim the git filepath to match the local relative path, assumming undercover is
|
8
|
+
# running in the correct directory (has to be equal to SimpleCov.root for paths to match)
|
9
|
+
# @param filepath[String]
|
10
|
+
# @return String
|
11
|
+
def fix_relative_filepath(filepath)
|
12
|
+
return filepath unless @code_dir
|
13
|
+
|
14
|
+
code_dir_abs = File.expand_path(@code_dir)
|
15
|
+
|
16
|
+
if Pathname.new(Dir.pwd).ascend.any? { |p| p.to_s == code_dir_abs }
|
17
|
+
prefix_to_skip = Pathname.new(Dir.pwd).relative_path_from(code_dir_abs).to_s
|
18
|
+
return filepath.delete_prefix(prefix_to_skip).gsub(/\A\//, '')
|
19
|
+
end
|
20
|
+
|
21
|
+
filepath
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'simplecov_json_formatter'
|
4
|
+
|
5
|
+
# Patch ResultExporter to allow setting a custom export_path
|
6
|
+
module SimpleCovJSONFormatter
|
7
|
+
class ResultExporter
|
8
|
+
def export_path
|
9
|
+
# :nocov:
|
10
|
+
File.join(SimpleCov.coverage_path, SimpleCov::Formatter::Undercover.output_filename || FILENAME)
|
11
|
+
# :nocov:
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module Undercover
|
17
|
+
class ResultHashFormatterWithRoot < SimpleCovJSONFormatter::ResultHashFormatter
|
18
|
+
def format
|
19
|
+
formatted_result[:meta] = {timestamp: @result.created_at.to_i}
|
20
|
+
format_files
|
21
|
+
add_undercover_meta_fields
|
22
|
+
formatted_result
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def add_undercover_meta_fields
|
28
|
+
formatted_result.tap do |result|
|
29
|
+
result[:meta].merge!(simplecov_root: SimpleCov.root)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# format_files uses relative path as keys, as opposed to the superclass method
|
34
|
+
def format_files
|
35
|
+
formatted_result[:coverage] ||= {}
|
36
|
+
@result.files.each do |source_file|
|
37
|
+
path = source_file.project_filename.delete_prefix('/')
|
38
|
+
formatted_result[:coverage][path] = format_source_file(source_file)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class UndercoverSimplecovFormatter < SimpleCov::Formatter::JSONFormatter
|
44
|
+
class << self
|
45
|
+
attr_accessor :output_filename
|
46
|
+
end
|
47
|
+
|
48
|
+
def format_result(result)
|
49
|
+
result_hash_formater = ResultHashFormatterWithRoot.new(result)
|
50
|
+
result_hash_formater.format
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
module SimpleCov
|
56
|
+
module Formatter
|
57
|
+
Undercover = ::Undercover::UndercoverSimplecovFormatter
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'undercover/root_to_relative_paths'
|
4
|
+
|
5
|
+
module Undercover
|
6
|
+
class SimplecovResultAdapter
|
7
|
+
include RootToRelativePaths
|
8
|
+
|
9
|
+
attr_reader :simplecov_result
|
10
|
+
|
11
|
+
# @param file[File] JSON file supplied by SimpleCov::Formatter::Undercover
|
12
|
+
# @return SimplecovResultAdapter
|
13
|
+
def self.parse(file, opts = nil)
|
14
|
+
# :nocov:
|
15
|
+
result_h = JSON.parse(file.read)
|
16
|
+
raise ArgumentError, 'empty SimpleCov' if result_h.empty?
|
17
|
+
|
18
|
+
new(result_h, opts)
|
19
|
+
# :nocov:
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param simplecov_result[SimpleCov::Result]
|
23
|
+
def initialize(simplecov_result, opts)
|
24
|
+
@simplecov_result = simplecov_result
|
25
|
+
@code_dir = opts&.path
|
26
|
+
end
|
27
|
+
|
28
|
+
# @param filepath[String]
|
29
|
+
# @return Array tuples (lines) and quadruples (branches) compatible with LcovParser
|
30
|
+
def coverage(filepath) # rubocop:disable Metrics/MethodLength
|
31
|
+
source_file = find_file(filepath)
|
32
|
+
|
33
|
+
return [] unless source_file
|
34
|
+
|
35
|
+
lines = source_file['lines'].map.with_index do |line_coverage, idx|
|
36
|
+
[idx + 1, line_coverage] if line_coverage
|
37
|
+
end.compact
|
38
|
+
branch_idx = 0
|
39
|
+
branches = source_file['branches'].map do |branch|
|
40
|
+
branch_idx += 1
|
41
|
+
[branch['start_line'], 0, branch_idx, branch['coverage']]
|
42
|
+
end
|
43
|
+
lines + branches
|
44
|
+
end
|
45
|
+
|
46
|
+
def skipped?(filepath, line_no)
|
47
|
+
source_file = find_file(filepath)
|
48
|
+
return false unless source_file
|
49
|
+
|
50
|
+
source_file['lines'][line_no - 1] == 'ignored'
|
51
|
+
end
|
52
|
+
|
53
|
+
# unused for now
|
54
|
+
def total_coverage; end
|
55
|
+
def total_branch_coverage; end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def find_file(filepath)
|
60
|
+
simplecov_result['coverage'][fix_relative_filepath(filepath)]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/undercover/version.rb
CHANGED
data/lib/undercover.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
$LOAD_PATH << 'lib'
|
4
|
+
require 'json'
|
4
5
|
require 'imagen'
|
5
6
|
require 'rainbow'
|
6
7
|
require 'bigdecimal'
|
7
8
|
require 'forwardable'
|
9
|
+
require 'simplecov'
|
8
10
|
|
9
11
|
require 'undercover/lcov_parser'
|
10
12
|
require 'undercover/result'
|
@@ -12,6 +14,8 @@ require 'undercover/cli'
|
|
12
14
|
require 'undercover/changeset'
|
13
15
|
require 'undercover/formatter'
|
14
16
|
require 'undercover/options'
|
17
|
+
require 'undercover/filter_set'
|
18
|
+
require 'undercover/simplecov_result_adapter'
|
15
19
|
require 'undercover/version'
|
16
20
|
|
17
21
|
module Undercover
|
@@ -21,29 +25,36 @@ module Undercover
|
|
21
25
|
|
22
26
|
attr_reader :changeset,
|
23
27
|
:lcov,
|
28
|
+
:simplecov_resultset,
|
24
29
|
:results,
|
25
30
|
:code_dir,
|
26
|
-
:
|
31
|
+
:filter_set,
|
32
|
+
:max_warnings_limit
|
27
33
|
|
28
34
|
# Initializes a new Undercover::Report
|
29
35
|
#
|
30
36
|
# @param changeset [Undercover::Changeset]
|
31
37
|
# @param opts [Undercover::Options]
|
32
38
|
def initialize(changeset, opts)
|
33
|
-
|
39
|
+
if opts.simplecov_resultset
|
40
|
+
@simplecov_resultset = SimplecovResultAdapter.parse(File.open(opts.simplecov_resultset), opts)
|
41
|
+
end
|
42
|
+
@lcov = LcovParser.parse(File.open(opts.lcov), opts) if opts.lcov
|
43
|
+
|
34
44
|
@code_dir = opts.path
|
35
|
-
@changeset = changeset
|
36
|
-
@
|
37
|
-
|
38
|
-
reject: opts.glob_reject_filters
|
39
|
-
}
|
45
|
+
@changeset = changeset
|
46
|
+
@filter_set = FilterSet.new(opts.glob_allow_filters, opts.glob_reject_filters)
|
47
|
+
@max_warnings_limit = opts.max_warnings_limit
|
40
48
|
@loaded_files = {}
|
41
49
|
@results = {}
|
42
50
|
end
|
43
51
|
|
44
52
|
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
45
53
|
def build
|
54
|
+
flag_count = 0
|
46
55
|
changeset.each_changed_line do |filepath, line_no|
|
56
|
+
break if max_warnings_limit && flag_count >= max_warnings_limit
|
57
|
+
|
47
58
|
dist_from_line_no = lambda do |res|
|
48
59
|
return BigDecimal::INFINITY if line_no < res.first_line
|
49
60
|
|
@@ -61,7 +72,10 @@ module Undercover
|
|
61
72
|
next unless loaded_files[filepath]
|
62
73
|
|
63
74
|
res = loaded_files[filepath].min(&dist_from_line_no_sorter)
|
64
|
-
|
75
|
+
if res.uncovered?(line_no) && !res.flagged?
|
76
|
+
res.flag
|
77
|
+
flag_count += 1
|
78
|
+
end
|
65
79
|
results[filepath] ||= Set.new
|
66
80
|
results[filepath] << res
|
67
81
|
end
|
@@ -96,9 +110,6 @@ module Undercover
|
|
96
110
|
def load_and_parse_file(filepath)
|
97
111
|
key = filepath.gsub(/^\.\//, '')
|
98
112
|
return if loaded_files[key]
|
99
|
-
|
100
|
-
coverage = lcov.coverage(filepath)
|
101
|
-
|
102
113
|
return unless include_file?(filepath)
|
103
114
|
|
104
115
|
root_ast = Imagen::Node::Root.new.build_from_file(
|
@@ -106,6 +117,9 @@ module Undercover
|
|
106
117
|
)
|
107
118
|
return if root_ast.children.empty?
|
108
119
|
|
120
|
+
# lcov will be deprecated at some point and we'll be able to refactor harder
|
121
|
+
coverage = simplecov_resultset || lcov
|
122
|
+
|
109
123
|
loaded_files[key] = []
|
110
124
|
root_ast.find_all(->(node) { !node.is_a?(Imagen::Node::Root) }).each do |imagen_node|
|
111
125
|
loaded_files[key] << Result.new(imagen_node, coverage, filepath)
|
@@ -114,8 +128,7 @@ module Undercover
|
|
114
128
|
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
115
129
|
|
116
130
|
def include_file?(filepath)
|
117
|
-
|
118
|
-
glob_filters[:allow].any?(fnmatch) && glob_filters[:reject].none?(fnmatch)
|
131
|
+
filter_set.include?(filepath)
|
119
132
|
end
|
120
133
|
end
|
121
134
|
end
|
data/undercover.gemspec
CHANGED
@@ -32,4 +32,6 @@ Gem::Specification.new do |spec|
|
|
32
32
|
spec.add_dependency 'imagen', '>= 0.2.0'
|
33
33
|
spec.add_dependency 'rainbow', '>= 2.1', '< 4.0'
|
34
34
|
spec.add_dependency 'rugged', '>= 0.27', '< 1.10'
|
35
|
+
spec.add_dependency 'simplecov'
|
36
|
+
spec.add_dependency 'simplecov_json_formatter'
|
35
37
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: undercover
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jan Grodowski
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-03
|
10
|
+
date: 2025-07-03 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: base64
|
@@ -91,6 +91,34 @@ dependencies:
|
|
91
91
|
- - "<"
|
92
92
|
- !ruby/object:Gem::Version
|
93
93
|
version: '1.10'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: simplecov
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
type: :runtime
|
102
|
+
prerelease: false
|
103
|
+
version_requirements: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
- !ruby/object:Gem::Dependency
|
109
|
+
name: simplecov_json_formatter
|
110
|
+
requirement: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '0'
|
115
|
+
type: :runtime
|
116
|
+
prerelease: false
|
117
|
+
version_requirements: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
94
122
|
email:
|
95
123
|
- jgrodowski@gmail.com
|
96
124
|
executables:
|
@@ -112,20 +140,23 @@ files:
|
|
112
140
|
- README.md
|
113
141
|
- Rakefile
|
114
142
|
- bin/undercover
|
143
|
+
- docs/actions.yml
|
115
144
|
- docs/circleci_advanced.yml
|
116
145
|
- docs/circleci_config.yml
|
117
|
-
- docs/codeship.md
|
118
146
|
- docs/screenshot_success.png
|
119
147
|
- docs/screenshot_warnings.png
|
120
148
|
- docs/semaphore.yml
|
121
|
-
- docs/travis.yml
|
122
149
|
- lib/undercover.rb
|
123
150
|
- lib/undercover/changeset.rb
|
124
151
|
- lib/undercover/cli.rb
|
152
|
+
- lib/undercover/filter_set.rb
|
125
153
|
- lib/undercover/formatter.rb
|
126
154
|
- lib/undercover/lcov_parser.rb
|
127
155
|
- lib/undercover/options.rb
|
128
156
|
- lib/undercover/result.rb
|
157
|
+
- lib/undercover/root_to_relative_paths.rb
|
158
|
+
- lib/undercover/simplecov_formatter.rb
|
159
|
+
- lib/undercover/simplecov_result_adapter.rb
|
129
160
|
- lib/undercover/version.rb
|
130
161
|
- undercover.gemspec
|
131
162
|
homepage: https://github.com/grodowski/undercover
|
data/docs/codeship.md
DELETED
@@ -1,19 +0,0 @@
|
|
1
|
-
Sample configuration to run `undercover` in Codeship CI. Edit these fields in **Project Settings**.
|
2
|
-
|
3
|
-
**Setup commands**
|
4
|
-
|
5
|
-
```
|
6
|
-
rvm use 2.5.3 --install
|
7
|
-
bundle install
|
8
|
-
gem install undercover
|
9
|
-
```
|
10
|
-
|
11
|
-
**Test pipeline**
|
12
|
-
|
13
|
-
```
|
14
|
-
bundle exec rspec --format documentation --color
|
15
|
-
# fetch origin/master to have a ref to compare against
|
16
|
-
git remote set-branches --add origin master
|
17
|
-
git fetch
|
18
|
-
undercover -c origin/master
|
19
|
-
```
|
data/docs/travis.yml
DELETED