undercover 0.6.3 → 0.6.5

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: 6858f42ada692894674f818d7713d20e8239051d06547f72e721f6122dda53be
4
- data.tar.gz: ef323b826f66d68a5ad0a5a7c14f5d37d3a85344f0efe82ac457181745000fb9
3
+ metadata.gz: 514293d0be75c8d741a434a7d01b338f4b7ea2cf1b8b91ada91ec698531252f4
4
+ data.tar.gz: 3c2b19dd909af9a37f5f5c1e3a1aba8f0cea06f91eb37a10df2d6646a2e6144b
5
5
  SHA512:
6
- metadata.gz: 365149055a1553d4bc6dfde0475cd4b1bc6a3a82c7a0cb5c5fbcf7604c078e320bafcd85b783d495bc4667bff58c0ebffeb5ec76a982c61bcb366e8a9a8edc18
7
- data.tar.gz: d5d7ec83fd50f1504f38bdd9961be84d365d5f3882045a1028f3daf45cd309a5ded497941e723cf3f3ce0eb335dfba34310f69b3d5efb91b0013fc5f69cc77a8
6
+ metadata.gz: a877a890d2992f62496dc64d5a93a04488fd8f7820a3ca4f673d71446bd53dc756117a50c4ae6504f891aebf4547472569bf8c9d7cd5b489eccc22d8affa0711
7
+ data.tar.gz: 2c1d6b90e6f4bad580581d85a928ff607728950c562a02aad160c2534ced6b68ed8c43e5f97b48a7dcd1618988b81c179676f36b4ceccbe8a83107bc42a85922
@@ -5,7 +5,7 @@ jobs:
5
5
  runs-on: ubuntu-latest
6
6
  strategy:
7
7
  matrix:
8
- ruby: ['3.3', '3.0']
8
+ ruby: ['3.4', '3.0']
9
9
  steps:
10
10
  - uses: actions/checkout@v4
11
11
  with:
@@ -33,7 +33,7 @@ jobs:
33
33
  steps:
34
34
  - uses: actions/download-artifact@v4
35
35
  with:
36
- name: undercover-3.3.lcov
36
+ name: undercover-3.4.lcov
37
37
  - name: Upload coverage
38
38
  run: |
