uncov 0.4.2 → 0.6.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.
@@ -4,41 +4,56 @@ 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 simplecov_trigger_files
19
+ code_files.keys + test_files
20
+ end
18
21
 
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] == '+'
22
+ private
23
23
 
24
- [changed_line.number, nil]
25
- end
26
- end.compact.to_h
24
+ def test_files
25
+ cache(:test_files) do
26
+ all_files_diff.filter_map do |file_diff|
27
+ file_diff.path if relevant_test_file?(file_diff.path) && File.exist?(file_diff.path)
28
+ end
27
29
  end
30
+ end
28
31
 
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)
32
+ def all_files_diff
33
+ cache(:all_files) do
34
+ git_diff
40
35
  end
36
+ end
37
+
38
+ def changed_lines(file_diff)
39
+ GitDiffParser.parse(file_diff.patch).flat_map do |patch|
40
+ patch.changed_lines.map do |changed_line|
41
+ next unless changed_line.content[0] == '+'
42
+
43
+ [changed_line.number, nil]
44
+ end
45
+ end.compact.to_h
46
+ end
47
+
48
+ def git_diff
49
+ repo = open_repo
50
+ git_target = repo.rev_parse(target)
51
+ repo.diff(git_target)
52
+ rescue Git::FailedError
53
+ raise Uncov::NotGitObjectError, target
54
+ end
41
55
 
42
- def target = Uncov.configuration.target
56
+ def target
57
+ Uncov.configuration.target
43
58
  end
44
59
  end
@@ -1,20 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # collect nocov information from files
4
- class Uncov::Finder::NoCov
5
- include Uncov::Cache
6
-
4
+ class Uncov::Finder::Nocov
7
5
  def files(all_files)
8
- all_files.to_h { |file_name, lines| [file_name, nocov_proc(file_name, lines)] }
6
+ all_files.files.transform_values do |lines|
7
+ nocov_lines(lines)
8
+ end
9
9
  end
10
10
 
11
11
  private
12
12
 
13
- def nocov_proc(file_name, lines_proc) = -> { cache(file_name) { read_nocov(lines_proc) } }
14
-
15
- def read_nocov(lines_proc)
13
+ def nocov_lines(lines)
16
14
  nocov = false
17
- lines_proc.call.filter_map do |number, line|
15
+ lines.filter_map do |number, line|
18
16
  line_nocov = line.strip.start_with?('# :nocov:')
19
17
  nocov = !nocov if line_nocov
20
18
  [number, true] if nocov || line_nocov # still true on disabling line
@@ -3,23 +3,38 @@
3
3
  require 'json'
4
4
 
5
5
  # collect coverage information, regenerates report if any trigger_files are newer then the report
6
- module Uncov::Finder::SimpleCov
6
+ module Uncov::Finder::Simplecov
7
7
  class << self
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
@@ -52,9 +67,9 @@ module Uncov::Finder::SimpleCov
52
67
  def raise_on_missing_coverage_path!
53
68
  return if coverage_path && File.exist?(coverage_path)
54
69
 
55
- raise Uncov::AutodetectSimpleCovPathError if Uncov.configuration.simplecov_file == 'autodetect'
70
+ raise Uncov::AutodetectSimplecovPathError if Uncov.configuration.simplecov_file == 'autodetect'
56
71
 
57
- raise Uncov::MissingSimpleCovReport, coverage_path
72
+ raise Uncov::MissingSimplecovReport, coverage_path
58
73
  end
59
74
  end
60
75
  end
data/lib/uncov/finder.rb CHANGED
@@ -4,43 +4,79 @@
4
4
  class Uncov::Finder
5
5
  include Uncov::Cache
6
6
 
7
- def initialize(simple_cov_trigger) = @simple_cov_trigger = simple_cov_trigger
8
- def git_file?(file_name) = git_files[file_name]
9
- def git_file_names = git_files.keys
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
- 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)
16
- def simple_cov_file_line?(file_name, line_number) = simple_cov_files.dig(file_name, line_number)
17
-
18
- def debug
19
- {
20
- git_files:,
21
- git_diff_files:,
22
- file_system_files: file_system_files.transform_values(&:call),
23
- no_cov_files: no_cov_files.transform_values(&:call),
24
- simple_cov_files:
25
- }
7
+ def initialize(simplecov_trigger)
8
+ @simplecov_trigger = simplecov_trigger
9
+ end
10
+
11
+ def build_line(file_name, line_number, context: false)
12
+ Uncov::Report::File::Line.new(
13
+ number: line_number,
14
+ content: file_system_files.line(file_name, line_number),
15
+ nocov: nocov_files.line(file_name, line_number),
16
+ simplecov: simplecov_files.line(file_name, line_number),
17
+ git_diff: git_diff_files.line?(file_name, line_number),
18
+ context:
19
+ )
20
+ end
21
+
22
+ def file_system_files
23
+ Uncov::Finder::Files.new(file_system_finder.code_files)
24
+ end
25
+
26
+ def git_files
27
+ Uncov::Finder::Files.new(git_finder.code_files)
28
+ end
29
+
30
+ def git_diff_files
31
+ Uncov::Finder::Files.new(git_diff_finder.code_files)
32
+ end
33
+
34
+ def nocov_files
35
+ cache(:nocov_files) do
36
+ Uncov::Finder::Files.new(Uncov::Finder::Nocov.new.files(file_system_files))
37
+ end
38
+ end
39
+
40
+ def simplecov_files
41
+ cache(:simplecov_files) do
42
+ Uncov::Finder::Files.new(Uncov::Finder::Simplecov.files(simplecov_trigger_files))
43
+ end
26
44
  end
