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/
PREFIXES_TO_IGNORE =

things to not prefix with lib/ etc

[]
VERSION =
"1.11.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



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

def coverage_report
  @coverage_report
end

.coverage_report_linesObject

emit only line coverage in coverage report for older coverage systems



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

def coverage_report_lines
  @coverage_report_lines
end

Class Method Details

.all_covered?(result) ⇒ Boolean

Returns:

  • (Boolean)


33
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
# File 'lib/single_cov.rb', line 33

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}")
    uncovered.reject! do |line_start, _, _, _, _|
      content[line_start - 1].match?(UNCOVERED_COMMENT_MARKER)
    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



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

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



70
71
72
73
74
75
76
77
78
79
80
# File 'lib/single_cov.rb', line 70

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



61
62
63
64
65
66
67
68
# File 'lib/single_cov.rb', line 61

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



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

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



142
143
144
# File 'lib/single_cov.rb', line 142

def disable
  @disabled = true
end

.not_covered!Object

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



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

def not_covered!
  main_process!
end

.rewrite(&block) ⇒ Object

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



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

def rewrite(&block)
  @rewrite = block
end

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



110
111
112
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
# File 'lib/single_cov.rb', line 110

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
    minitest_should_not_be_running!
    return if minitest_running_subset_of_tests?
  when :rspec
    return if rspec_running_subset_of_tests?
  else
    raise "Unsupported framework #{framework.inspect}"
  end

  start_coverage_recording

  override_at_exit do |status, _exception|
    if enabled? && main_process? && status == 0
      results = coverage_results
      generate_report results
      exit 1 unless SingleCov.all_covered?(results)
    end
  end
end