39
39
  ruby -e "$(curl -s https://undercover-ci.com/uploader.rb)" -- \
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 3.3.3
1
+ ruby 3.4.2
data/CHANGELOG.md CHANGED
@@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ # [0.6.5] - 2025-07-01
10
+
11
+ ### Fixed
12
+ - Improved performance for large PRs with lazy diff enumeration ([#229](https://github.com/grodowski/undercover/pull/229))
13
+
14
+ # [0.6.4] - 2025-03-29
15
+
16
+ ### Fixed
17
+ - Fix more false positives due to source file / coverage locator issues ([#222](https://github.com/grodowski/undercover/issues/222))
18
+
9
19
  # [0.6.3] - 2024-12-23
10
20
 
11
21
  ### Fixed
@@ -143,9 +153,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
143
153
  ### Added
144
154
  - First release of `undercover` 🎉
145
155
 
146
- [Unreleased]: https://github.com/grodowski/undercover/compare/v0.6.3...HEAD
147
- [0.6.3]:https://github.com/grodowski/undercover/compare/v0.6.3...v0.6.0
148
- [0.6.0]: https://github.com/grodowski/undercover/compare/v0.6.0...v0.5.0
156
+ [Unreleased]: https://github.com/grodowski/undercover/compare/v0.6.5...HEAD
157
+ [0.6.5]: https://github.com/grodowski/undercover/compare/v0.6.4...0.6.5
158
+ [0.6.4]: https://github.com/grodowski/undercover/compare/v0.6.3...v0.6.4
159
+ [0.6.3]: https://github.com/grodowski/undercover/compare/v0.6.0...v0.6.3
160
+ [0.6.0]: https://github.com/grodowski/undercover/compare/v0.5.0...v0.6.0
149
161
  [0.5.0]: https://github.com/grodowski/undercover/compare/v0.4.7...v0.5.0
150
162
  [0.4.7]: https://github.com/grodowski/undercover/compare/v0.4.6...v0.4.7
151
163
  [0.4.6]: https://github.com/grodowski/undercover/compare/v0.4.5...v0.4.6
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:
@@ -110,6 +109,7 @@ Usage: undercover [options]
110
109
  -g, --git-dir dir Override `.git` with a custom directory
111
110
  -c, --compare ref Generate coverage warnings for all changes after `ref`
112
111
  -r, --ruby-syntax ver Ruby syntax version, one of: current, ruby18, ruby19, ruby20, ruby21, ruby22, ruby23, ruby24, ruby25, ruby26, ruby30, ruby31, ruby32, ruby33
112
+ -w, --max-warnings limit Maximum number of warnings to generate before stopping analysis. Useful as a performance improvement for large diffs.
113
113
  -f, --include-files globs Include files matching specified glob patterns (comma separated). Defaults to '*.rb,*.rake,*.ru,Rakefile'
114
114
  -x, --exclude-files globs Skip files matching specified glob patterns (comma separated). Empty by default.
115
115
  -h, --help Prints this help
@@ -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,25 @@ 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
46
  # TODO: refactor to a standalone validator (depending on changeset AND lcov)
59
47
  def validate(lcov_report_path)
60
- return :no_changes if files.empty?
48
+ return :no_changes if full_diff.deltas.empty?
61
49
 
62
50
  :stale_coverage if last_modified > File.mtime(lcov_report_path)
63
51
  end
@@ -68,7 +56,7 @@ module Undercover
68
56
  # as it makes sense to run Undercover with the most recent file versions
69
57
  def full_diff
70
58
  base = compare_base_obj || head
71
- base.diff(repo.index).merge!(repo.diff_workdir(head))
59
+ @full_diff ||= base.diff(repo.index).merge!(repo.diff_workdir(head))
72
60
  end
73
61
 
74
62
  def compare_base_obj
@@ -83,6 +71,6 @@ module Undercover
83
71
  repo.head.target
84
72
  end
85
73
 
86
- attr_reader :repo, :compare_base
74
+ attr_reader :repo, :compare_base, :filter_set
87
75
  end
88
76
  end
@@ -20,7 +20,6 @@ module Undercover
20
20
 
21
21
  run_report(opts)
22
22
  end
23
- # rubocop:enable
24
23
 
25
24
  def self.run_report(opts)
26
25
  report = Undercover::Report.new(changeset(opts), opts).build
@@ -44,7 +43,8 @@ module Undercover
44
43
 
45
44
  def self.changeset(opts)
46
45
  git_dir = File.join(opts.path, opts.git_dir)
47
- 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)
48
48
  end
49
49
  end
50
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
@@ -5,16 +5,29 @@ require 'pathname'
5
5
 
6
6
  module Undercover
7
7
  class Options # rubocop:disable Metrics/ClassLength
8
- RUN_MODE = [
9
- RUN_MODE_DIFF_STRICT = :diff_strict, # warn for changed lines
10
- # RUN_MODE_DIFF_FILES = :diff_files, # warn for changed whole files
11
- # RUN_MODE_ALL = :diff_all, # warn for allthethings
12
- # RUN_MODE_FILES = :files # warn for specific files (cli option)
8
+ DIFF_TRIGGER_MODE = [
9
+ # Default, analyse all code blocks with changed lines. Untested lines and branches will trigger warnings if they
10
+ # strictly belong to the diff.
11
+ DIFF_TRIGGER_LINE = :diff_trigger_line,
12
+ #
13
+ # Doesn't exist yet
14
+ # Analyse all code blocks with changed lines. Untested lines and branches always trigger warnings for code block.
15
+ # DIFF_TRIGGER_BLOCK = :diff_trigger_block,
16
+ # Analyse all code blocks in each changed file.
17
+ # DIFF_TRIGGER_FILE = :diff_trigger_file,
18
+ # Analyse all code blocks in all files, ignores current diff (use --include-files and --exclude-files for control)
19
+ # ALL = :all,
13
20
  ].freeze
14
21
 
15
- OUTPUT_FORMATTERS = [
16
- OUTPUT_STDOUT = :stdout, # outputs warnings to stdout with exit 1
17
- # OUTPUT_CIRCLEMATOR = :circlemator # posts warnings as review comments
22
+ FILE_SCOPE = [
23
+ # Extended scope helps identify Ruby files that are not required in the test suite.
24
+ # Warning: currently doesn't respect :nocov: syntax in files not traced by SimpleCov.
25
+ # (use --include-files and --exclude-files for control)
26
+ FILE_SCOPE_EXTENDED = :scope_extended,
27
+ #
28
+ # Doesn't exist yet
29
+ # Analyse file that appear in coverage reports. Historically, the default undercover mode.
30
+ # FILE_SCOPE_COVERAGE = :scope_coverage,
18
31
  ].freeze
19
32
 
20
33
  DEFAULT_FILE_INCLUDE_GLOBS = %w[*.rb *.rake *.ru Rakefile].freeze
@@ -25,19 +38,21 @@ module Undercover
25
38
  :git_dir,
26
39
  :compare,
27
40
  :syntax_version,
41
+ :run_mode,
42
+ :file_scope,
28
43
  :glob_allow_filters,
29
- :glob_reject_filters
44
+ :glob_reject_filters,
45
+ :max_warnings_limit
30
46
 
31
47
  def initialize
32
- # TODO: use run modes
33
- # TODO: use formatters
34
- @run_mode = RUN_MODE_DIFF_STRICT
35
- @enabled_formatters = [OUTPUT_STDOUT]
48
+ @run_mode = DIFF_TRIGGER_LINE
49
+ @file_scope = FILE_SCOPE_EXTENDED
36
50
  # set defaults
37
51
  self.path = '.'
38
52
  self.git_dir = '.git'
39
53
  self.glob_allow_filters = DEFAULT_FILE_INCLUDE_GLOBS
40
54
  self.glob_reject_filters = DEFAULT_FILE_EXCLUDE_GLOBS
55
+ self.max_warnings_limit = nil
41
56
  end
42
57
 
43
58
  # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
@@ -62,6 +77,7 @@ module Undercover
62
77
  git_dir_option(opts)
63
78
  compare_option(opts)
64
79
  ruby_syntax_option(opts)
80
+ max_warnings_limit_option(opts)
65
81
  file_filters(opts)
66
82
  end.parse(args)
67
83
 
@@ -124,6 +140,13 @@ module Undercover
124
140
  end
125
141
  end
126
142
 
143
+ def max_warnings_limit_option(parser)
144
+ desc = 'Maximum number of warnings to generate before stopping analysis'
145
+ parser.on('-w', '--max-warnings limit', Integer, desc) do |limit|
146
+ self.max_warnings_limit = limit
147
+ end
148
+ end
149
+
127
150
  def guess_lcov_path
128
151
  cwd = Pathname.new(File.expand_path(path))
129
152
  self.lcov = File.join(cwd, 'coverage', 'lcov', "#{cwd.split.last}.lcov")
@@ -10,13 +10,13 @@ module Undercover
10
10
 
11
11
  def_delegators :node, :first_line, :last_line, :name
12
12
 
13
- def initialize(node, file_cov, file_path) # rubocop:disable Metrics/MethodLength
13
+ def initialize(node, file_cov, file_path) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
14
14
  @node = node
15
15
  @coverage = file_cov.select do |ln, _|
16
16
  if first_line == last_line
17
17
  ln == first_line
18
- elsif node.empty_def?
19
- ln >= first_line
18
+ elsif node.empty_def? || node.is_a?(Imagen::Node::Block)
19
+ ln >= first_line && ln <= last_line # rubocop:disable Style/ComparableBetween
20
20
  else
21
21
  ln > first_line && ln < last_line
22
22
  end
@@ -98,11 +98,11 @@ module Undercover
98
98
  Rainbow(' hits: n/a').italic.darkgray.dark
99
99
  elsif covered.positive?
100
100
  Rainbow(formatted_line).green +
101
- Rainbow(" hits: #{covered}").italic.darkgray.dark + \
101
+ Rainbow(" hits: #{covered}").italic.darkgray.dark +
102
102
  count_covered_branches(num)
103
103
  elsif covered.zero?
104
104
  Rainbow(formatted_line).red +
105
- Rainbow(" hits: #{covered}").italic.darkgray.dark + \
105
+ Rainbow(" hits: #{covered}").italic.darkgray.dark +
106
106
  count_covered_branches(num)
107
107
  end
108
108
  end.join("\n")
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Undercover
4
- VERSION = '0.6.3'
4
+ VERSION = '0.6.5'
5
5
  end
data/lib/undercover.rb CHANGED
@@ -12,6 +12,7 @@ require 'undercover/cli'
12
12
  require 'undercover/changeset'
13
13
  require 'undercover/formatter'
14
14
  require 'undercover/options'
15
+ require 'undercover/filter_set'
15
16
  require 'undercover/version'
16
17
 
17
18
  module Undercover
@@ -23,7 +24,8 @@ module Undercover
23
24
  :lcov,
24
25
  :results,
25
26
  :code_dir,
26
- :glob_filters
27
+ :filter_set,
28
+ :max_warnings_limit
27
29
 
28
30
  # Initializes a new Undercover::Report
29
31
  #
@@ -32,18 +34,19 @@ module Undercover
32
34
  def initialize(changeset, opts)
33
35
  @lcov = LcovParser.parse(File.open(opts.lcov))
34
36
  @code_dir = opts.path
35
- @changeset = changeset.update
36
- @glob_filters = {
37
- allow: opts.glob_allow_filters,
38
- reject: opts.glob_reject_filters
39
- }
37
+ @changeset = changeset
38
+ @filter_set = FilterSet.new(opts.glob_allow_filters, opts.glob_reject_filters)
39
+ @max_warnings_limit = opts.max_warnings_limit
40
40
  @loaded_files = {}
41
41
  @results = {}
42
42
  end
43
43
 
44
44
  # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
45
45
  def build
46
+ flag_count = 0
46
47
  changeset.each_changed_line do |filepath, line_no|
48
+ break if max_warnings_limit && flag_count >= max_warnings_limit
49
+
47
50
  dist_from_line_no = lambda do |res|
48
51
  return BigDecimal::INFINITY if line_no < res.first_line
49
52
 
@@ -61,7 +64,10 @@ module Undercover
61
64
  next unless loaded_files[filepath]
62
65
 
63
66
  res = loaded_files[filepath].min(&dist_from_line_no_sorter)
64
- res.flag if res&.uncovered?(line_no)
67
+ if res.uncovered?(line_no)
68
+ res.flag
69
+ flag_count += 1
70
+ end
65
71
  results[filepath] ||= Set.new
66
72
  results[filepath] << res
67
73
  end
@@ -107,16 +113,14 @@ module Undercover
107
113
  return if root_ast.children.empty?
108
114
 
109
115
  loaded_files[key] = []
110
- # TODO: children[0] ignores the lonely_method (see spec fixtures)!
111
- root_ast.children[0].find_all(->(_) { true }).each do |imagen_node|
116
+ root_ast.find_all(->(node) { !node.is_a?(Imagen::Node::Root) }).each do |imagen_node|
112
117
  loaded_files[key] << Result.new(imagen_node, coverage, filepath)
113
118
  end
114
119
  end
115
120
  # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
116
121
 
117
122
  def include_file?(filepath)
118
- fnmatch = proc { |glob| File.fnmatch(glob, filepath) }
119
- glob_filters[:allow].any?(fnmatch) && glob_filters[:reject].none?(fnmatch)
123
+ filter_set.include?(filepath)
120
124
  end
121
125
  end
122
126
  end
data/undercover.gemspec CHANGED
@@ -27,8 +27,9 @@ Gem::Specification.new do |spec|
27
27
 
28
28
  spec.required_ruby_version = '>= 3.0.0'
29
29
 
30
+ spec.add_dependency 'base64'
30
31
  spec.add_dependency 'bigdecimal'
31
32
  spec.add_dependency 'imagen', '>= 0.2.0'
32
33
  spec.add_dependency 'rainbow', '>= 2.1', '< 4.0'
33
- spec.add_dependency 'rugged', '>= 0.27', '< 1.8'
34
+ spec.add_dependency 'rugged', '>= 0.27', '< 1.10'
34
35
  end
metadata CHANGED
@@ -1,15 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: undercover
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.3
4
+ version: 0.6.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jan Grodowski
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-12-23 00:00:00.000000000 Z
10
+ date: 2025-07-01 00:00:00.000000000 Z
12
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: base64
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
13
26
  - !ruby/object:Gem::Dependency
14
27
  name: bigdecimal
15
28
  requirement: !ruby/object:Gem::Requirement
@@ -67,7 +80,7 @@ dependencies:
67
80
  version: '0.27'
68
81
  - - "<"
69
82
  - !ruby/object:Gem::Version
70
- version: '1.8'
83
+ version: '1.10'
71
84
  type: :runtime
72
85
  prerelease: false
73
86
  version_requirements: !ruby/object:Gem::Requirement
@@ -77,8 +90,7 @@ dependencies:
77
90
  version: '0.27'
78
91
  - - "<"
79
92
  - !ruby/object:Gem::Version
80
- version: '1.8'
81
- description:
93
+ version: '1.10'
82
94
  email:
83
95
  - jgrodowski@gmail.com
84
96
  executables:
@@ -110,6 +122,7 @@ files:
110
122
  - lib/undercover.rb
111
123
  - lib/undercover/changeset.rb
112
124
  - lib/undercover/cli.rb
125
+ - lib/undercover/filter_set.rb
113
126
  - lib/undercover/formatter.rb
114
127
  - lib/undercover/lcov_parser.rb
115
128
  - lib/undercover/options.rb
@@ -121,7 +134,6 @@ licenses:
121
134
  - MIT
122
135
  metadata:
123
136
  rubygems_mfa_required: 'true'
124
- post_install_message:
125
137
  rdoc_options: []
126
138
  require_paths:
127
139
  - lib
@@ -136,8 +148,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
136
148
  - !ruby/object:Gem::Version
137
149
  version: '0'
138
150
  requirements: []
139
- rubygems_version: 3.5.11
140
- signing_key:
151
+ rubygems_version: 3.6.2
141
152
  specification_version: 4
142
153
  summary: Actionable code coverage - detects untested code blocks in recent changes
143
154
  test_files: []