Module: SingleCov

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

Constant Summary collapse

COVERAGES =
[]
MAX_OUTPUT =
40
APP_FOLDERS =
["models", "serializers", "helpers", "controllers", "mailers", "views", "jobs", "channels"]
BRANCH_COVERAGE_SUPPORTED =
(RUBY_VERSION >= "2.5.0")
UNCOVERED_COMMENT_MARKER =
"uncovered"
VERSION =
"1.4.0"

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.coverage_reportObject

enable coverage reporting, changed by forking-test-runner to combine multiple reports



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

def coverage_report
  @coverage_report
end

Class Method Details

.all_covered?(result) ⇒ Boolean

Returns:

  • (Boolean)


27
28
29
30
31
32
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/single_cov.rb', line 27

def all_covered?(result)
  errors = COVERAGES.map do |file, expected_uncovered|
    if coverage = result["#{root}/#{file}"]
      line_coverage = (coverage.is_a?(Hash) ? coverage.fetch(:lines) : coverage)
      uncovered_lines = line_coverage.each_with_index.map { |c, i| i + 1 if c == 0 }.compact

      branch_coverage = (coverage.is_a?(Hash) && coverage[:branches])
      uncovered_branches = (branch_coverage ? uncovered_branches(branch_coverage, uncovered_lines) : [])

      uncovered = uncovered_lines.concat uncovered_branches
      next if uncovered.size == expected_uncovered

      # ignore lines that are marked as uncovered via comments
      # NOTE: ideally we should also warn when using uncovered but the section is indeed covered
      content = File.readlines(file)
      uncovered.reject! do |line_start, _, _, _|
        content[line_start - 1].include?(UNCOVERED_COMMENT_MARKER)
      end
      next if uncovered.size == expected_uncovered

      # branches are unsorted and added to the end, only sort when displayed
      if branch_coverage
        uncovered.sort_by! { |line_start, char_start, _, _| [line_start, char_start || 0] }
      end

      uncovered.map! do |line_start, char_start, line_end, char_end|
        if char_start # branch coverage
          if line_start == line_end
            "#{file}:#{line_start}:#{char_start}-#{char_end}"
          else # possibly unreachable since branches always seem to be on the same line
            "#{file}:#{line_start}:#{char_start}-#{line_end}:#{char_end}"
          end
        else
          "#{file}:#{line_start}"
        end
      end

      warn_about_bad_coverage(file, expected_uncovered, uncovered)
    else
      warn_about_no_coverage(file)
    end
  end.compact

  return true if errors.empty?

  errors = errors.join("\n").split("\n") # unify arrays with multiline strings
  errors[MAX_OUTPUT..-1] = "... coverage output truncated" if errors.size >= MAX_OUTPUT
  warn errors

  errors.all? { |l| l.end_with?('?') } # ok if we just have warnings
end

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



88
89
90
91
92
93
94
95
96
97
98
# File 'lib/single_cov.rb', line 88

def assert_tested(files: glob('{app,lib}/**/*.rb'), tests: default_tests, untested: [])
  missing = files - tests.map { |t| file_under_test(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



79
80
81
82
83
84
85
86
# File 'lib/single_cov.rb', line 79

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



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

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

.disableObject

use this in forks when using rspec to silence duplicated output



133
134
135
# File 'lib/single_cov.rb', line 133

def disable
  @disabled = true
end

.not_covered!Object



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

def not_covered!
  store_pid
end

.rewrite(&block) ⇒ Object

optionally rewrite the file we guessed with a lambda



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

def rewrite(&block)
  @rewrite = block
end

.setup(framework, root: nil, branches: BRANCH_COVERAGE_SUPPORTED) ⇒ Object



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/single_cov.rb', line 100

def setup(framework, root: nil, branches: BRANCH_COVERAGE_SUPPORTED)
  if defined?(SimpleCov)
    raise "Load SimpleCov after SingleCov"
  end
  if branches && !BRANCH_COVERAGE_SUPPORTED
    raise "Branch coverage needs ruby >= 2.5.0"
  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