27
45
 
28
46
  private
29
47
 
30
- attr_reader :simple_cov_trigger
48
+ attr_reader :simplecov_trigger
31
49
 
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) }
50
+ def file_system_finder
51
+ cache(:file_system_finder) do
52
+ Uncov::Finder::FileSystem.new
53
+ end
54
+ end
37
55
 
38
- def simple_cov_trigger_files
39
- case simple_cov_trigger
56
+ def git_finder
57
+ cache(:git_finder) do
58
+ Uncov::Finder::Git.new
59
+ end
60
+ end
61
+
62
+ def git_diff_finder
63
+ cache(:git_diff_finder) do
64
+ Uncov::Finder::GitDiff.new
65
+ end
66
+ end
67
+
68
+ def simplecov_trigger_files
69
+ case simplecov_trigger
40
70
  when :git
41
- git_file_names
71
+ git_finder
42
72
  when :git_diff
43
- git_diff_file_names
44
- end
73
+ git_diff_finder
74
+ when :file_system
75
+ file_system_finder
76
+ else
77
+ # :nocov:
78
+ raise Uncov::UnsupportedSimplecovTriggerError, simplecov_trigger
79
+ # :nocov:
80
+ end.simplecov_trigger_files
45
81
  end
46
82
  end
@@ -3,27 +3,14 @@
3
3
  # chose formater to output the report
4
4
  module Uncov::Formatter
5
5
  class << self
6
- def formats = %w[terminal]
7
-
8
- def output(report)
9
- if report.files.empty?
10
- return puts 'No files to report.'.green
11
- elsif !report.uncov?
12
- return puts "All changed files(#{report.files.count}) have 100% test coverage!".green
13
- end
14
-
15
- output_report(report)
6
+ def formatters
7
+ @formatters ||= Uncov.plugins.plugins_map('formatter')
16
8
  end
17
9
 
18
- private
10
+ def output(report)
11
+ raise Uncov::UnsupportedFormatterError, Uncov.configuration.output_format unless formatters.key?(Uncov.configuration.output_format)
19
12
 
20
- def output_report(report)
21
- case Uncov.configuration.output_format
22
- when 'terminal'
23
- Uncov::Formatter::Terminal.new(report).output
24
- else
25
- raise Uncov::UnsupportedFormatterError, Uncov.configuration.output_format
26
- end
13
+ formatters[Uncov.configuration.output_format].new(report).output
27
14
  end
28
15
  end
29
16
  end
@@ -4,6 +4,20 @@
4
4
  # @return [Integer] only added context lines matching all_line_numbers and not in important_line_numbers
5
5
  module Uncov::Report::Context
6
6
  class << self
7
+ def add_context(finder, file_name, lines_hash)
8
+ return if Uncov.configuration.context.zero?
9
+
10
+ line_numbers =
11
+ lines_hash.filter_map do |line_number, line|
12
+ line_number if line.trigger?
13
+ end
14
+ all_line_numbers = finder.file_system_files.lines(file_name).keys
15
+ context_line_numbers = calculate(all_line_numbers, line_numbers, Uncov.configuration.context)
16
+ context_line_numbers.each do |line_number|
17
+ mark_context_line(finder, file_name, lines_hash, line_number)
18
+ end
19
+ end
20
+
7
21
  def calculate(all_line_numbers, important_line_number, context)
8
22
  context_line_numbers = {}
9
23
  important_line_number.each do |line_number|
@@ -14,5 +28,15 @@ module Uncov::Report::Context
14
28
  end
15
29
  (context_line_numbers.keys.sort & all_line_numbers) - important_line_number
16
30
  end
31
+
32
+ private
33
+
34
+ def mark_context_line(finder, file_name, lines_hash, line_number)
35
+ if lines_hash.key?(line_number)
36
+ lines_hash[line_number].context = true
37
+ else
38
+ lines_hash[line_number] = finder.build_line(file_name, line_number, context: true)
39
+ end
40
+ end
17
41
  end
18
42
  end
