uncov 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f5a03db793136112b1d041fd43b2d045e03c2edb8e2ceed196746e3edc5257b9
4
- data.tar.gz: 5185e319bf464056f3fa5c693132a65c58468959f3449aa71c6b3bebde4cb54b
3
+ metadata.gz: 328d5a3a593abb320621afee6841e28c19be46fd4ea1092ae52d6ef513131469
4
+ data.tar.gz: ba6658737c8230c2f90dd045a568c478dbee86d782d57c5c75e9f0d9303eb796
5
5
  SHA512:
6
- metadata.gz: a8cc75fcd5ca03b4a2a9579539ca8c321114faa0b9ef37f628aa3da0225a0ffddb2b6ec2e4cbc34c9215366bd6f9668439135b5f33e9b3dffd06c3cee87db293
7
- data.tar.gz: 517046c75414ed4c13add3297c54a5fd6ff2aad29f11b1f54e269253cb0a51b5f35cc9511d6c497d0e888d7ec5af1ef9f723203032b78f9125c33e6b43f6238e
6
+ metadata.gz: abba73e5527b9f48108ecd90757036af35f4dd4f9a8b2c2399e7e9c9c804a5d99195bcc0f6d9117583f5f07e9efa2e94db15ccd3b3f3829c09629dcc9bcac57e
7
+ data.tar.gz: fdb66c5ff19eb0c3bcacb136147a3d3493ed44400602c071137b91fdc1edacbef6ef6fe13663aa05ae24c9cd68ac773b95080466aed1e4026de6b52a3bcf0a2b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 2025-05-12: [v0.5.0](https://github.com/mpapis/uncov/releases/tag/v0.5.0)
4
+
5
+ ### Minor
6
+
7
+ - [#43](https://github.com/mpapis/uncov/pull/43): Trigger tests on test_file_changes. Add git_files report generator., by [@mpapis](https://github.com/mpapis)
8
+ - [#45](https://github.com/mpapis/uncov/pull/45): Add diff_files report type, by [@mpapis](https://github.com/mpapis)
9
+ - [#46](https://github.com/mpapis/uncov/pull/46): Add nocov report filter and flags, by [@mpapis](https://github.com/mpapis)
10
+
11
+ ### Patch
12
+
13
+ - [#44](https://github.com/mpapis/uncov/pull/44): Allow any git revision specification for target, by [@mpapis](https://github.com/mpapis)
14
+
15
+
16
+
17
+ ## 2025-05-10: [v0.4.2](https://github.com/mpapis/uncov/releases/tag/v0.4.2)
18
+
19
+ ### Patch
20
+
21
+ - [#42](https://github.com/mpapis/uncov/pull/42): Fix calculating relevant and covered lines, by [@mpapis](https://github.com/mpapis)
22
+
23
+
24
+
3
25
  ## 2025-05-05: [v0.4.1](https://github.com/mpapis/uncov/releases/tag/v0.4.1)
4
26
 
5
27
  ### Patch
data/README.md CHANGED
@@ -34,7 +34,35 @@ uncov
34
34
  ### Display configuration options
35
35
  ```bash
36
36
  $ uncov -h
37
-
37
+ Usage: uncov [options]
38
+ -t, --target TARGET Target branch for comparison, default: "HEAD"
39
+ -r, --report FILTER Report filter to generate file/line list, one_of: "diff_files", "diff_lines"(default), "git_files", "nocov_lines"
40
+ -o, --output-format FORMAT Output format, one_of: "terminal"(default)
41
+ -C, --context LINES_NUMBER Additional lines context in output, default: 1
42
+ --test-command COMMAND Test command that generates SimpleCov, default: "COVERAGE=true bundle exec rake test"
43
+ --simplecov-file PATH SimpleCov results file, default: "autodetect"
44
+ --relevant-files FN_GLOB Only show uncov for matching code files AND trigger tests if matching code files are newer than the report, default: "{{bin,exe,exec}/*,{app,lib}/**/*.{rake,rb},Rakefile}"
45
+ --relevant-tests FN_GLOB Trigger tests if matching test files are newer than the report, default: "{test,spec}/**/*_{test,spec}.rb"
46
+ --nocov-ignore Ignore :nocov: markers - consider all lines, default: false
47
+ --nocov-covered Report :nocov: lines that have coverage, default: false
48
+ --debug Get some insights, default: false
49
+ -h, --help Print this help
50
+
51
+ Report FILTERs:
52
+ diff_files - Report missing coverage on added/changed files in the git diff
53
+ diff_lines - Report missing coverage on added lines in the git diff
54
+ git_files - Report missing coverage on files tracked with git
55
+ nocov_lines - Report coverage on nocov lines, requires one or both: --nocov-ignore / --nocov-covered
56
+
57
+ Report FILTERs take NOTICE:
58
+ git*/diff* - filters will not consider new files unless added to the git index with `git add`.
59
+ nocov* - filters/flags only work with coverage/.resultset.json SimpleCov files,
60
+ coverage.json does not provide the information needed.
61
+
62
+ FN_GLOB: shell filename globing -> https://ruby-doc.org/core-3.1.1/File.html#method-c-fnmatch
63
+ in bash: `shopt -s extglob dotglob globstar` and test with `ls {app,lib}/**/*.rb`
64
+
65
+ uncov 0.5.0 by Michal Papis <mpapis@gmail.com>
38
66
  ```
39
67
 
40
68
 
data/lib/uncov/cli.rb CHANGED
@@ -6,9 +6,9 @@ require 'optparse'
6
6
  class Uncov::CLI
7
7
  def self.start(args)
8
8
  Uncov.configure(args)
9
- report = Uncov::Report.build
9
+ report = Uncov::Report.generate
10
10
  Uncov::Formatter.output(report)
11
- !report.uncov?
11
+ !report.trigger?
12
12
  rescue StandardError => e
13
13
  raise if Uncov.configuration.debug
14
14
 
@@ -2,16 +2,16 @@
2
2
 
3
3
  # configuration option
4
4
  class Uncov::Configuration::Option
5
- attr_reader :name, :description, :options, :default, :allowed_values, :value_parse, :value
5
+ attr_reader :name, :description, :options, :default, :value_parse, :value
6
6
 
7
7
  def initialize(name, description, options, default, allowed_values, value_parse)
8
8
  @name = name
9
9
  @description = description
10
10
  @options = Array(options)
11
11
  @default = default.freeze
12
+ @value = default
12
13
  @allowed_values = allowed_values
13
14
  @value_parse = value_parse
14
- self.value = default
15
15
  end
16
16
 
17
17
  def value=(value)
@@ -41,10 +41,16 @@ class Uncov::Configuration::Option
41
41
  if value == default
42
42
  "#{value.inspect}(default)"
43
43
  else
44
- # :nocov: for now as there is no case yet in Configuration
45
44
  value.inspect
46
- # :nocov:
47
45
  end
48
46
  end
49
47
  end
48
+
49
+ def allowed_values
50
+ if @allowed_values.respond_to?(:call)
51
+ @allowed_values = @allowed_values.call
52
+ else
53
+ @allowed_values
54
+ end
55
+ end
50
56
  end
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'formatter'
4
- require_relative 'report'
4
+ require_relative 'report/generator'
5
5
 
6
6
  # handle configuration for uncov
7
7
  class Uncov::Configuration
8
8
  CONFIG_FILE = '.uncov'
9
+ # equivalent of `shopt -s extglob dotglob globstar` for testing with `bash` & `ls`
9
10
  FILE_MATCH_FLAGS = File::FNM_EXTGLOB | File::FNM_PATHNAME | File::FNM_DOTMATCH
10
11
 
11
12
  class << self
@@ -19,16 +20,23 @@ class Uncov::Configuration
19
20
  end
20
21
 
21
22
  option 'target', 'Target branch for comparison', options: ['-t', '--target TARGET'], default: 'HEAD'
22
- option 'report', 'Report type to generate', options: ['-r', '--report TYPE'], default: 'diff_lines', allowed_values: Uncov::Report.types
23
+ option 'report', 'Report filter to generate file/line list',
24
+ options: ['-r', '--report FILTER'], default: 'diff_lines', allowed_values: -> { Uncov::Report::Generator.filters.keys }
23
25
  option 'output_format', 'Output format',
24
- options: ['-o', '--output-format FORMAT'], default: 'terminal', allowed_values: Uncov::Formatter.formats
26
+ options: ['-o', '--output-format FORMAT'], default: 'terminal', allowed_values: -> { Uncov::Formatter.formats }
25
27
  option 'context', 'Additional lines context in output',
26
28
  options: ['-C', '--context LINES_NUMBER'], default: 1, value_parse: lambda(&:to_i)
27
29
  option 'test_command', 'Test command that generates SimpleCov',
28
30
  options: '--test-command COMMAND', default: 'COVERAGE=true bundle exec rake test'
29
31
  option 'simplecov_file', 'SimpleCov results file', options: '--simplecov-file PATH', default: 'autodetect'
30
- option 'relevant_files', 'Relevant files shell filename globing: https://ruby-doc.org/core-3.1.1/File.html#method-c-fnmatch',
31
- options: '--relevant-files', default: '{{bin,exe,exec}/*,{app,lib}/**/*.{rake,rb},Rakefile}'
32
+ option 'relevant_files', 'Only show uncov for matching code files AND trigger tests if matching code files are newer than the report',
33
+ options: '--relevant-files FN_GLOB', default: '{{bin,exe,exec}/*,{app,lib}/**/*.{rake,rb},Rakefile}'
34
+ option 'relevant_tests', 'Trigger tests if matching test files are newer than the report',
35
+ options: '--relevant-tests FN_GLOB', default: '{test,spec}/**/*_{test,spec}.rb'
36
+ option 'nocov_ignore', 'Ignore :nocov: markers - consider all lines',
37
+ options: '--nocov-ignore', default: false, value_parse: ->(_value) { true }
38
+ option 'nocov_covered', 'Report :nocov: lines that have coverage',
39
+ options: '--nocov-covered', default: false, value_parse: ->(_value) { true }
32
40
  option 'debug', 'Get some insights', options: '--debug', default: false, value_parse: ->(_value) { true }
33
41
 
34
42
  def initialize
@@ -66,16 +74,40 @@ class Uncov::Configuration
66
74
  def parser_header(parser) = parser.banner = 'Usage: uncov [options]'
67
75
 
68
76
  def parser_footer(parser)
69
- parser.on('-v', '--version', 'Show version') do
70
- puts "uncov #{Uncov::VERSION} by Michal Papis <mpapis@gmail.com>"
71
- throw :exit, 0
72
- end
73
77
  parser.on('-h', '--help', 'Print this help') do
74
- puts parser
75
- puts "uncov #{Uncov::VERSION} by Michal Papis <mpapis@gmail.com>"
78
+ puts parser.help
76
79
  throw :exit, 0
77
80
  end
81
+ footer_extras(parser)
78
82
  end
79
83
 
80
- def options = @options ||= {}
84
+ def footer_extras(parser)
85
+ # TODO: the release workflow does not like ' in help, please avoid it - or fix the workflow
86
+ parser.separator <<~HELP
87
+
88
+ Report FILTERs:
89
+ #{footer_extras_types}
90
+
91
+ Report FILTERs take NOTICE:
92
+ git*/diff* - filters will not consider new files unless added to the git index with `git add`.
93
+ nocov* - filters/flags only work with coverage/.resultset.json SimpleCov files,
94
+ coverage.json does not provide the information needed.
95
+
96
+ FN_GLOB: shell filename globing -> https://ruby-doc.org/core-3.1.1/File.html#method-c-fnmatch
97
+ in bash: `shopt -s extglob dotglob globstar` and test with `ls {app,lib}/**/*.rb`
98
+
99
+ uncov #{Uncov::VERSION} by Michal Papis <mpapis@gmail.com>
100
+ HELP
101
+ end
102
+
103
+ def footer_extras_types
104
+ report_type_length = Uncov::Report::Generator.filters.keys.map(&:length).max
105
+ Uncov::Report::Generator.filters.map do |name, config|
106
+ "#{name.ljust(report_type_length)} - #{config[:description]}"
107
+ end.join("\n")
108
+ end
109
+
110
+ def options
111
+ @options ||= {}
112
+ end
81
113
  end
@@ -4,7 +4,11 @@
4
4
  class Uncov::Finder::FileSystem
5
5
  include Uncov::Cache
6
6
 
7
- def files = all_files.to_h { |file_name| [file_name, lines_proc(file_name)] }
7
+ def files
8
+ all_files.to_h do |file_name|
9
+ [file_name, lines(file_name)]
10
+ end
11
+ end
8
12
 
9
13
  private
10
14
 
@@ -12,7 +16,11 @@ class Uncov::Finder::FileSystem
12
16
  Dir.glob(Uncov.configuration.relevant_files, Uncov::Configuration::FILE_MATCH_FLAGS).select { |f| File.file?(f) }
13
17
  end
14
18
 
15
- def lines_proc(file_name) = -> { cache(file_name) { read_lines(file_name) } }
19
+ def lines(file_name)
20
+ cache(file_name) do
21
+ read_lines(file_name)
22
+ end
23
+ end
16
24
 
17
25
  def read_lines(file_name)
18
26
  File.readlines(file_name).each_with_index.to_h { |line, line_index| [line_index + 1, line.rstrip] }
@@ -3,14 +3,30 @@
3
3
  require_relative 'git_base'
4
4
 
5
5
  # collect list of files stored in git
6
- module Uncov::Finder::Git
7
- class << self
8
- include Uncov::Finder::GitBase
6
+ class Uncov::Finder::Git
7
+ include Uncov::Finder::GitBase
9
8
 
10
- def files
11
- open_repo.ls_files.keys.filter_map do |file_name|
12
- [file_name, true] if relevant_file?(file_name)
9
+ def code_files
10
+ cache(:code_files) do
11
+ all_file_names.filter_map do |file_name|
12
+ [file_name, true] if relevant_code_file?(file_name)
13
13
  end.to_h
14
14
  end
15
15
  end
16
+
17
+ def test_files
18
+ cache(:test_files) do
19
+ all_file_names.filter_map do |file_name|
20
+ [file_name, true] if relevant_test_file?(file_name)
21
+ end.to_h
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def all_file_names
28
+ cache(:all_files) do
29
+ open_repo.ls_files.keys
30
+ end
31
+ end
16
32
  end
@@ -4,14 +4,22 @@ require 'git'
4
4
 
5
5
  # common parts for git finders
6
6
  module Uncov::Finder::GitBase
7
+ include Uncov::Cache
8
+
7
9
  protected
8
10
 
9
- def relevant_file?(path)
11
+ def relevant_code_file?(path)
10
12
  File.fnmatch?(Uncov.configuration.relevant_files, path, Uncov::Configuration::FILE_MATCH_FLAGS)
11
13
  end
12
14
 
15
+ def relevant_test_file?(path)
16
+ File.fnmatch?(Uncov.configuration.relevant_tests, path, Uncov::Configuration::FILE_MATCH_FLAGS)
17
+ end
18
+
13
19
  def open_repo
14
- ::Git.open('.')
20
+ cache(:repo) do
21
+ ::Git.open('.')
22
+ end
15
23
  rescue ArgumentError => e
16
24
  raise Uncov::NotGitRepoError, Uncov.configuration.path if e.message.end_with?(' is not in a git working tree')
17
25
 
@@ -4,41 +4,57 @@ require_relative 'git_base'
4
4
  require 'git_diff_parser'
5
5
 
6
6
  # collect list of changed files and their added lines (removed do not impact coverage)
7
- module Uncov::Finder::GitDiff
8
- class << self
9
- include Uncov::Finder::GitBase
7
+ class Uncov::Finder::GitDiff
8
+ include Uncov::Finder::GitBase
10
9
 
11
- def files
12
- git_diff.filter_map do |file_diff|
13
- [file_diff.path, changed_lines(file_diff)] if relevant_file?(file_diff.path) && File.exist?(file_diff.path)
10
+ def code_files
11
+ cache(:code_files) do
12
+ all_files_diff.filter_map do |file_diff|
13
+ [file_diff.path, changed_lines(file_diff)] if relevant_code_file?(file_diff.path) && File.exist?(file_diff.path)
14
14
  end.to_h
15
15
  end
16
+ end
16
17
 
17
- private
18
+ def test_files
19
+ cache(:test_files) do
20
+ all_files_diff.filter_map do |file_diff|
21
+ [file_diff.path, true] if relevant_test_file?(file_diff.path) && File.exist?(file_diff.path)
22
+ end.to_h
23
+ end
24
+ end
18
25
 
19
- def changed_lines(file_diff)
20
- GitDiffParser.parse(file_diff.patch).flat_map do |patch|
21
- patch.changed_lines.map do |changed_line|
22
- next unless changed_line.content[0] == '+'
26
+ private
23
27
 
24
- [changed_line.number, nil]
25
- end
26
- end.compact.to_h
28
+ def all_files_diff
29
+ cache(:all_files) do
30
+ git_diff
27
31
  end
32
+ end
28
33
 
29
- def git_diff
30
- repo = open_repo
31
- git_target =
32
- case target
33
- when 'HEAD'
34
- target
35
- else
36
- repo.branches[target] or raise Uncov::NotGitBranchError, target
37
- end
38
-
39
- repo.diff(git_target)
40
- end
34
+ def changed_lines(file_diff)
35
+ GitDiffParser.parse(file_diff.patch).flat_map do |patch|
36
+ patch.changed_lines.map do |changed_line|
37
+ next unless changed_line.content[0] == '+'
38
+
39
+ [changed_line.number, nil]
40
+ end
41
+ end.compact.to_h
42
+ end
43
+
44
+ def git_diff
45
+ repo = open_repo
46
+ # TODO: resolve the need for verifying the target with git gem
47
+ git_target = repo.lib.send(:command, 'rev-parse', '--verify', target)
48
+ repo.diff(git_target)
49
+ rescue Git::FailedError => e
50
+ raise Uncov::NotGitObjectError, target if e.result.status.exitstatus == 128 && e.result.stderr.include?('fatal: Needed a single revision')
51
+
52
+ # :nocov: when we find a failing example, we can test it
53
+ raise
54
+ # :nocov:
55
+ end
41
56
 
42
- def target = Uncov.configuration.target
57
+ def target
58
+ Uncov.configuration.target
43
59
  end
44
60
  end
@@ -5,16 +5,22 @@ class Uncov::Finder::NoCov
5
5
  include Uncov::Cache
6
6
 
7
7
  def files(all_files)
8
- all_files.to_h { |file_name, lines| [file_name, nocov_proc(file_name, lines)] }
8
+ all_files.to_h do |file_name, lines|
9
+ [file_name, nocov_lines(file_name, lines)]
10
+ end
9
11
  end
10
12
 
11
13
  private
12
14
 
13
- def nocov_proc(file_name, lines_proc) = -> { cache(file_name) { read_nocov(lines_proc) } }
15
+ def nocov_lines(file_name, lines)
16
+ cache(file_name) do
17
+ read_nocov(lines)
18
+ end
19
+ end
14
20
 
15
- def read_nocov(lines_proc)
21
+ def read_nocov(lines)
16
22
  nocov = false
17
- lines_proc.call.filter_map do |number, line|
23
+ lines.filter_map do |number, line|
18
24
  line_nocov = line.strip.start_with?('# :nocov:')
19
25
  nocov = !nocov if line_nocov
20
26
  [number, true] if nocov || line_nocov # still true on disabling line
@@ -8,18 +8,33 @@ module Uncov::Finder::SimpleCov
8
8
  def files(trigger_files)
9
9
  regenerate_report if requires_regeneration?(trigger_files)
10
10
  raise_on_missing_coverage_path!
11
- coverage.transform_values { |file_coverage| covered_lines(file_coverage) }
11
+ coverage.transform_values do |file_coverage|
12
+ covered_lines(file_coverage)
13
+ end
12
14
  end
13
15
 
14
16
  private
15
17
 
16
18
  def requires_regeneration?(trigger_files)
19
+ if Uncov.configuration.debug
20
+ warn("{coverage_path: #{coverage_path}(#{coverage_path && File.exist?(coverage_path) ? 'exist' : 'missing'})}")
21
+ warn("{trigger_files: #{trigger_files.inspect}}")
22
+ end
17
23
  return true unless coverage_path
18
24
  return true unless File.exist?(coverage_path)
19
25
  return false if trigger_files.empty?
20
26
 
27
+ changed_files?(trigger_files)
28
+ end
29
+
30
+ def changed_files?(trigger_files)
21
31
  coverage_path_mtime = File.mtime(coverage_path)
22
- trigger_files.any? { |file_name| File.exist?(file_name) && File.mtime(file_name) > coverage_path_mtime }
32
+ changed_trigger_files =
33
+ trigger_files.select do |file_name|
34
+ File.exist?(file_name) && File.mtime(file_name) > coverage_path_mtime
35
+ end
36
+ warn("{changed_trigger_files: #{changed_trigger_files.inspect}}") if Uncov.configuration.debug
37
+ changed_trigger_files.any?
23
38
  end
24
39
 
25
40
  def regenerate_report
data/lib/uncov/finder.rb CHANGED
@@ -8,19 +8,24 @@ class Uncov::Finder
8
8
  def git_file?(file_name) = git_files[file_name]
9
9
  def git_file_names = git_files.keys
10
10
  def git_diff_file_names = git_diff_files.keys
11
- def git_diff_file_line?(file_name, line_number) = git_diff_files[file_name].key?(line_number)
12
11
  def git_diff_file_lines(file_name) = git_diff_files[file_name]
13
- def file_system_file_line(file_name, line_number) = file_system_files[file_name]&.call&.dig(line_number)
14
- def file_system_file_lines(file_name) = file_system_files[file_name]&.call
15
- def no_cov_file_line?(file_name, line_number) = no_cov_files[file_name]&.call&.dig(line_number)
12
+ def git_diff_file_line?(file_name, line_number) = git_diff_files[file_name]&.key?(line_number)
13
+ def file_system_file_names = file_system_files.keys
14
+ def file_system_file_line(file_name, line_number) = file_system_files[file_name]&.dig(line_number)
15
+ def file_system_file_lines(file_name) = file_system_files[file_name]
16
+ def no_cov_file_names = no_cov_files.keys
17
+ def no_cov_file_lines(file_name) = no_cov_files[file_name]
18
+ def no_cov_file_line?(file_name, line_number) = no_cov_files[file_name]&.dig(line_number)
16
19
  def simple_cov_file_line?(file_name, line_number) = simple_cov_files.dig(file_name, line_number)
17
20
 
18
21
  def debug
19
22
  {
20
23
  git_files:,
24
+ git_test_files:,
21
25
  git_diff_files:,
22
- file_system_files: file_system_files.transform_values(&:call),
23
- no_cov_files: no_cov_files.transform_values(&:call),
26
+ git_diff_test_files:,
27
+ file_system_files:,
28
+ no_cov_files:,
24
29
  simple_cov_files:
25
30
  }
26
31
  end
@@ -29,18 +34,64 @@ class Uncov::Finder
29
34
 
30
35
  attr_reader :simple_cov_trigger
31
36
 
32
- def git_files = cache(:git_files) { Uncov::Finder::Git.files }
33
- def git_diff_files = cache(:git_diff_files) { Uncov::Finder::GitDiff.files }
34
- def file_system_files = cache(:file_system_files) { Uncov::Finder::FileSystem.new.files }
35
- def no_cov_files = cache(:no_cov_files) { Uncov::Finder::NoCov.new.files(file_system_files) }
36
- def simple_cov_files = cache(:simple_cov_files) { Uncov::Finder::SimpleCov.files(simple_cov_trigger_files) }
37
+ def git_files
38
+ git_finder.code_files
39
+ end
40
+
41
+ def git_test_files
42
+ git_finder.test_files
43
+ end
44
+
45
+ def git_finder
46
+ cache(:git_finder) do
47
+ Uncov::Finder::Git.new
48
+ end
49
+ end
50
+
51
+ def git_diff_files
52
+ git_diff_finder.code_files
53
+ end
54
+
55
+ def git_diff_test_files
56
+ git_diff_finder.test_files
57
+ end
58
+
59
+ def git_diff_finder
60
+ cache(:git_diff_finder) do
61
+ Uncov::Finder::GitDiff.new
62
+ end
63
+ end
64
+
65
+ def file_system_files
66
+ cache(:file_system_files) do
67
+ Uncov::Finder::FileSystem.new.files
68
+ end
69
+ end
70
+
71
+ def no_cov_files
72
+ cache(:no_cov_files) do
73
+ Uncov::Finder::NoCov.new.files(file_system_files)
74
+ end
75
+ end
76
+
77
+ def simple_cov_files
78
+ cache(:simple_cov_files) do
79
+ Uncov::Finder::SimpleCov.files(simple_cov_trigger_files)
80
+ end
81
+ end
37
82
 
38
83
  def simple_cov_trigger_files
39
84
  case simple_cov_trigger
40
85
  when :git
41
- git_file_names
86
+ git_file_names + git_test_files.keys
42
87
  when :git_diff
43
- git_diff_file_names
88
+ git_diff_file_names + git_diff_test_files.keys
89
+ when :file_system
90
+ file_system_file_names
91
+ else
92
+ # :nocov:
93
+ raise Uncov::UnsupportedSimpleCovTriggerError, simple_cov_trigger
94
+ # :nocov:
44
95
  end
45
96
  end
46
97
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'colorize'
4
- require 'forwardable'
5
4
 
6
5
  # print report to terminal with colors
7
6
  class Uncov::Formatter::Terminal
@@ -14,14 +13,19 @@ class Uncov::Formatter::Terminal
14
13
  end
15
14
 
16
15
  def output
17
- puts "Found #{report.uncovered_files.size} files with uncovered changes:".yellow
16
+ puts "Found #{report.display_files.size} files with uncovered changes:".yellow
18
17
  output_files
19
18
  puts
20
- puts format('Overall coverage of changes: %.2f%%', report.coverage).yellow
19
+ puts format(
20
+ 'Overall coverage of changes: %<coverage>.2f%% (%<covered_lines>d / %<relevant_lines>d)',
21
+ coverage: report.coverage,
22
+ covered_lines: report.covered_lines_count,
23
+ relevant_lines: report.relevant_lines_count
24
+ ).yellow
21
25
  end
22
26
 
23
27
  def output_files
24
- report.uncovered_files.each do |file_coverage|
28
+ report.display_files.each do |file_coverage|
25
29
  output_file(file_coverage)
26
30
  end
27
31
  end
@@ -40,8 +44,8 @@ class Uncov::Formatter::Terminal
40
44
  '%<name>s -> %<coverage>.2f%% (%<covered_lines>d / %<relevant_lines>d) changes covered, uncovered lines:',
41
45
  name: file_coverage.file_name,
42
46
  coverage: file_coverage.coverage,
43
- covered_lines: file_coverage.covered_lines.count,
44
- relevant_lines: file_coverage.relevant_lines.count
47
+ covered_lines: file_coverage.covered_lines_count,
48
+ relevant_lines: file_coverage.relevant_lines_count
45
49
  ).yellow
46
50
  end
47
51
 
@@ -50,6 +54,8 @@ class Uncov::Formatter::Terminal
50
54
  puts format_line(line, max).red
51
55
  elsif line.context
52
56
  puts format_line(line, max).green
57
+ elsif line.nocov_covered?
58
+ puts format_line(line, max).blue
53
59
  else
54
60
  # :nocov:
55
61
  raise 'unknown display line' # unreachable code
@@ -8,7 +8,7 @@ module Uncov::Formatter
8
8
  def output(report)
9
9
  if report.files.empty?
10
10
  return puts 'No files to report.'.green
11
- elsif !report.uncov?
11
+ elsif !report.trigger?
12
12
  return puts "All changed files(#{report.files.count}) have 100% test coverage!".green
13
13
  end
14
14
 
@@ -2,15 +2,38 @@
2
2
 
3
3
  # represents file line coverage in report
4
4
  class Uncov::Report::File::Line < Uncov::Struct.new(:number, :content, :simple_cov, :no_cov, :context, :git_diff)
5
+ def no_cov
6
+ return false if Uncov.configuration.nocov_ignore
7
+
8
+ self[:no_cov]
9
+ end
10
+
5
11
  def uncov?
6
12
  simple_cov == false && !no_cov
7
13
  end
8
14
 
15
+ def nocov_covered?
16
+ # :nocov
17
+ Uncov.configuration.nocov_covered && simple_cov == true && self[:no_cov]
18
+ # :nocov
19
+ end
20
+
21
+ def covered?
22
+ return false if Uncov.configuration.nocov_ignore && self[:no_cov]
23
+
24
+ (simple_cov == true && !no_cov) ||
25
+ (Uncov.configuration.nocov_covered && simple_cov == false && self[:no_cov])
26
+ end
27
+
28
+ def trigger?
29
+ uncov? || nocov_covered?
30
+ end
31
+
9
32
  def display?
10
- uncov? || context
33
+ trigger? || context
11
34
  end
12
35
 
13
36
  def relevant?
14
- !no_cov
37
+ trigger? || covered?
15
38
  end
16
39
  end
@@ -6,27 +6,27 @@ class Uncov::Report::File < Uncov::Struct.new(:file_name, :lines, :git)
6
6
 
7
7
  def coverage
8
8
  cache(:coverage) do
9
- if relevant_lines.count.zero?
9
+ if relevant_lines_count.zero?
10
10
  100.0
11
11
  else
12
- (covered_lines.count.to_f / relevant_lines.count * 100).round(2)
12
+ (covered_lines_count.to_f / relevant_lines_count * 100).round(2)
13
13
  end
14
14
  end
15
15
  end
16
16
 
17
- def uncov?
18
- uncov_lines.any?
17
+ def trigger?
18
+ cache(:trigger) do
19
+ lines.any?(&:trigger?)
20
+ end
19
21
  end
20
22
 
21
- def uncov_lines
22
- cache(:uncov_lines) do
23
- lines.select(&:uncov?)
24
- end
23
+ def display?
24
+ display_lines.any?
25
25
  end
26
26
 
27
- def covered_lines
28
- cache(:covered_lines) do
29
- lines.reject(&:uncov?)
27
+ def covered_lines_count
28
+ cache(:covered_lines_count) do
29
+ lines.count(&:covered?)
30
30
  end
31
31
  end
32
32
 
@@ -36,9 +36,9 @@ class Uncov::Report::File < Uncov::Struct.new(:file_name, :lines, :git)
36
36
  end
37
37
  end
38
38
 
39
- def relevant_lines
40
- cache(:relevant_lines) do
41
- lines.select(&:relevant?)
39
+ def relevant_lines_count
40
+ cache(:relevant_lines_count) do
41
+ lines.count(&:relevant?)
42
42
  end
43
43
  end
44
44
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ # report only files lines from the diff
4
+ module Uncov::Report::Generator::DiffFiles
5
+ Uncov::Report::Generator.register(self, :git_diff, 'Report missing coverage on added/changed files in the git diff')
6
+
7
+ class << self
8
+ def files(finder)
9
+ finder.git_diff_file_names.map do |file_name|
10
+ Uncov::Report::File.new(
11
+ file_name:,
12
+ git: true,
13
+ lines: lines(finder, file_name)
14
+ )
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def lines(finder, file_name)
21
+ lines_hash = file_lines(finder, file_name)
22
+ add_context(lines_hash)
23
+ lines_hash.sort.to_h.values
24
+ end
25
+
26
+ def file_lines(finder, file_name)
27
+ finder.file_system_file_lines(file_name).to_h do |line_number, content|
28
+ [line_number, new_line(finder, file_name, line_number, content)]
29
+ end
30
+ end
31
+
32
+ def add_context(lines_hash)
33
+ return if Uncov.configuration.context.zero?
34
+
35
+ line_numbers =
36
+ lines_hash.filter_map do |line_number, line|
37
+ line_number if line.trigger?
38
+ end
39
+ all_line_numbers = lines_hash.keys
40
+ context_line_numbers = Uncov::Report::Context.calculate(all_line_numbers, line_numbers, Uncov.configuration.context)
41
+ context_line_numbers.each do |line_number|
42
+ lines_hash[line_number].context = true
43
+ end
44
+ end
45
+
46
+ def new_line(finder, file_name, line_number, content)
47
+ Uncov::Report::File::Line.new(
48
+ number: line_number,
49
+ content:,
50
+ no_cov: finder.no_cov_file_line?(file_name, line_number),
51
+ simple_cov: finder.simple_cov_file_line?(file_name, line_number),
52
+ git_diff: finder.git_diff_file_line?(file_name, line_number),
53
+ context: false
54
+ )
55
+ end
56
+ end
57
+ end
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # report only files lines from the diff
4
- module Uncov::Report::DiffLines
4
+ module Uncov::Report::Generator::DiffLines
5
+ Uncov::Report::Generator.register(self, :git_diff, 'Report missing coverage on added lines in the git diff')
6
+
5
7
  class << self
6
8
  def files(finder)
7
9
  finder.git_diff_file_names.map do |file_name|
@@ -32,7 +34,7 @@ module Uncov::Report::DiffLines
32
34
 
33
35
  line_numbers =
34
36
  lines_hash.filter_map do |line_number, line|
35
- line_number if line.uncov?
37
+ line_number if line.trigger?
36
38
  end
37
39
  all_line_numbers = finder.file_system_file_lines(file_name).keys
38
40
  context_line_numbers = Uncov::Report::Context.calculate(all_line_numbers, line_numbers, Uncov.configuration.context)
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ # report only files lines from the diff
4
+ module Uncov::Report::Generator::GitFiles
5
+ Uncov::Report::Generator.register(self, :git, 'Report missing coverage on files tracked with git')
6
+
7
+ class << self
8
+ def files(finder)
9
+ finder.git_file_names.map do |file_name|
10
+ Uncov::Report::File.new(
11
+ file_name:,
12
+ git: true,
13
+ lines: lines(finder, file_name)
14
+ )
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def lines(finder, file_name)
21
+ lines_hash = file_lines(finder, file_name)
22
+ add_context(lines_hash)
23
+ lines_hash.sort.to_h.values
24
+ end
25
+
26
+ def file_lines(finder, file_name)
27
+ finder.file_system_file_lines(file_name).to_h do |line_number, content|
28
+ [line_number, new_line(finder, file_name, line_number, content)]
29
+ end
30
+ end
31
+
32
+ def add_context(lines_hash)
33
+ return if Uncov.configuration.context.zero?
34
+
35
+ line_numbers =
36
+ lines_hash.filter_map do |line_number, line|
37
+ line_number if line.trigger?
38
+ end
39
+ all_line_numbers = lines_hash.keys
40
+ context_line_numbers = Uncov::Report::Context.calculate(all_line_numbers, line_numbers, Uncov.configuration.context)
41
+ context_line_numbers.each do |line_number|
42
+ lines_hash[line_number].context = true
43
+ end
44
+ end
45
+
46
+ def new_line(finder, file_name, line_number, content)
47
+ Uncov::Report::File::Line.new(
48
+ number: line_number,
49
+ content:,
50
+ no_cov: finder.no_cov_file_line?(file_name, line_number),
51
+ simple_cov: finder.simple_cov_file_line?(file_name, line_number),
52
+ git_diff: finder.git_diff_file_line?(file_name, line_number),
53
+ context: false
54
+ )
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ # report only files lines from the diff
4
+ module Uncov::Report::Generator::NocovLines
5
+ Uncov::Report::Generator.register(self, :file_system, 'Report coverage on nocov lines, requires one or both: --nocov-ignore / --nocov-covered')
6
+
7
+ class << self
8
+ def files(finder)
9
+ finder.no_cov_file_names.filter_map do |file_name|
10
+ next if finder.no_cov_file_lines(file_name).empty?
11
+
12
+ Uncov::Report::File.new(
13
+ file_name:,
14
+ git: finder.git_file?(file_name),
15
+ lines: lines(finder, file_name)
16
+ )
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def lines(finder, file_name)
23
+ lines_hash = nocov_files_lines(finder, file_name)
24
+ add_context(finder, file_name, lines_hash)
25
+ lines_hash.sort.to_h.values
26
+ end
27
+
28
+ def nocov_files_lines(finder, file_name)
29
+ finder.no_cov_file_lines(file_name).keys.to_h do |line_number|
30
+ [line_number, new_line(finder, file_name, line_number)]
31
+ end
32
+ end
33
+
34
+ def add_context(finder, file_name, lines_hash)
35
+ return if Uncov.configuration.context.zero?
36
+
37
+ line_numbers =
38
+ lines_hash.filter_map do |line_number, line|
39
+ line_number if line.trigger?
40
+ end
41
+ all_line_numbers = finder.file_system_file_lines(file_name).keys
42
+ context_line_numbers = Uncov::Report::Context.calculate(all_line_numbers, line_numbers, Uncov.configuration.context)
43
+ context_line_numbers.each do |line_number|
44
+ context_line(finder, file_name, lines_hash, line_number)
45
+ end
46
+ end
47
+
48
+ def context_line(finder, file_name, lines_hash, line_number)
49
+ if lines_hash.key?(line_number)
50
+ lines_hash[line_number].context = true
51
+ else
52
+ lines_hash[line_number] = new_line(finder, file_name, line_number, context: true)
53
+ end
54
+ end
55
+
56
+ def new_line(finder, file_name, line_number, context: false)
57
+ Uncov::Report::File::Line.new(
58
+ number: line_number,
59
+ content: finder.file_system_file_line(file_name, line_number),
60
+ no_cov: finder.no_cov_file_line?(file_name, line_number),
61
+ simple_cov: finder.simple_cov_file_line?(file_name, line_number),
62
+ git_diff: finder.git_diff_file_line?(file_name, line_number),
63
+ context:
64
+ )
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../report'
4
+
5
+ # generate report files and lines for the configured report type
6
+ module Uncov::Report::Generator
7
+ class << self
8
+ def filters
9
+ @filters ||= {}
10
+ end
11
+
12
+ def register(generator_class, simple_cov_trigger, description)
13
+ class_name = generator_class.to_s.split('::').last
14
+ generator_name = class_name.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
15
+ filters[generator_name] = { generator_class:, simple_cov_trigger:, description: }
16
+ end
17
+
18
+ def generate
19
+ raise Uncov::UnsupportedReportTypeError, Uncov.configuration.report unless filters.key?(Uncov.configuration.report)
20
+
21
+ filters[Uncov.configuration.report] => { generator_class:, simple_cov_trigger: }
22
+ generator_class.files(Uncov::Finder.new(simple_cov_trigger))
23
+ end
24
+ end
25
+ end
data/lib/uncov/report.rb CHANGED
@@ -8,38 +8,37 @@ class Uncov::Report < Uncov::Struct.new(:files)
8
8
  include Uncov::Cache
9
9
 
10
10
  class << self
11
- def types
12
- %w[diff_lines]
13
- end
14
-
15
- def build
16
- files =
17
- case Uncov.configuration.report
18
- when 'diff_lines'
19
- finder = Uncov::Finder.new(:git_diff)
20
- Uncov::Report::DiffLines.files(finder)
21
- end
22
- new(files:)
11
+ def generate
12
+ new(files: Uncov::Report::Generator.generate)
23
13
  end
24
14
  end
25
15
 
26
- def uncovered_files
27
- cache(:uncovered_files) do
28
- files.select(&:uncov?)
16
+ def display_files
17
+ cache(:display_files) do
18
+ files.select(&:display?)
29
19
  end
30
20
  end
31
21
 
32
22
  def coverage
33
23
  cache(:coverage) do
34
- if files.empty?
24
+ if relevant_lines_count.zero?
35
25
  100.0
36
26
  else
37
- files.sum(&:coverage) / files.size
27
+ (covered_lines_count.to_f / relevant_lines_count * 100).round(2)
38
28
  end
39
29
  end
40
30
  end
41
31
 
42
- def uncov?
43
- uncovered_files.any?
32
+ def relevant_lines_count = files.sum(&:relevant_lines_count)
33
+ def covered_lines_count = files.sum(&:covered_lines_count)
34
+
35
+ def trigger?
36
+ cache(:trigger) do
37
+ files.any?(&:trigger?)
38
+ end
39
+ end
40
+
41
+ def display?
42
+ display_files.any?
44
43
  end
45
44
  end
data/lib/uncov/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Uncov
4
- VERSION = '0.4.1'
4
+ VERSION = '0.5.0'
5
5
  end
data/lib/uncov.rb CHANGED
@@ -10,7 +10,7 @@ module Uncov
10
10
  def configure(args = [])
11
11
  yield(configuration) if block_given?
12
12
  configuration.parse_cli(args) if args.any?
13
- warn({ configuration: configuration.options_values }.inspect) if configuration.debug
13
+ warn("{configuration: #{configuration.options_values.inspect}}") if configuration.debug
14
14
  nil
15
15
  end
16
16
 
@@ -29,8 +29,10 @@ module Uncov
29
29
 
30
30
  class ConfigurationError < Error; end
31
31
  class GitError < Error; end
32
- class SimpleCovError < Error; end
32
+ class FinderError < Error; end
33
+ class SimpleCovError < FinderError; end
33
34
  class FormatterError < Error; end
35
+ class ReportError < Error; end
34
36
  class OptionValueNotAllowed < ConfigurationError; end
35
37
 
36
38
  class NotGitRepoError < GitError
@@ -40,11 +42,18 @@ module Uncov
40
42
  def message = "#{path.inspect} is not in a git working tree"
41
43
  end
42
44
 
43
- class NotGitBranchError < GitError
45
+ class NotGitObjectError < GitError
44
46
  attr_reader :target_branch
45
47
 
46
48
  def initialize(target_branch) = @target_branch = target_branch
47
- def message = "Target branch #{target_branch.inspect} not found locally or in remote"
49
+ def message = "Git target #{target_branch.inspect} not found locally"
50
+ end
51
+
52
+ class UnsupportedSimpleCovTriggerError < FinderError
53
+ attr_reader :trigger
54
+
55
+ def initialize(trigger) = @trigger = trigger
56
+ def message = "#{trigger.inspect} is not a supported simple_cov_trigger type"
48
57
  end
49
58
 
50
59
  class FailedToGenerateReport < SimpleCovError
@@ -68,4 +77,11 @@ module Uncov
68
77
  def initialize(output_format) = @output_format = output_format
69
78
  def message = "#{output_format.inspect} is not a supported formatter"
70
79
  end
80
+
81
+ class UnsupportedReportTypeError < ReportError
82
+ attr_reader :type
83
+
84
+ def initialize(type) = @type = type
85
+ def message = "#{type.inspect} is not a supported report type"
86
+ end
71
87
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: uncov
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michał Papis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-05-05 00:00:00.000000000 Z
11
+ date: 2025-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colorize
@@ -111,9 +111,13 @@ files:
111
111
  - lib/uncov/formatter/terminal.rb
112
112
  - lib/uncov/report.rb
113
113
  - lib/uncov/report/context.rb
114
- - lib/uncov/report/diff_lines.rb
115
114
  - lib/uncov/report/file.rb
116
115
  - lib/uncov/report/file/line.rb
116
+ - lib/uncov/report/generator.rb
117
+ - lib/uncov/report/generator/diff_files.rb
118
+ - lib/uncov/report/generator/diff_lines.rb
119
+ - lib/uncov/report/generator/git_files.rb
120
+ - lib/uncov/report/generator/nocov_lines.rb
117
121
  - lib/uncov/struct.rb
118
122
  - lib/uncov/version.rb
119
123
  homepage: https://github.com/mpapis/uncov