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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2a3601cf9aa4d26cee9c04f12b44273f8a67e0e8584b020e4e9bb763e03bf24e
4
- data.tar.gz: 8198c30de2bc16734a1575b7aa0213c25ccafe120848dad9c24257d688b7bca9
3
+ metadata.gz: cb507959b33f47004438d9cba6c7cf02dfa8d439e20e34caf341e2f2375fcc04
4
+ data.tar.gz: 3f801011026a04205318a607b303199aa940c01c4421d06ff2ec99a9edba5886
5
5
  SHA512:
6
- metadata.gz: 6e6a60fad9a478c37895ae24035022a9acf86f789ce0d798194b6d13b82c7db52c9de6590a9a9d3b8a84d304f220a30ef2c8e5226bab6fa58cea96f09c3856ca
7
- data.tar.gz: fd1ca272d59b29debe708bd2f8df2bf867dccc60484416002dbf004d21ea83370e408e7703098f8d6e2ff0345d2234a7e662f94254a7dcc339f7370bae9f9885
6
+ metadata.gz: 0540c422fa1729247cd1136402c1246c7cec3e4276077be48368397d26947c2ca41c8247aa6488f5e1aa8d0690f239c0ef9360da8c9ba912bb3d00b92bfbafde
7
+ data.tar.gz: ced783b94a0f156d89bb8fba42c737282e879320a62cca94e00021776683873eca03d0acf216c6bdb5c56b2b9c7f52ba3e313eed378c230dace2746e0ec5ee8e
@@ -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 }}.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 20.12.2
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.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
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
@@ -12,5 +12,6 @@ gem 'rspec', '~> 3.0'
12
12
  gem 'rubocop'
13
13
  gem 'simplecov'
14
14
  gem 'simplecov-html'
15
- gem 'simplecov-lcov', '~> 0.8'
15
+ gem 'simplecov_json_formatter'
16
+ gem 'simplecov-lcov'
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,58 @@ 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, 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
- 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 '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
- Then run your test suite once through to generate the initial `coverage/lcov/*.lcov` file before you can run the `undercover` command
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
- - [Travis CI](docs/travis.yml)
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
- -l, --lcov path LCOV report file path
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
@@ -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
@@ -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::Changeset.new(git_dir, opts.compare)
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
@@ -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")
@@ -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,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
@@ -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.1'
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.1
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-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
@@ -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