undercover 0.6.4 → 0.7.4

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: 2a3601cf9aa4d26cee9c04f12b44273f8a67e0e8584b020e4e9bb763e03bf24e
4
- data.tar.gz: 8198c30de2bc16734a1575b7aa0213c25ccafe120848dad9c24257d688b7bca9
3
+ metadata.gz: fb439eb426932d432434081cf9e17cce9b19b90a85c71b6a4c173d20cceb9392
4
+ data.tar.gz: a2703dd7d19a18f943dd3fdaebc34faf23cc995b1cc94b673756f1b5142cf046
5
5
  SHA512:
6
- metadata.gz: 6e6a60fad9a478c37895ae24035022a9acf86f789ce0d798194b6d13b82c7db52c9de6590a9a9d3b8a84d304f220a30ef2c8e5226bab6fa58cea96f09c3856ca
7
- data.tar.gz: fd1ca272d59b29debe708bd2f8df2bf867dccc60484416002dbf004d21ea83370e408e7703098f8d6e2ff0345d2234a7e662f94254a7dcc339f7370bae9f9885
6
+ metadata.gz: 034ff6bd2826c4826b06780bd874e28c04c8fbcb0fcde85a5c0a1dd8901e4f5d2ebfe6c149b828c0fcf28fd4064f14ef122f69c763f59170b62d99941281d44b
7
+ data.tar.gz: 114c9a9873fb75c17a3bfe79612bfe8d2891cd2d2b2dd68f8253d49f3529def86bca607d4774d1f113ade2fd8eb2d6d5d55e53d4b47b4a5464c3a6075eb2da3b
@@ -18,25 +18,25 @@ jobs:
18
18
  run: |
19
19
  gem install bundler undercover --no-doc
20
20
  bundle install --jobs 4 --retry 3
21
- bundle exec rake
21
+ rake
22
22
  - name: undercover (local)
23
23
  run: |
24
24
  git fetch --update-head-ok origin master:master
25
- undercover --compare master
25
+ undercover --simplecov coverage/undercover_coverage.json --compare master
26
26
  - uses: actions/upload-artifact@v4
27
27
  with:
28
- name: undercover-${{ matrix.ruby }}.lcov
29
- path: coverage/lcov/undercover.lcov
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.lcov
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
- --lcov /home/runner/work/undercover/undercover/undercover.lcov
42
+ --simplecov /home/runner/work/undercover/undercover/undercover_coverage.json
data/.tool-versions CHANGED
@@ -1 +1,2 @@
1
1
  ruby 3.4.2
