simplecov 0.22.0 → 1.0.0.rc1
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 +4 -4
- data/CHANGELOG.md +81 -1
- data/LICENSE +1 -1
- data/README.md +1009 -511
- data/doc/alternate-formatters.md +0 -5
- data/doc/commercial-services.md +5 -5
- data/exe/simplecov +11 -0
- data/lib/minitest/simplecov_plugin.rb +13 -5
- data/lib/simplecov/autostart.rb +11 -0
- data/lib/simplecov/cli/clean.rb +47 -0
- data/lib/simplecov/cli/coverage.rb +91 -0
- data/lib/simplecov/cli/diff.rb +151 -0
- data/lib/simplecov/cli/dotfile.rb +100 -0
- data/lib/simplecov/cli/merge.rb +116 -0
- data/lib/simplecov/cli/open.rb +50 -0
- data/lib/simplecov/cli/report.rb +84 -0
- data/lib/simplecov/cli/run.rb +36 -0
- data/lib/simplecov/cli/serve.rb +139 -0
- data/lib/simplecov/cli/uncovered.rb +107 -0
- data/lib/simplecov/cli.rb +150 -0
- data/lib/simplecov/color.rb +74 -0
- data/lib/simplecov/combine/branches_combiner.rb +3 -2
- data/lib/simplecov/combine/files_combiner.rb +7 -1
- data/lib/simplecov/combine/lines_combiner.rb +19 -17
- data/lib/simplecov/combine/methods_combiner.rb +26 -0
- data/lib/simplecov/combine/results_combiner.rb +5 -4
- data/lib/simplecov/command_guesser.rb +46 -32
- data/lib/simplecov/configuration/coverage.rb +171 -0
- data/lib/simplecov/configuration/coverage_criteria.rb +156 -0
- data/lib/simplecov/configuration/filters.rb +195 -0
- data/lib/simplecov/configuration/formatting.rb +119 -0
- data/lib/simplecov/configuration/ignored_entries.rb +63 -0
- data/lib/simplecov/configuration/merging.rb +74 -0
- data/lib/simplecov/configuration/thresholds.rb +174 -0
- data/lib/simplecov/configuration.rb +79 -405
- data/lib/simplecov/coverage_statistics.rb +12 -9
- data/lib/simplecov/coverage_violations.rb +148 -0
- data/lib/simplecov/defaults.rb +27 -20
- data/lib/simplecov/directive.rb +162 -0
- data/lib/simplecov/exit_codes/exit_code_handling.rb +8 -2
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +19 -57
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +45 -0
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +17 -27
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +41 -0
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +38 -21
- data/lib/simplecov/exit_codes.rb +3 -0
- data/lib/simplecov/exit_handling.rb +158 -0
- data/lib/simplecov/file_list.rb +61 -17
- data/lib/simplecov/filter.rb +69 -24
- data/lib/simplecov/formatter/base.rb +101 -0
- data/lib/simplecov/formatter/html_formatter/public/application.css +1 -0
- data/lib/simplecov/formatter/html_formatter/public/application.js +18 -0
- data/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
- data/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
- data/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
- data/lib/simplecov/formatter/html_formatter/public/index.html +56 -0
- data/lib/simplecov/formatter/html_formatter.rb +79 -0
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +84 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +127 -0
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +99 -0
- data/lib/simplecov/formatter/json_formatter.rb +77 -0
- data/lib/simplecov/formatter/multi_formatter.rb +4 -5
- data/lib/simplecov/formatter/simple_formatter.rb +9 -11
- data/lib/simplecov/formatter.rb +4 -0
- data/lib/simplecov/last_run.rb +10 -3
- data/lib/simplecov/lines_classifier.rb +26 -13
- data/lib/simplecov/load_global_config.rb +9 -4
- data/lib/simplecov/parallel_adapters/base.rb +51 -0
- data/lib/simplecov/parallel_adapters/generic.rb +42 -0
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +77 -0
- data/lib/simplecov/parallel_adapters.rb +83 -0
- data/lib/simplecov/parallel_coordination.rb +95 -0
- data/lib/simplecov/process.rb +20 -14
- data/lib/simplecov/profiles/bundler_filter.rb +1 -1
- data/lib/simplecov/profiles/hidden_filter.rb +1 -1
- data/lib/simplecov/profiles/rails.rb +24 -10
- data/lib/simplecov/profiles/root_filter.rb +6 -5
- data/lib/simplecov/profiles/strict.rb +32 -0
- data/lib/simplecov/profiles/test_frameworks.rb +1 -4
- data/lib/simplecov/profiles.rb +32 -3
- data/lib/simplecov/result/missing_source_files_reporter.rb +49 -0
- data/lib/simplecov/result/source_file_builder.rb +51 -0
- data/lib/simplecov/result.rb +97 -19
- data/lib/simplecov/result_adapter.rb +68 -6
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +28 -0
- data/lib/simplecov/result_merger/resultset_file.rb +38 -0
- data/lib/simplecov/result_merger/resultset_store.rb +50 -0
- data/lib/simplecov/result_merger.rb +46 -90
- data/lib/simplecov/result_processing.rb +162 -0
- data/lib/simplecov/simulate_coverage.rb +54 -8
- data/lib/simplecov/source_file/branch.rb +1 -3
- data/lib/simplecov/source_file/branch_builder.rb +114 -0
- data/lib/simplecov/source_file/builder_context.rb +28 -0
- data/lib/simplecov/source_file/line.rb +7 -2
- data/lib/simplecov/source_file/line_builder.rb +43 -0
- data/lib/simplecov/source_file/method.rb +52 -0
- data/lib/simplecov/source_file/method_builder.rb +58 -0
- data/lib/simplecov/source_file/ruby_data_parser.rb +88 -0
- data/lib/simplecov/source_file/skip_chunks.rb +77 -0
- data/lib/simplecov/source_file/source_loader.rb +63 -0
- data/lib/simplecov/source_file/statistics.rb +57 -0
- data/lib/simplecov/source_file.rb +66 -232
- data/lib/simplecov/static_coverage_extractor/visitor.rb +193 -0
- data/lib/simplecov/static_coverage_extractor.rb +111 -0
- data/lib/simplecov/useless_results_remover.rb +16 -7
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov-html.rb +4 -0
- data/lib/simplecov.rb +131 -377
- data/lib/simplecov_json_formatter.rb +4 -0
- data/schemas/coverage-v1.0.schema.json +300 -0
- data/schemas/coverage.schema.json +300 -0
- metadata +88 -56
- data/lib/simplecov/default_formatter.rb +0 -20
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class SourceFile
|
|
5
|
+
# Builds the `SourceFile::Line` objects for a source file from the
|
|
6
|
+
# raw line-coverage array. Each line carries its source text, its
|
|
7
|
+
# 1-based line number, and the Coverage hit count (or nil for
|
|
8
|
+
# never-counted lines). Applies `# simplecov:disable` /
|
|
9
|
+
# `# :nocov:` block ranges via `skipped!`.
|
|
10
|
+
class LineBuilder
|
|
11
|
+
def initialize(source_file)
|
|
12
|
+
@source_file = source_file
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
lines = build_lines
|
|
17
|
+
mark_skipped(lines, @source_file.skip_chunks_for(:line))
|
|
18
|
+
lines
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
# When `:line` coverage is disabled, the Ruby Coverage module
|
|
24
|
+
# doesn't emit "lines" data, so look up `nil` (never-counted) for
|
|
25
|
+
# every position. The source rows are still useful — e.g. for the
|
|
26
|
+
# HTML report's source view — even without per-line hits.
|
|
27
|
+
def build_lines
|
|
28
|
+
line_coverage = @source_file.coverage_data["lines"] || []
|
|
29
|
+
@source_file.src.map.with_index(1) do |src, i|
|
|
30
|
+
SourceFile::Line.new(src, i, line_coverage[i - 1])
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The array the lines are kept in is 0-based whereas the line
|
|
35
|
+
# numbers in the chunks are 1-based (more understandable elsewhere),
|
|
36
|
+
# so each range needs to be shifted down by one to slice into the
|
|
37
|
+
# `lines` array.
|
|
38
|
+
def mark_skipped(lines, chunks)
|
|
39
|
+
chunks.each { |chunk| lines[(chunk.begin - 1)..(chunk.end - 1)].each(&:skipped!) }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class SourceFile
|
|
5
|
+
# Represents a single method detected in coverage data.
|
|
6
|
+
# Provides helpers similar to Branch for coverage status.
|
|
7
|
+
class Method
|
|
8
|
+
attr_reader :source_file, :coverage, :class_name, :method_name,
|
|
9
|
+
:start_line, :start_col, :end_line, :end_col
|
|
10
|
+
|
|
11
|
+
def initialize(source_file, info, coverage)
|
|
12
|
+
@source_file = source_file
|
|
13
|
+
@class_name, @method_name, @start_line, @start_col, @end_line, @end_col = info
|
|
14
|
+
@coverage = coverage
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def covered?
|
|
18
|
+
!skipped? && coverage.positive?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def skipped?
|
|
22
|
+
return @skipped if defined?(@skipped)
|
|
23
|
+
|
|
24
|
+
@skipped = lines.all?(&:skipped?)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Flag the method as skipped directly, without going through its lines.
|
|
28
|
+
def skipped!
|
|
29
|
+
@skipped = true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def missed?
|
|
33
|
+
!skipped? && coverage.zero?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def lines
|
|
37
|
+
@lines ||= start_line && end_line ? source_file.lines[(start_line - 1)..(end_line - 1)] : []
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Whether this method's source range intersects the given inclusive line range.
|
|
41
|
+
def overlaps_with?(line_range)
|
|
42
|
+
return false unless start_line && end_line
|
|
43
|
+
|
|
44
|
+
start_line <= line_range.end && end_line >= line_range.begin
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_s
|
|
48
|
+
"#{class_name}##{method_name}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class SourceFile
|
|
5
|
+
# Builds the `SourceFile::Method` objects for a source file from
|
|
6
|
+
# the raw method-coverage data. Applies the
|
|
7
|
+
# `ignore_methods :eval_generated` filter and marks methods inside
|
|
8
|
+
# `# simplecov:disable` / `# :nocov:` chunks as skipped.
|
|
9
|
+
class MethodBuilder
|
|
10
|
+
def initialize(source_file)
|
|
11
|
+
@source_file = source_file
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
methods = @source_file.coverage_data.fetch("methods", {}).filter_map do |info, hit_count|
|
|
16
|
+
info = RubyDataParser.call(info)
|
|
17
|
+
next if eval_generated_to_ignore?(info)
|
|
18
|
+
|
|
19
|
+
SourceFile::Method.new(@source_file, info, hit_count)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
process_skipped(methods)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# See `BranchBuilder#eval_generated_condition_to_ignore?` for the
|
|
28
|
+
# rationale. Coverage reports an eval'd `def` at the eval caller's
|
|
29
|
+
# line and name, so a method whose `(name, start_line)` is absent
|
|
30
|
+
# from the real-source `def` set is eval-generated. Only consulted
|
|
31
|
+
# when the user has opted in via
|
|
32
|
+
# `SimpleCov.ignore_methods :eval_generated`. See #1046.
|
|
33
|
+
def eval_generated_to_ignore?(info)
|
|
34
|
+
return false unless SimpleCov.ignored_method?(:eval_generated)
|
|
35
|
+
|
|
36
|
+
positions = @source_file.real_source_positions
|
|
37
|
+
# simplecov:disable branch — nil branch fires only when Prism is unavailable
|
|
38
|
+
return false unless positions
|
|
39
|
+
|
|
40
|
+
# simplecov:enable branch
|
|
41
|
+
|
|
42
|
+
_class_name, name, start_line, * = info
|
|
43
|
+
!positions[:methods].include?([name, start_line])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def process_skipped(methods)
|
|
47
|
+
chunks = @source_file.skip_chunks_for(:method)
|
|
48
|
+
return methods if chunks.empty?
|
|
49
|
+
|
|
50
|
+
methods.each do |method|
|
|
51
|
+
method.skipped! if chunks.any? { |chunk| method.overlaps_with?(chunk) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
methods
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ripper"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
class SourceFile
|
|
7
|
+
# `Coverage.result` reports condition and method keys as Ruby
|
|
8
|
+
# arrays. When the resultset is round-tripped through JSON those
|
|
9
|
+
# array keys become their stringified inspect form, so this parser
|
|
10
|
+
# walks the literal back into a real Array without using `eval` (see
|
|
11
|
+
# #801). The grammar covers symbols, strings, integers, unary minus,
|
|
12
|
+
# and constant paths — every shape Coverage ever emits.
|
|
13
|
+
module RubyDataParser
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# Tests use the real data structures (except for integration tests)
|
|
17
|
+
# so no need to put them through here.
|
|
18
|
+
def call(structure)
|
|
19
|
+
return structure if structure.is_a?(Array)
|
|
20
|
+
|
|
21
|
+
parse_array_string(structure.to_s)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Parse a string like '[:if, 0, 3, 4, 3, 21]' or
|
|
25
|
+
# '["ClassName", :method1, 2, 2, 5, 5]' back into a Ruby array.
|
|
26
|
+
def parse_array_string(str)
|
|
27
|
+
# Try plain Ripper first; only pre-quote `#<...>` inspect segments
|
|
28
|
+
# if the input isn't already valid Ruby (otherwise we corrupt
|
|
29
|
+
# `"#<Class:Foo>"` strings that *are* valid Ruby literals — exactly
|
|
30
|
+
# the shape simplecov-on-simplecov method-coverage keys take).
|
|
31
|
+
sexp = Ripper.sexp(str) || Ripper.sexp(quote_inspected_class_segments(str))
|
|
32
|
+
# simplecov:disable — defensive: Ripper.sexp returning nil from both passes requires malformed input
|
|
33
|
+
array_node = sexp&.dig(1, 0)
|
|
34
|
+
# simplecov:enable
|
|
35
|
+
raise ArgumentError, "expected array literal: #{str.inspect}" unless array_node && array_node[0] == :array
|
|
36
|
+
|
|
37
|
+
Array(array_node[1]).map { |element| parse_element(element) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def parse_element(node)
|
|
41
|
+
case node[0]
|
|
42
|
+
when :@int, :unary then parse_integer_node(node)
|
|
43
|
+
when :symbol_literal, :dyna_symbol then parse_symbol_node(node)
|
|
44
|
+
when :string_literal then unescape_ruby(string_literal_text(node[1]))
|
|
45
|
+
when :var_ref then node.dig(1, 1) # `Foo`
|
|
46
|
+
when :const_path_ref then "#{parse_element(node[1])}::#{node[2][1]}" # `Foo::Bar`
|
|
47
|
+
else
|
|
48
|
+
# simplecov:disable — defensive fallback for unexpected Ripper node shapes
|
|
49
|
+
raise ArgumentError, "unexpected element: #{node.inspect}"
|
|
50
|
+
# simplecov:enable
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def parse_integer_node(node)
|
|
55
|
+
node[0] == :@int ? node[1].to_i : -node[2][1].to_i
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def parse_symbol_node(node)
|
|
59
|
+
if node[0] == :symbol_literal
|
|
60
|
+
node.dig(1, 1, 1).to_sym
|
|
61
|
+
else
|
|
62
|
+
unescape_ruby(string_literal_text(node[1])).to_sym
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Concatenate the text fragments of a `:string_content` node. Ripper
|
|
67
|
+
# may emit zero, one, or many `:@tstring_content` children depending
|
|
68
|
+
# on the literal.
|
|
69
|
+
def string_literal_text(string_content)
|
|
70
|
+
Array(string_content[1..]).map { |child| child[1] }.join
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Undo the same backslash-prefix escapes the previous hand-rolled
|
|
74
|
+
# parser undid: `\X` → `X` for any X.
|
|
75
|
+
def unescape_ruby(raw)
|
|
76
|
+
raw.gsub(/\\(.)/) { ::Regexp.last_match(1) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Method coverage keys can contain inspect-format class references
|
|
80
|
+
# like `#<Class:Foo>` or `#<Class:0x...>`, which aren't valid Ruby
|
|
81
|
+
# syntax. Wrap them in quotes so Ripper can parse the surrounding
|
|
82
|
+
# array literal; downstream we treat them as opaque strings.
|
|
83
|
+
def quote_inspected_class_segments(str)
|
|
84
|
+
str.gsub(/#<[^>]*>/) { |segment| %("#{segment.gsub('"', '\\"')}") }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module SimpleCov
|
|
6
|
+
class SourceFile
|
|
7
|
+
# Computes the set of line ranges that should be excluded from a
|
|
8
|
+
# SourceFile's coverage for each criterion. Two sources contribute:
|
|
9
|
+
#
|
|
10
|
+
# * The deprecated `# :nocov:` block toggle (lines wrapped between
|
|
11
|
+
# even-numbered pairs of nocov markers are excluded from line and
|
|
12
|
+
# branch coverage).
|
|
13
|
+
# * `# simplecov:disable` / `# simplecov:enable` block directives,
|
|
14
|
+
# which can be scoped per-criterion (`# simplecov:disable branch`,
|
|
15
|
+
# etc.) — see `SimpleCov::Directive`.
|
|
16
|
+
class SkipChunks
|
|
17
|
+
@nocov_warned = Set.new
|
|
18
|
+
class << self
|
|
19
|
+
attr_reader :nocov_warned
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(filename, src)
|
|
23
|
+
@filename = filename
|
|
24
|
+
@src = src
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# `:method` ignores nocov chunks (Ruby's Coverage doesn't tie
|
|
28
|
+
# method entries to line ranges); `:line` / `:branch` honor both
|
|
29
|
+
# the nocov chunks and the per-criterion directive ranges.
|
|
30
|
+
def for(criterion)
|
|
31
|
+
if criterion == :method
|
|
32
|
+
directive_chunks.fetch(:method)
|
|
33
|
+
else
|
|
34
|
+
nocov_chunks + directive_chunks.fetch(criterion)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# no_cov_chunks is zero indexed to work directly with the array
|
|
39
|
+
# holding the lines.
|
|
40
|
+
def nocov_chunks
|
|
41
|
+
@nocov_chunks ||= build_nocov_chunks
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def directive_chunks
|
|
45
|
+
@directive_chunks ||= Directive.disabled_ranges(@src)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def build_nocov_chunks
|
|
51
|
+
no_cov_lines = @src.map.with_index(1).select { |line_src, _index| LinesClassifier.no_cov_line?(line_src) }
|
|
52
|
+
|
|
53
|
+
warn_nocov_deprecation(no_cov_lines.first.last) if no_cov_lines.any?
|
|
54
|
+
|
|
55
|
+
# If we have an uneven number of nocovs we assume they go to the
|
|
56
|
+
# end of the file, the source doesn't really matter. Can't deal
|
|
57
|
+
# with this within the each_slice due to differing behavior in
|
|
58
|
+
# JRuby: jruby/jruby#6048
|
|
59
|
+
no_cov_lines << ["", @src.size] if no_cov_lines.size.odd?
|
|
60
|
+
|
|
61
|
+
no_cov_lines.each_slice(2).map do |(_line_src_start, index_start), (_line_src_end, index_end)|
|
|
62
|
+
index_start..index_end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Emit a one-time-per-file deprecation warning pointing the user
|
|
67
|
+
# at the `# simplecov:disable` / `# simplecov:enable` replacement.
|
|
68
|
+
def warn_nocov_deprecation(first_line_number)
|
|
69
|
+
return unless self.class.nocov_warned.add?(@filename)
|
|
70
|
+
|
|
71
|
+
token = SimpleCov.current_nocov_token
|
|
72
|
+
warn "#{@filename}:#{first_line_number}: [DEPRECATION] `# :#{token}:` is deprecated and will be removed " \
|
|
73
|
+
"in a future release. Replace with `# simplecov:disable` / `# simplecov:enable` block comments."
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class SourceFile
|
|
5
|
+
# Reads a source file into an array of lines, honoring the source's
|
|
6
|
+
# shebang and `coding:` magic comment when present. Always
|
|
7
|
+
# transcodes to UTF-8 with invalid/undef bytes replaced — both for
|
|
8
|
+
# JRuby compatibility and to keep encoding shenanigans in one place
|
|
9
|
+
# (see #866).
|
|
10
|
+
module SourceLoader
|
|
11
|
+
SHEBANG_REGEX = /\A#!/
|
|
12
|
+
RUBY_FILE_ENCODING_MAGIC_COMMENT_REGEX = /\A#\s*(?:-\*-)?\s*(?:en)?coding:\s*(\S+)\s*(?:-\*-)?\s*\z/
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def call(filename)
|
|
17
|
+
lines = []
|
|
18
|
+
# The default encoding is UTF-8
|
|
19
|
+
File.open(filename, "rb:UTF-8") do |file|
|
|
20
|
+
current_line = file.gets
|
|
21
|
+
|
|
22
|
+
if shebang?(current_line)
|
|
23
|
+
lines << current_line
|
|
24
|
+
current_line = file.gets
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
read_lines(file, lines, current_line)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def shebang?(line)
|
|
32
|
+
SHEBANG_REGEX.match?(line)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def read_lines(file, lines, current_line)
|
|
36
|
+
return lines unless current_line
|
|
37
|
+
|
|
38
|
+
set_encoding_based_on_magic_comment(file, current_line)
|
|
39
|
+
lines.concat([current_line], ensure_remove_undefs(file.readlines))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Encoding magic comment must be placed at first line except for
|
|
43
|
+
# shebang.
|
|
44
|
+
def set_encoding_based_on_magic_comment(file, line)
|
|
45
|
+
if (match = RUBY_FILE_ENCODING_MAGIC_COMMENT_REGEX.match(line))
|
|
46
|
+
file.set_encoding(match[1], "UTF-8")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# invalid/undef replace are technically not really necessary but
|
|
51
|
+
# nice to have and work around a JRuby incompatibility. Setting
|
|
52
|
+
# these options on `file.set_encoding` doesn't seem to work
|
|
53
|
+
# properly, so it has to be done here.
|
|
54
|
+
def ensure_remove_undefs(file_lines)
|
|
55
|
+
file_lines.each do |line|
|
|
56
|
+
# simplecov:disable — defensive: only fires for non-UTF-8 source files
|
|
57
|
+
line.encode!("UTF-8", invalid: :replace, undef: :replace) unless line.encoding == Encoding::UTF_8
|
|
58
|
+
# simplecov:enable
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class SourceFile
|
|
5
|
+
# Builds the `CoverageStatistics` triple (`:line`, `:branch`, `:method`)
|
|
6
|
+
# for a SourceFile, regardless of which criteria were actually enabled
|
|
7
|
+
# during the run — disabled or empty criteria collapse to 0/0/0 so
|
|
8
|
+
# downstream consumers don't have to special-case enable-state.
|
|
9
|
+
class Statistics
|
|
10
|
+
def initialize(source_file)
|
|
11
|
+
@source_file = source_file
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
{
|
|
16
|
+
**line_statistics,
|
|
17
|
+
**branch_statistics,
|
|
18
|
+
**method_statistics
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def line_statistics
|
|
25
|
+
sf = @source_file
|
|
26
|
+
{
|
|
27
|
+
line: CoverageStatistics.new(
|
|
28
|
+
total_strength: sf.lines.sum { |line| line.coverage.to_i },
|
|
29
|
+
covered: sf.covered_lines.size,
|
|
30
|
+
missed: sf.missed_lines.size,
|
|
31
|
+
omitted: sf.never_lines.size
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def branch_statistics
|
|
37
|
+
sf = @source_file
|
|
38
|
+
# Files added via track_files but never loaded/required have no
|
|
39
|
+
# branch data. Report 0% instead of misleading 100% (see #902).
|
|
40
|
+
if sf.not_loaded? && sf.covered_branches.empty? && sf.missed_branches.empty?
|
|
41
|
+
return {branch: CoverageStatistics.new(covered: 0, missed: 0, percent: 0.0)}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
{branch: CoverageStatistics.new(covered: sf.covered_branches.size, missed: sf.missed_branches.size)}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def method_statistics
|
|
48
|
+
sf = @source_file
|
|
49
|
+
if sf.not_loaded? && sf.covered_methods.empty? && sf.missed_methods.empty?
|
|
50
|
+
return {method: CoverageStatistics.new(covered: 0, missed: 0, percent: 0.0)}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
{method: CoverageStatistics.new(covered: sf.covered_methods.size, missed: sf.missed_methods.size)}
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|