Module: SingleCov

Defined in:
lib/single_cov.rb,
lib/single_cov/version.rb

Constant Summary collapse

COVERAGES =
[]
MAX_OUTPUT =
Integer(ENV["SINGLE_COV_MAX_OUTPUT"] || "40")
RAILS_APP_FOLDERS =
["models", "serializers", "helpers", "controllers", "mailers", "views", "jobs", "channels"]
UNCOVERED_COMMENT_MARKER =
/#.*uncovered/
NOCOV_MARKER =
/^\s*#\s*:nocov:/
PREFIXES_TO_IGNORE =

things to not prefix with lib/ etc

[]
VERSION =
"2.1.0"

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.coverage_reportObject

enable coverage reporting: path to output file, changed by forking-test-runner at runtime to combine many reports



12
13
14
# File 'lib/single_cov.rb', line 12

def coverage_report
  @coverage_report
end

.coverage_report_linesObject

emit only line coverage in coverage report for older coverage systems



15
16
17
# File 'lib/single_cov.rb', line 15

def coverage_report_lines
  @coverage_report_lines
end

Class Method Details

.all_covered?(result) ⇒ Boolean

Returns:

  • (Boolean)


34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/single_cov.rb', line 34

def all_covered?(result)
  errors = COVERAGES.flat_map do |file, expected_uncovered|
    next no_coverage_error(file) unless (coverage = result["#{root}/#{file}"])

    uncovered = uncovered(coverage)
    next if uncovered.size == expected_uncovered

    # ignore lines that are marked as uncovered via comments
    # TODO: warn when using uncovered but the section is indeed covered
    content = File.readlines("#{root}/#{file}")
    nocov_lines_nums = nocov_line_numbers(content)
    uncovered.reject! do |line_start, _, _, _, _|
      content[line_start - 1].match?(UNCOVERED_COMMENT_MARKER) ||
        nocov_lines_nums.include?(line_start)
    end
    next if uncovered.size == expected_uncovered

    bad_coverage_error(file, expected_uncovered, uncovered)
  end.compact

  return true if errors.empty?

  if errors.size >= MAX_OUTPUT
    errors[MAX_OUTPUT..-1] = "... coverage output truncated (use SINGLE_COV_MAX_OUTPUT=999 to expand)"
  end
  @error_logger.puts errors

  errors.all? { |l| warning?(l) }
end

.assert_full_coverage(tests: default_tests, currently_complete: [], location: nil) ⇒ Object



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/single_cov.rb', line 85

def assert_full_coverage(tests: default_tests, currently_complete: [], location: nil)
  location ||= caller(0..1)[1].split(':in').first
  complete = tests.select { |file| File.read(file) =~ /SingleCov.covered!(?:(?!uncovered).)*(\s*|\s*\#.*)$/ }
  missing_complete = currently_complete - complete
  newly_complete = complete - currently_complete
  errors = []

  if missing_complete.any?
    errors << <<~MSG
      The following file(s) were previously marked as having 100% SingleCov test coverage (had no `coverage:` option) but are no longer marked as such.
      #{missing_complete.join("\n")}
      Please increase test coverage in these files to maintain 100% coverage and remove `coverage:` usage.

      If this test fails during a file removal, make it pass by removing all references to the removed file's path from the code base.
    MSG
  end

  if newly_complete.any?
    errors << <<~MSG
      The following files are newly at 100% SingleCov test coverage.
      Please add the following to #{location} to ensure 100% coverage is maintained moving forward.
      #{newly_complete.join("\n")}
    MSG
  end

  raise errors.join("\n") if errors.any?
end

.assert_tested(files: glob('{app,lib}/**/*.rb'), tests: default_tests, untested: []) ⇒ Object



73
74
75
76
77
78
79
80
81
82
83
# File 'lib/single_cov.rb', line 73

def assert_tested(files: glob('{app,lib}/**/*.rb'), tests: default_tests, untested: [])
  missing = files - tests.map { |t| guess_covered_file(t) }
  fixed = untested - missing
  missing -= untested

  if fixed.any?
    raise "Remove #{fixed.inspect} from untested!"
  elsif missing.any?
    raise missing.map { |f| "missing test for #{f}" }.join("\n")
  end
end

.assert_used(tests: default_tests) ⇒ Object



64
65
66
67
68
69
70
71
# File 'lib/single_cov.rb', line 64

def assert_used(tests: default_tests)
  bad = tests.select do |file|
    File.read(file) !~ /SingleCov.(not_)?covered!/
  end
  unless bad.empty?
    raise bad.map { |f| "#{f}: needs to use SingleCov.covered!" }.join("\n")
  end
end

.covered!(file: nil, uncovered: 0) ⇒ Object

mark the file under test as needing coverage



28
29
30
31
32
# File 'lib/single_cov.rb', line 28

def covered!(file: nil, uncovered: 0)
  file = ensure_covered_file(file)
  COVERAGES << [file, uncovered]
  main_process!
end

.disableObject

use this in forks when using rspec to silence duplicated output



164
165
166
# File 'lib/single_cov.rb', line 164

def disable
  @disabled = true
end

.not_covered!Object

mark a test file as not covering anything to make assert_used pass



23
24
25
# File 'lib/single_cov.rb', line 23

def not_covered!
  main_process!
end

.report_at_exitObject



156
157
158
159
160
161
# File 'lib/single_cov.rb', line 156

def report_at_exit
  return unless enabled?
  results = coverage_results
  generate_report results
  SingleCov.all_covered?(results)
end

.rewrite(&block) ⇒ Object

optionally rewrite the matching path single-cov guessed with a lambda



18
19
20
# File 'lib/single_cov.rb', line 18

def rewrite(&block)
  @rewrite = block
end

.setup(framework, root: nil, branches: true, err: $stderr) ⇒ Object



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/single_cov.rb', line 113

def setup(framework, root: nil, branches: true, err: $stderr)
  @error_logger = err

  if defined?(SimpleCov)
    raise "Load SimpleCov after SingleCov"
  end

  @branches = branches
  @root = root

  case framework
  when :minitest
    return if minitest_running_subset_of_tests?(ARGV)
  when :rspec
    return if rspec_running_subset_of_tests?
  else
    raise "Unsupported framework #{framework.inspect}"
  end

  start_coverage_recording

  # minitest overrides at_exit, so we need to get into it to execute after it finishes
  # so when using the `minitest` executable or loading minitest before SingleCov the first branch is used
  if defined?(Minitest)
    (class << Minitest; self; end).prepend(Module.new do
      def run(args = [])
        original_args = args.dup # minitest modified them
        result = super
        if result && !SingleCov.send(:minitest_running_subset_of_tests?, original_args)
          return SingleCov.report_at_exit
        end
        result
      end
    end)
  else
    override_at_exit do |status, _exception|
      if main_process? && status == 0 && !report_at_exit
        exit 1
      end
    end
  end
end