2
+ nodejs v24.4.0
data/CHANGELOG.md CHANGED
@@ -6,6 +6,43 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ # [0.7.4] - 2025-07-13
10
+
11
+ ### Fixed
12
+ - Fix `fnmatch` for `FilterSet` to support glob braces (sets) in `--include-files` and `--exclude-files`
13
+
14
+ # [0.7.3] - 2025-07-13
15
+
16
+ ### Fixed
17
+ - Improve Options#parse with glob braces support, strip quotes properly from .undercover config files
18
+ - Fix Result#coverage_f to support ignored branches
19
+ - Fix error parsing JSON coverage with branch coverage disabled ([#231](https://github.com/grodowski/undercover/issues/231))
20
+ - Fixed NoMethodError and Errno::ENOENT that were occurring when coverage report doesn't exist ([#232](https://github.com/grodowski/undercover/issues/232))
21
+
22
+ # [0.7.2] - 2025-07-07
23
+
24
+ ### Fixed
25
+ - Resolved errors when .lcov files doesn't exist using `--lcov` CLI flag and `guess_lcov_path`
26
+
27
+ # [0.7.0] - 2025-07-03
28
+
29
+ ### Added
30
+ - New native SimpleCov formatter that generates coverage data directly without requiring LCOV conversion ([#223](https://github.com/grodowski/undercover/pull/223))
31
+ - `--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))
32
+ - Improved path handling for projects running in nested subdirectories or monorepo setups ([#223](https://github.com/grodowski/undercover/pull/223))
33
+
34
+ ### Fixed
35
+ - `: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))
36
+
37
+ # [0.6.6] - 2025-07-01
38
+
39
+ - Bugfix in `max_warnings_limit` following ([#229](https://github.com/grodowski/undercover/pull/229))
40
+
41
+ # [0.6.5] - 2025-07-01
42
+
43
+ ### Fixed
44
+ - Improved performance for large PRs with lazy diff enumeration ([#229](https://github.com/grodowski/undercover/pull/229))
45
+
9
46
  # [0.6.4] - 2025-03-29
10
47
 
11
48
  ### Fixed
@@ -148,9 +185,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
148
185
  ### Added
149
186
  - First release of `undercover` 🎉
150
187
 
151
- [Unreleased]: https://github.com/grodowski/undercover/compare/v0.6.3...HEAD
152
- [0.6.3]:https://github.com/grodowski/undercover/compare/v0.6.3...v0.6.0
153
- [0.6.0]: https://github.com/grodowski/undercover/compare/v0.6.0...v0.5.0
188
+ [Unreleased]: https://github.com/grodowski/undercover/compare/v0.7.4...HEAD
189
+ [0.7.4]: https://github.com/grodowski/undercover/compare/v0.7.3...v0.7.4
190
+ [0.7.3]: https://github.com/grodowski/undercover/compare/v0.7.2...v0.7.3
191
+ [0.7.2]: https://github.com/grodowski/undercover/compare/v0.7.1...v0.7.2
192
+ [0.7.1]: https://github.com/grodowski/undercover/compare/v0.7.0...v0.7.1
193
+ [0.7.0]: https://github.com/grodowski/undercover/compare/v0.6.6...v0.7.0
194
+ [0.6.6]: https://github.com/grodowski/undercover/compare/v0.6.5...0.6.6
195
+ [0.6.5]: https://github.com/grodowski/undercover/compare/v0.6.4...0.6.5
196
+ [0.6.4]: https://github.com/grodowski/undercover/compare/v0.6.3...v0.6.4
197
+ [0.6.3]: https://github.com/grodowski/undercover/compare/v0.6.0...v0.6.3
198
+ [0.6.0]: https://github.com/grodowski/undercover/compare/v0.5.0...v0.6.0
154
199
  [0.5.0]: https://github.com/grodowski/undercover/compare/v0.4.7...v0.5.0
155
200
  [0.4.7]: https://github.com/grodowski/undercover/compare/v0.4.6...v0.4.7
156
201
  [0.4.6]: https://github.com/grodowski/undercover/compare/v0.4.5...v0.4.6
data/Gemfile CHANGED
@@ -10,7 +10,8 @@ gem 'pry'
10
10
  gem 'rake', '~> 13.0'
11
11
  gem 'rspec', '~> 3.0'
12
12
  gem 'rubocop'
13
+ gem 'ruby-lsp-rspec', require: false
13
14
  gem 'simplecov'
14
15
  gem 'simplecov-html'
15
- gem 'simplecov-lcov', '~> 0.8'
16
+ gem 'simplecov_json_formatter'
16
17
  gem 'timecop'
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
  [![Build Status](https://github.com/grodowski/undercover/actions/workflows/ruby.yml/badge.svg)](https://github.com/grodowski/undercover/actions)
12
- [![Maintainability](https://api.codeclimate.com/v1/badges/b403feed68a18c072ec5/maintainability)](https://codeclimate.com/github/grodowski/undercover/maintainability)
13
12
  ![Downloads](https://img.shields.io/gem/dt/undercover)
14
13
 
15
14
  A sample output of `undercover` ran before a commit may look like this:
@@ -36,34 +35,56 @@ Or install it yourself as:
36
35
 
37
36
  $ gem install undercover
38
37
 
39
- ## Setting up required LCOV reporting
38
+ ## Setting up coverage reporting
40
39
 
41
- To make your specs or tests compatible with `undercover` by providing an LCOV report, please add `simplecov` and `simplecov-lcov` to your test setup.
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 'simplecov'
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 'simplecov-lcov'
53
- SimpleCov::Formatter::LcovFormatter.config.report_with_single_file = true
54
- SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter
50
+ require 'undercover/simplecov_formatter'
51
+
52
+ # optional, the filename defaults to `coverage.json` and is automatically recognised by the gem
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
- require 'undercover'
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 'undercover'
77
+ end
78
+
79
+ # spec_helper.rb
80
+ require 'simplecov'
81
+ require 'undercover/simplecov_formatter'
82
+ SimpleCov.formatter = SimpleCov::Formatter::Undercover
64
83
  ```
65
84
 
66
- Then run your test suite once through to generate the initial `coverage/lcov/*.lcov` file before you can run the `undercover` command
85
+ 3. Update CLI usage: Use `--simplecov` flag instead of `--lcov`, or rely on auto-detection of `coverage/coverage.json`
86
+
87
+ Note: LCOV support will be deprecated in a future release, but remains fully functional for existing projects.
67
88
 
68
89
  ## Usage
69
90
 
@@ -80,11 +101,10 @@ undercover --compare origin/master
80
101
  ```
81
102
 
82
103
  Check out `docs/` for CI configuration examples:
83
- - [Travis CI](docs/travis.yml)
104
+ - [GitHub Actions](docs/actions.yml)
84
105
  - [CircleCI - simple](docs/circleci_config.yml)
85
106
  - [CircleCI - advanced](docs/circleci_advanced.yml)
86
107
  - [Semaphore](docs/semaphore.yml)
87
- - [Codeship](docs/codeship.md)
88
108
 
89
109
  Merging coverage results ([sample gist](https://gist.github.com/grodowski/9744ff91034dce8df20c2a8210409fb0)) is required for parallel tests before processing with `undercover`.
90
110
 
@@ -105,11 +125,13 @@ Options can be passed when running the command from the command line:
105
125
 
106
126
  ```sh
107
127
  Usage: undercover [options]
108
- -l, --lcov path LCOV report file path
128
+ -s, --simplecov path SimpleCov JSON report file
129
+ -l, --lcov path LCOV report file path (to be deprecated)
109
130
  -p, --path path Project directory
110
131
  -g, --git-dir dir Override `.git` with a custom directory
111
132
  -c, --compare ref Generate coverage warnings for all changes after `ref`
112
133
  -r, --ruby-syntax ver Ruby syntax version, one of: current, ruby18, ruby19, ruby20, ruby21, ruby22, ruby23, ruby24, ruby25, ruby26, ruby30, ruby31, ruby32, ruby33
134
+ -w, --max-warnings limit Maximum number of warnings to generate before stopping analysis. Useful as a performance improvement for large diffs.
113
135
  -f, --include-files globs Include files matching specified glob patterns (comma separated). Defaults to '*.rb,*.rake,*.ru,Rakefile'
114
136
  -x, --exclude-files globs Skip files matching specified glob patterns (comma separated). Empty by default.
115
137
  -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
@@ -20,7 +20,7 @@ jobs:
20
20
  bundle exec rspec
21
21
  - run:
22
22
  name: Store coverage report
23
- command: mv coverage/lcov/project.lcov /tmp/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 LCOV report
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 --lcov /tmp/coverage/project.lcov \
42
+ undercover --simplecov /tmp/coverage/coverage.json \
43
43
  --compare origin/master
44
44
 
45
45
  workflows:
@@ -1,5 +1,5 @@
1
1
  # Simple CircleCI config.yml example.
2
- # See workflow_config.yml for a more advanced example,
2
+ # See circleci_advanced.yml for a more advanced example,
3
3
  # that includes sharing data between containers.
4
4
 
5
5
  version: 2
@@ -8,31 +8,12 @@ module Undercover
8
8
  class Changeset
9
9
  T_ZERO = Time.strptime('0', '%s').freeze
10
10
 
11
- extend Forwardable
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
- @files = {}
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
- files.keys.sort
30
+ full_diff.deltas.map { |d| d.new_file[:path] }.sort
50
31
  end
51
32
 
52
33
  def each_changed_line
53
- files.each do |filepath, line_numbers|
54
- line_numbers.each { |ln| yield filepath, ln }
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 files.empty?
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
@@ -22,9 +22,29 @@ module Undercover
22
22
  end
23
23
 
24
24
  def self.run_report(opts)
25
+ coverage_path = opts.simplecov_resultset || opts.lcov
26
+ return handle_missing_coverage_path(opts) if coverage_path.nil?
27
+ return handle_missing_file(coverage_path) unless File.exist?(coverage_path)
28
+
25
29
  report = Undercover::Report.new(changeset(opts), opts).build
30
+ handle_report_validation(report, coverage_path)
31
+ end
32
+
33
+ def self.handle_missing_coverage_path(opts)
34
+ puts Rainbow('❌ ERROR: No coverage report found. Checked default paths:').red
35
+ puts Rainbow(' - ./coverage/coverage.json (SimpleCov)').red
36
+ puts Rainbow(" - ./coverage/lcov/#{Pathname.new(File.expand_path(opts.path)).split.last}.lcov (LCOV)").red
37
+ puts Rainbow('Set a custom path with --lcov or --simplecov option').red
38
+ 1
39
+ end
40
+
41
+ def self.handle_missing_file(coverage_path)
42
+ puts Rainbow("❌ ERROR: Coverage report not found at: #{coverage_path}").red
43
+ 1
44
+ end
26
45
 
27
- error = report.validate(opts.lcov)
46
+ def self.handle_report_validation(report, coverage_path)
47
+ error = report.validate(coverage_path)
28
48
  if error
29
49
  puts(WARNINGS_TO_S[error])
30
50
  return 0 if error == :no_changes
@@ -43,7 +63,8 @@ module Undercover
43
63
 
44
64
  def self.changeset(opts)
45
65
  git_dir = File.join(opts.path, opts.git_dir)
46
- Undercover::Changeset.new(git_dir, opts.compare)
66
+ filter_set = Undercover::FilterSet.new(opts.glob_allow_filters, opts.glob_reject_filters)
67
+ Undercover::Changeset.new(git_dir, opts.compare, filter_set)
47
68
  end
48
69
  end
49
70
  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, File::FNM_EXTGLOB) }
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
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'optparse'
4
4
  require 'pathname'
5
+ require 'shellwords'
5
6
 
6
7
  module Undercover
7
8
  class Options # rubocop:disable Metrics/ClassLength
@@ -34,6 +35,7 @@ module Undercover
34
35
  DEFAULT_FILE_EXCLUDE_GLOBS = %w[test/* spec/* db/* config/* *_test.rb *_spec.rb].freeze
35
36
 
36
37
  attr_accessor :lcov,
38
+ :simplecov_resultset,
37
39
  :path,
38
40
  :git_dir,
39
41
  :compare,
@@ -41,7 +43,8 @@ module Undercover
41
43
  :run_mode,
42
44
  :file_scope,
43
45
  :glob_allow_filters,
44
- :glob_reject_filters
46
+ :glob_reject_filters,
47
+ :max_warnings_limit
45
48
 
46
49
  def initialize
47
50
  @run_mode = DIFF_TRIGGER_LINE
@@ -51,6 +54,7 @@ module Undercover
51
54
  self.git_dir = '.git'
52
55
  self.glob_allow_filters = DEFAULT_FILE_INCLUDE_GLOBS
53
56
  self.glob_reject_filters = DEFAULT_FILE_EXCLUDE_GLOBS
57
+ self.max_warnings_limit = nil
54
58
  end
55
59
 
56
60
  # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
@@ -66,18 +70,23 @@ module Undercover
66
70
  end
67
71
 
68
72
  opts.on_tail('--version', 'Show version') do
73
+ # :nocov:
69
74
  puts VERSION
70
75
  exit
76
+ # :nocov:
71
77
  end
72
78
 
73
79
  lcov_path_option(opts)
80
+ resultset_path_option(opts)
74
81
  project_path_option(opts)
75
82
  git_dir_option(opts)
76
83
  compare_option(opts)
77
84
  ruby_syntax_option(opts)
85
+ max_warnings_limit_option(opts)
78
86
  file_filters(opts)
79
87
  end.parse(args)
80
88
 
89
+ guess_resultset_path unless simplecov_resultset
81
90
  guess_lcov_path unless lcov
82
91
  self
83
92
  end
@@ -96,19 +105,39 @@ module Undercover
96
105
  def args_from_options_file(path)
97
106
  return [] unless File.exist?(path)
98
107
 
99
- File.read(path).split('\n').flat_map(&:split)
108
+ File.read(path).split("\n").flat_map { parse_line(_1) }
100
109
  end
101
110
 
102
111
  def project_options_file
103
112
  './.undercover'
104
113
  end
105
114
 
115
+ def parse_line(line)
116
+ line = line.strip
117
+ return [] if line.empty? || line.start_with?('#')
118
+
119
+ Shellwords.split(line)
120
+ end
121
+
122
+ def split_comma_separated_with_braces(input)
123
+ return [] if input.empty?
124
+
125
+ input.split(/,(?![^{]*})/).map(&:strip) # split on commas that are not inside braces
126
+ end
127
+
106
128
  def lcov_path_option(parser)
107
129
  parser.on('-l', '--lcov path', 'LCOV report file path') do |path|
108
130
  self.lcov = path
109
131
  end
110
132
  end
111
133
 
134
+ def resultset_path_option(parser)
135
+ desc = 'SimpleCov::Formatter::Undercover output file path (alternative to LCOV that will become default)'
136
+ parser.on('-s', '--simplecov path', desc) do |path|
137
+ self.simplecov_resultset = path
138
+ end
139
+ end
140
+
112
141
  def project_path_option(parser)
113
142
  parser.on('-p', '--path path', 'Project directory') do |path|
114
143
  self.path = path
@@ -137,21 +166,35 @@ module Undercover
137
166
  end
138
167
  end
139
168
 
169
+ def max_warnings_limit_option(parser)
170
+ desc = 'Maximum number of warnings to generate before stopping analysis'
171
+ parser.on('-w', '--max-warnings limit', Integer, desc) do |limit|
172
+ self.max_warnings_limit = limit
173
+ end
174
+ end
175
+
176
+ def guess_resultset_path
177
+ cwd = Pathname.new(File.expand_path(path))
178
+ try_path = File.join(cwd, 'coverage', 'coverage.json')
179
+ self.simplecov_resultset = try_path if File.exist?(try_path)
180
+ end
181
+
140
182
  def guess_lcov_path
141
183
  cwd = Pathname.new(File.expand_path(path))
142
- self.lcov = File.join(cwd, 'coverage', 'lcov', "#{cwd.split.last}.lcov")
184
+ try_path = File.join(cwd, 'coverage', 'lcov', "#{cwd.split.last}.lcov")
185
+ self.lcov = try_path if File.exist?(try_path)
143
186
  end
144
187
 
145
188
  def file_filters(parser)
146
189
  desc = 'Include files matching specified glob patterns (comma separated). ' \
147
190
  "Defaults to '#{DEFAULT_FILE_INCLUDE_GLOBS.join(',')}'"
148
191
  parser.on('-f', '--include-files globs', desc) do |comma_separated_globs|
149
- self.glob_allow_filters = comma_separated_globs.strip.split(',')
192
+ self.glob_allow_filters = split_comma_separated_with_braces(comma_separated_globs)
150
193
  end
151
194
 
152
195
  desc = 'Skip files matching specified glob patterns (comma separated). Empty by default.'
153
196
  parser.on('-x', '--exclude-files globs', desc) do |comma_separated_globs|
154
- self.glob_reject_filters = comma_separated_globs.strip.split(',')
197
+ self.glob_reject_filters = split_comma_separated_with_braces(comma_separated_globs)
155
198
  end
156
199
  end
157
200
  end
@@ -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, file_cov, file_path) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
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,9 +60,14 @@ 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
- lines[ln] = 0 if branch_cov.zero?
70
+ lines[ln] = 0 if branch_cov != 'ignored' && branch_cov.zero?
61
71
  elsif block_or_line_cov.zero?
62
72
  lines[ln] = 0
63
73
  end
@@ -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,65 @@
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
+ return lines unless source_file['branches']
39
+
40
+ branch_idx = 0
41
+ branches = source_file['branches'].map do |branch|
42
+ branch_idx += 1
43
+ [branch['start_line'], 0, branch_idx, branch['coverage']]
44
+ end
45
+ lines + branches
46
+ end
47
+
48
+ def skipped?(filepath, line_no)
49
+ source_file = find_file(filepath)
50
+ return false unless source_file
51
+
52
+ source_file['lines'][line_no - 1] == 'ignored'
53
+ end
54
+
55
+ # unused for now
56
+ def total_coverage; end
57
+ def total_branch_coverage; end
58
+
59
+ private
60
+
61
+ def find_file(filepath)
62
+ simplecov_result['coverage'][fix_relative_filepath(filepath)]
63
+ end
64
+ end
65
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Undercover
4
- VERSION = '0.6.4'
4
+ VERSION = '0.7.4'
5
5
  end
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
- :glob_filters
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
- @lcov = LcovParser.parse(File.open(opts.lcov))
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.update
36
- @glob_filters = {
37
- allow: opts.glob_allow_filters,
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
- res.flag if res&.uncovered?(line_no)
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
- fnmatch = proc { |glob| File.fnmatch(glob, filepath) }
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.6.4
4
+ version: 0.7.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jan Grodowski
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-29 00:00:00.000000000 Z
10
+ date: 2025-07-13 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
@@ -1,13 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.6.2
4
- - 2.5.5
5
- - 2.4.5
6
- - 2.3.7
7
- before_install:
8
- - gem install bundler undercover --no-doc
9
- - gem update --system
10
- script:
11
- - bundle exec rake
12
- - git fetch origin master:master
13
- - undercover --compare master