@@ -1,20 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # represents file line coverage in report
4
- class Uncov::Report::File::Line < Uncov::Struct.new(:number, :content, :simple_cov, :no_cov, :context, :git_diff)
4
+ class Uncov::Report::File::Line < Uncov::Struct.new(:number, :content, :simplecov, :nocov, :context, :git_diff)
5
+ def nocov
6
+ return false if Uncov.configuration.nocov_ignore
7
+
8
+ self[:nocov]
9
+ end
10
+
5
11
  def uncov?
6
- simple_cov == false && !no_cov
12
+ simplecov == false && !nocov
13
+ end
14
+
15
+ def nocov_covered?
16
+ # :nocov
17
+ Uncov.configuration.nocov_covered && simplecov == true && self[:nocov]
18
+ # :nocov
7
19
  end
8
20
 
9
21
  def covered?
10
- simple_cov == true && !no_cov
22
+ return false if Uncov.configuration.nocov_ignore && self[:nocov]
23
+
24
+ (simplecov == true && !nocov) ||
25
+ (Uncov.configuration.nocov_covered && simplecov == false && self[:nocov])
26
+ end
27
+
28
+ def trigger?
29
+ uncov? || nocov_covered?
11
30
  end
12
31
 
13
32
  def display?
14
- uncov? || context
33
+ trigger? || context
15
34
  end
16
35
 
17
36
  def relevant?
18
- [false, true].include?(simple_cov) && !no_cov
37
+ trigger? || covered?
19
38
  end
20
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.select(&:covered?)
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,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pluginator'
4
+ require_relative '../report'
5
+
6
+ # generate report files and lines for the configured report type
7
+ module Uncov::Report::Filters
8
+ class << self
9
+ def filters
10
+ @filters ||= Uncov.plugins.plugins_map('report/filters')
11
+ end
12
+
13
+ def files
14
+ raise Uncov::UnsupportedReportTypeError, Uncov.configuration.report unless filters.key?(Uncov.configuration.report)
15
+
16
+ filter = filters[Uncov.configuration.report]
17
+ filter.files(Uncov::Finder.new(filter.simplecov_trigger))
18
+ end
19
+ end
20
+ 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::Filters.files)
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).round(2)
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.2'
4
+ VERSION = '0.6.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
 
@@ -21,6 +21,10 @@ module Uncov
21
21
  def configuration_reset!
22
22
  @configuration = Configuration.new
23
23
  end
24
+
25
+ def plugins
26
+ @plugins ||= Pluginator.find('uncov', extends: ['plugins_map'])
27
+ end
24
28
  end
25
29
 
26
30
  class Error < StandardError
@@ -29,8 +33,10 @@ module Uncov
29
33
 
30
34
  class ConfigurationError < Error; end
31
35
  class GitError < Error; end
32
- class SimpleCovError < Error; end
36
+ class FinderError < Error; end
37
+ class SimplecovError < FinderError; end
33
38
  class FormatterError < Error; end
39
+ class ReportError < Error; end
34
40
  class OptionValueNotAllowed < ConfigurationError; end
35
41
 
36
42
  class NotGitRepoError < GitError
@@ -40,25 +46,32 @@ module Uncov
40
46
  def message = "#{path.inspect} is not in a git working tree"
41
47
  end
42
48
 
43
- class NotGitBranchError < GitError
49
+ class NotGitObjectError < GitError
44
50
  attr_reader :target_branch
45
51
 
46
52
  def initialize(target_branch) = @target_branch = target_branch
47
- def message = "Target branch #{target_branch.inspect} not found locally or in remote"
53
+ def message = "Git target #{target_branch.inspect} not found locally"
54
+ end
55
+
56
+ class UnsupportedSimplecovTriggerError < FinderError
57
+ attr_reader :trigger
58
+
59
+ def initialize(trigger) = @trigger = trigger
60
+ def message = "#{trigger.inspect} is not a supported simplecov_trigger type"
48
61
  end
49
62
 
50
- class FailedToGenerateReport < SimpleCovError
63
+ class FailedToGenerateReport < SimplecovError
51
64
  def message = cause.message
52
65
  end
53
66
 
54
- class MissingSimpleCovReport < SimpleCovError
67
+ class MissingSimplecovReport < SimplecovError
55
68
  attr_reader :coverage_path
56
69
 
57
70
  def initialize(coverage_path) = @coverage_path = coverage_path
58
71
  def message = "SimpleCov results not found at #{coverage_path.inspect}"
59
72
  end
60
73
 
61
- class AutodetectSimpleCovPathError < SimpleCovError
74
+ class AutodetectSimplecovPathError < SimplecovError
62
75
  def message = 'Could not autodetect coverage report path'
63
76
  end
64
77
 
@@ -68,4 +81,11 @@ module Uncov
68
81
  def initialize(output_format) = @output_format = output_format
69
82
  def message = "#{output_format.inspect} is not a supported formatter"
70
83
  end
84
+
85
+ class UnsupportedReportTypeError < ReportError
86
+ attr_reader :type
87
+
88
+ def initialize(type) = @type = type
89
+ def message = "#{type.inspect} is not a supported report type"
90
+ end
71
91
  end