simplecov 0.22.0 → 1.0.2
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/LICENSE +1 -1
- data/README.md +1058 -508
- 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 +93 -0
- data/lib/simplecov/cli/diff.rb +152 -0
- data/lib/simplecov/cli/dotfile.rb +105 -0
- data/lib/simplecov/cli/merge.rb +118 -0
- data/lib/simplecov/cli/open.rb +50 -0
- data/lib/simplecov/cli/report.rb +89 -0
- data/lib/simplecov/cli/run.rb +36 -0
- data/lib/simplecov/cli/serve.rb +149 -0
- data/lib/simplecov/cli/uncovered.rb +108 -0
- data/lib/simplecov/cli.rb +150 -0
- data/lib/simplecov/color.rb +79 -0
- data/lib/simplecov/combine/branches_combiner.rb +28 -5
- data/lib/simplecov/combine/files_combiner.rb +47 -1
- data/lib/simplecov/combine/lines_combiner.rb +19 -17
- data/lib/simplecov/combine/methods_combiner.rb +49 -0
- data/lib/simplecov/combine/results_combiner.rb +7 -5
- data/lib/simplecov/command_guesser.rb +46 -32
- data/lib/simplecov/configuration/coverage.rb +175 -0
- data/lib/simplecov/configuration/coverage_criteria.rb +158 -0
- data/lib/simplecov/configuration/filters.rb +197 -0
- data/lib/simplecov/configuration/formatting.rb +129 -0
- data/lib/simplecov/configuration/ignored_entries.rb +63 -0
- data/lib/simplecov/configuration/merging.rb +164 -0
- data/lib/simplecov/configuration/thresholds.rb +178 -0
- data/lib/simplecov/configuration.rb +105 -412
- data/lib/simplecov/coverage_statistics.rb +15 -9
- data/lib/simplecov/coverage_violations.rb +149 -0
- data/lib/simplecov/defaults.rb +29 -22
- data/lib/simplecov/deprecation.rb +45 -0
- data/lib/simplecov/directive.rb +167 -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 +15 -0
- data/lib/simplecov/exit_handling.rb +158 -0
- data/lib/simplecov/file_list.rb +70 -19
- data/lib/simplecov/filter.rb +69 -24
- data/lib/simplecov/formatter/base.rb +102 -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 +85 -0
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +90 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +128 -0
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +99 -0
- data/lib/simplecov/formatter/json_formatter.rb +89 -0
- data/lib/simplecov/formatter/multi_formatter.rb +11 -7
- data/lib/simplecov/formatter/simple_formatter.rb +11 -11
- data/lib/simplecov/formatter.rb +4 -0
- data/lib/simplecov/last_run.rb +10 -3
- data/lib/simplecov/lines_classifier.rb +25 -13
- data/lib/simplecov/load_global_config.rb +9 -4
- data/lib/simplecov/parallel_adapters/base.rb +61 -0
- data/lib/simplecov/parallel_adapters/generic.rb +44 -0
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +93 -0
- data/lib/simplecov/parallel_adapters.rb +83 -0
- data/lib/simplecov/parallel_coordination.rb +129 -0
- data/lib/simplecov/process.rb +52 -12
- 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 +108 -19
- data/lib/simplecov/result_adapter.rb +105 -8
- 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 +59 -91
- data/lib/simplecov/result_processing.rb +179 -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 +9 -4
- 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 +60 -0
- data/lib/simplecov/source_file/ruby_data_parser.rb +88 -0
- data/lib/simplecov/source_file/skip_chunks.rb +75 -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/condition_folding.rb +49 -0
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +247 -0
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +55 -0
- data/lib/simplecov/static_coverage_extractor/value_position.rb +82 -0
- data/lib/simplecov/static_coverage_extractor/visitor.rb +196 -0
- data/lib/simplecov/static_coverage_extractor.rb +110 -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 +178 -380
- data/lib/simplecov_json_formatter.rb +4 -0
- data/schemas/coverage-v1.0.schema.json +306 -0
- data/schemas/coverage.schema.json +306 -0
- data/sig/simplecov.rbs +1638 -0
- metadata +94 -57
- data/CHANGELOG.md +0 -191
- data/lib/simplecov/default_formatter.rb +0 -20
|
@@ -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,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
class SourceFile
|
|
5
|
+
# Computes the set of line ranges that should be excluded from a
|
|
6
|
+
# SourceFile's coverage for each criterion. Two sources contribute:
|
|
7
|
+
#
|
|
8
|
+
# * The deprecated `# :nocov:` block toggle (lines wrapped between
|
|
9
|
+
# even-numbered pairs of nocov markers are excluded from line and
|
|
10
|
+
# branch coverage).
|
|
11
|
+
# * `# simplecov:disable` / `# simplecov:enable` block directives,
|
|
12
|
+
# which can be scoped per-criterion (`# simplecov:disable branch`,
|
|
13
|
+
# etc.) — see `SimpleCov::Directive`.
|
|
14
|
+
class SkipChunks
|
|
15
|
+
@nocov_warned = Set.new
|
|
16
|
+
class << self
|
|
17
|
+
attr_reader :nocov_warned
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(filename, src)
|
|
21
|
+
@filename = filename
|
|
22
|
+
@src = src
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# `:method` ignores nocov chunks (Ruby's Coverage doesn't tie
|
|
26
|
+
# method entries to line ranges); `:line` / `:branch` honor both
|
|
27
|
+
# the nocov chunks and the per-criterion directive ranges.
|
|
28
|
+
def for(criterion)
|
|
29
|
+
if criterion == :method
|
|
30
|
+
directive_chunks.fetch(:method)
|
|
31
|
+
else
|
|
32
|
+
nocov_chunks + directive_chunks.fetch(criterion)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# no_cov_chunks is zero indexed to work directly with the array
|
|
37
|
+
# holding the lines.
|
|
38
|
+
def nocov_chunks
|
|
39
|
+
@nocov_chunks ||= build_nocov_chunks
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def directive_chunks
|
|
43
|
+
@directive_chunks ||= Directive.disabled_ranges(@src)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def build_nocov_chunks
|
|
49
|
+
no_cov_lines = @src.map.with_index(1).select { |line_src, _index| LinesClassifier.no_cov_line?(line_src) }
|
|
50
|
+
|
|
51
|
+
warn_nocov_deprecation(no_cov_lines.first.last) if no_cov_lines.any?
|
|
52
|
+
|
|
53
|
+
# If we have an uneven number of nocovs we assume they go to the
|
|
54
|
+
# end of the file, the source doesn't really matter. Can't deal
|
|
55
|
+
# with this within the each_slice due to differing behavior in
|
|
56
|
+
# JRuby: jruby/jruby#6048
|
|
57
|
+
no_cov_lines << ["", @src.size] if no_cov_lines.size.odd?
|
|
58
|
+
|
|
59
|
+
no_cov_lines.each_slice(2).map do |(_line_src_start, index_start), (_line_src_end, index_end)|
|
|
60
|
+
index_start..index_end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Emit a one-time-per-file deprecation warning pointing the user
|
|
65
|
+
# at the `# simplecov:disable` / `# simplecov:enable` replacement.
|
|
66
|
+
def warn_nocov_deprecation(first_line_number)
|
|
67
|
+
return unless self.class.nocov_warned.add?(@filename)
|
|
68
|
+
|
|
69
|
+
token = SimpleCov.current_nocov_token
|
|
70
|
+
warn "#{@filename}:#{first_line_number}: [DEPRECATION] `# :#{token}:` is deprecated and will be removed " \
|
|
71
|
+
"in a future release. Replace with `# simplecov:disable` / `# simplecov:enable` block comments."
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
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 = [] #: Array[String]
|
|
18
|
+
# The default encoding is UTF-8
|
|
19
|
+
File.open(filename, "rb:UTF-8") do |file|
|
|
20
|
+
current_line = file.gets
|
|
21
|
+
|
|
22
|
+
if current_line && 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
|
|
@@ -1,46 +1,64 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "directive"
|
|
4
|
+
require_relative "static_coverage_extractor"
|
|
5
|
+
require_relative "source_file/ruby_data_parser"
|
|
6
|
+
require_relative "source_file/source_loader"
|
|
7
|
+
require_relative "source_file/skip_chunks"
|
|
8
|
+
require_relative "source_file/builder_context"
|
|
9
|
+
require_relative "source_file/line_builder"
|
|
10
|
+
require_relative "source_file/branch_builder"
|
|
11
|
+
require_relative "source_file/method_builder"
|
|
12
|
+
require_relative "source_file/statistics"
|
|
13
|
+
|
|
3
14
|
module SimpleCov
|
|
4
15
|
#
|
|
5
16
|
# Representation of a source file including it's coverage data, source code,
|
|
6
17
|
# source lines and featuring helpers to interpret that data.
|
|
7
18
|
#
|
|
8
19
|
class SourceFile
|
|
20
|
+
include BuilderContext
|
|
21
|
+
|
|
9
22
|
# The full path to this source file (e.g. /User/colszowka/projects/simplecov/lib/simplecov/source_file.rb)
|
|
10
23
|
attr_reader :filename
|
|
11
24
|
# The array of coverage data received from the Coverage.result
|
|
12
25
|
attr_reader :coverage_data
|
|
13
26
|
|
|
14
|
-
def initialize(filename, coverage_data)
|
|
27
|
+
def initialize(filename, coverage_data, loaded: true)
|
|
15
28
|
@filename = filename
|
|
16
29
|
@coverage_data = coverage_data
|
|
30
|
+
@loaded = loaded
|
|
17
31
|
end
|
|
18
32
|
|
|
19
33
|
# The path to this source file relative to the projects directory
|
|
20
34
|
def project_filename
|
|
21
|
-
@filename.delete_prefix(SimpleCov.root)
|
|
35
|
+
@filename.delete_prefix(SimpleCov.root).sub(%r{\A[/\\]}, "")
|
|
22
36
|
end
|
|
23
37
|
|
|
24
|
-
# The source code for this file. Aliased as :source
|
|
38
|
+
# The source code for this file. Aliased as :source.
|
|
39
|
+
# Intentionally read lazily to suppress reading unused source code.
|
|
25
40
|
def src
|
|
26
|
-
|
|
27
|
-
# suppress reading unused source code.
|
|
28
|
-
@src ||= load_source
|
|
41
|
+
@src ||= SourceLoader.call(filename)
|
|
29
42
|
end
|
|
30
43
|
alias source src
|
|
31
44
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
# Returns a hash keyed by every supported coverage criterion. Each
|
|
46
|
+
# value is a CoverageStatistics, even for criteria that weren't
|
|
47
|
+
# enabled during the run — those collapse to 0/0/0. Consumers
|
|
48
|
+
# (FileList, formatters) decide which keys to surface based on
|
|
49
|
+
# `SimpleCov.coverage_criterion_enabled?`.
|
|
50
|
+
# The per-criterion coverage statistics for this file. With no argument
|
|
51
|
+
# returns the `{line:, branch:, method:}` Hash; pass a criterion symbol
|
|
52
|
+
# (`:line` / `:branch` / `:method`) to get that one CoverageStatistics.
|
|
53
|
+
def coverage_statistics(criterion = nil)
|
|
54
|
+
stats = (@coverage_statistics ||= Statistics.new(self).call)
|
|
55
|
+
criterion ? stats[criterion] : stats
|
|
38
56
|
end
|
|
39
57
|
|
|
40
58
|
# Returns all source lines for this file as instances of SimpleCov::SourceFile::Line,
|
|
41
59
|
# and thus including coverage data. Aliased as :source_lines
|
|
42
60
|
def lines
|
|
43
|
-
@lines ||=
|
|
61
|
+
@lines ||= LineBuilder.new(self).call
|
|
44
62
|
end
|
|
45
63
|
alias source_lines lines
|
|
46
64
|
|
|
@@ -68,7 +86,7 @@ module SimpleCov
|
|
|
68
86
|
|
|
69
87
|
# Returns the number of relevant lines (covered + missed)
|
|
70
88
|
def lines_of_code
|
|
71
|
-
coverage_statistics[:line]&.total
|
|
89
|
+
coverage_statistics[:line]&.total || 0
|
|
72
90
|
end
|
|
73
91
|
|
|
74
92
|
# Access SimpleCov::SourceFile::Line source lines by line number
|
|
@@ -76,65 +94,60 @@ module SimpleCov
|
|
|
76
94
|
lines[number - 1]
|
|
77
95
|
end
|
|
78
96
|
|
|
79
|
-
# The coverage for this file in percent
|
|
80
|
-
|
|
81
|
-
|
|
97
|
+
# The coverage for this file in percent, for the given criterion (line by
|
|
98
|
+
# default). Returns nil if the criterion was not measured.
|
|
99
|
+
def covered_percent(criterion = :line)
|
|
100
|
+
coverage_statistics(criterion)&.percent
|
|
82
101
|
end
|
|
83
102
|
|
|
84
|
-
def covered_strength
|
|
85
|
-
coverage_statistics
|
|
103
|
+
def covered_strength(criterion = :line)
|
|
104
|
+
coverage_statistics(criterion)&.strength
|
|
86
105
|
end
|
|
87
106
|
|
|
88
107
|
def no_lines?
|
|
89
|
-
lines.
|
|
108
|
+
lines.empty? || (lines.length == never_lines.size)
|
|
90
109
|
end
|
|
91
110
|
|
|
92
111
|
def relevant_lines
|
|
93
112
|
lines.size - never_lines.size - skipped_lines.size
|
|
94
113
|
end
|
|
95
114
|
|
|
96
|
-
#
|
|
97
115
|
# Return all the branches inside current source file
|
|
98
116
|
def branches
|
|
99
|
-
@branches ||=
|
|
117
|
+
@branches ||= BranchBuilder.new(self).call
|
|
100
118
|
end
|
|
101
119
|
|
|
102
120
|
def no_branches?
|
|
103
121
|
total_branches.empty?
|
|
104
122
|
end
|
|
105
123
|
|
|
124
|
+
# DEPRECATED: use `covered_percent(:branch)`.
|
|
106
125
|
def branches_coverage_percent
|
|
107
|
-
|
|
126
|
+
SimpleCov::Deprecation.warn("`SimpleCov::SourceFile#branches_coverage_percent` is deprecated. " \
|
|
127
|
+
"Use `covered_percent(:branch)`.")
|
|
128
|
+
covered_percent(:branch)
|
|
108
129
|
end
|
|
109
130
|
|
|
110
|
-
#
|
|
111
131
|
# Return the relevant branches to source file
|
|
112
132
|
def total_branches
|
|
113
133
|
@total_branches ||= covered_branches + missed_branches
|
|
114
134
|
end
|
|
115
135
|
|
|
116
|
-
#
|
|
117
136
|
# Return hash with key of line number and branch coverage count as value
|
|
118
137
|
def branches_report
|
|
119
|
-
@branches_report ||=
|
|
138
|
+
@branches_report ||=
|
|
139
|
+
branches.reject(&:skipped?).group_by(&:report_line).transform_values { |bs| bs.map(&:report) }
|
|
120
140
|
end
|
|
121
141
|
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
# @return [Array]
|
|
128
|
-
#
|
|
142
|
+
# Select the covered branches. We use a tree schema here because
|
|
143
|
+
# some conditions like `case` may have an additional `else` that
|
|
144
|
+
# isn't declared in code but is given by default by the coverage
|
|
145
|
+
# report.
|
|
129
146
|
def covered_branches
|
|
130
147
|
@covered_branches ||= branches.select(&:covered?)
|
|
131
148
|
end
|
|
132
149
|
|
|
133
|
-
#
|
|
134
150
|
# Select the missed branches with coverage equal to zero
|
|
135
|
-
#
|
|
136
|
-
# @return [Array]
|
|
137
|
-
#
|
|
138
151
|
def missed_branches
|
|
139
152
|
@missed_branches ||= branches.select(&:missed?)
|
|
140
153
|
end
|
|
@@ -143,213 +156,34 @@ module SimpleCov
|
|
|
143
156
|
branches_report.fetch(line_number, [])
|
|
144
157
|
end
|
|
145
158
|
|
|
146
|
-
#
|
|
147
159
|
# Check if any branches missing on given line number
|
|
148
|
-
#
|
|
149
|
-
# @param [Integer] line_number
|
|
150
|
-
#
|
|
151
|
-
# @return [Boolean]
|
|
152
|
-
#
|
|
153
160
|
def line_with_missed_branch?(line_number)
|
|
154
|
-
branches_for_line(line_number).
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
private
|
|
158
|
-
|
|
159
|
-
# no_cov_chunks is zero indexed to work directly with the array holding the lines
|
|
160
|
-
def no_cov_chunks
|
|
161
|
-
@no_cov_chunks ||= build_no_cov_chunks
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def build_no_cov_chunks
|
|
165
|
-
no_cov_lines = src.map.with_index(1).select { |line_src, _index| LinesClassifier.no_cov_line?(line_src) }
|
|
166
|
-
|
|
167
|
-
# if we have an uneven number of nocovs we assume they go to the
|
|
168
|
-
# end of the file, the source doesn't really matter
|
|
169
|
-
# Can't deal with this within the each_slice due to differing
|
|
170
|
-
# behavior in JRuby: jruby/jruby#6048
|
|
171
|
-
no_cov_lines << ["", src.size] if no_cov_lines.size.odd?
|
|
172
|
-
|
|
173
|
-
no_cov_lines.each_slice(2).map do |(_line_src_start, index_start), (_line_src_end, index_end)|
|
|
174
|
-
index_start..index_end
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def load_source
|
|
179
|
-
lines = []
|
|
180
|
-
# The default encoding is UTF-8
|
|
181
|
-
File.open(filename, "rb:UTF-8") do |file|
|
|
182
|
-
current_line = file.gets
|
|
183
|
-
|
|
184
|
-
if shebang?(current_line)
|
|
185
|
-
lines << current_line
|
|
186
|
-
current_line = file.gets
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
read_lines(file, lines, current_line)
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
SHEBANG_REGEX = /\A#!/.freeze
|
|
194
|
-
def shebang?(line)
|
|
195
|
-
SHEBANG_REGEX.match?(line)
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def read_lines(file, lines, current_line)
|
|
199
|
-
return lines unless current_line
|
|
200
|
-
|
|
201
|
-
set_encoding_based_on_magic_comment(file, current_line)
|
|
202
|
-
lines.concat([current_line], ensure_remove_undefs(file.readlines))
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
RUBY_FILE_ENCODING_MAGIC_COMMENT_REGEX = /\A#\s*(?:-\*-)?\s*(?:en)?coding:\s*(\S+)\s*(?:-\*-)?\s*\z/.freeze
|
|
206
|
-
def set_encoding_based_on_magic_comment(file, line)
|
|
207
|
-
# Check for encoding magic comment
|
|
208
|
-
# Encoding magic comment must be placed at first line except for shebang
|
|
209
|
-
if (match = RUBY_FILE_ENCODING_MAGIC_COMMENT_REGEX.match(line))
|
|
210
|
-
file.set_encoding(match[1], "UTF-8")
|
|
211
|
-
end
|
|
161
|
+
branches_for_line(line_number).any? { |_type, count| count.zero? }
|
|
212
162
|
end
|
|
213
163
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
# simplecov-html to have encoding shenaningans in one place. See #866
|
|
218
|
-
# also setting these option on `file.set_encoding` doesn't seem to work
|
|
219
|
-
# properly so it has to be done here.
|
|
220
|
-
file_lines.each do |line|
|
|
221
|
-
if line.encoding == Encoding::UTF_8
|
|
222
|
-
line
|
|
223
|
-
else
|
|
224
|
-
line.encode!("UTF-8", invalid: :replace, undef: :replace)
|
|
225
|
-
end
|
|
226
|
-
end
|
|
164
|
+
# Return all methods detected in this source file
|
|
165
|
+
def methods
|
|
166
|
+
@methods ||= MethodBuilder.new(self).call
|
|
227
167
|
end
|
|
228
168
|
|
|
229
|
-
def
|
|
230
|
-
|
|
231
|
-
lines = src.map.with_index(1) do |src, i|
|
|
232
|
-
SimpleCov::SourceFile::Line.new(src, i, coverage_data["lines"][i - 1])
|
|
233
|
-
end
|
|
234
|
-
process_skipped_lines(lines)
|
|
169
|
+
def covered_methods
|
|
170
|
+
@covered_methods ||= methods.select(&:covered?)
|
|
235
171
|
end
|
|
236
172
|
|
|
237
|
-
def
|
|
238
|
-
|
|
239
|
-
# chunks are 1-based and are expected to be like this in other parts (and it's also
|
|
240
|
-
# arguably more understandable)
|
|
241
|
-
no_cov_chunks.each { |chunk| lines[(chunk.begin - 1)..(chunk.end - 1)].each(&:skipped!) }
|
|
242
|
-
|
|
243
|
-
lines
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
def lines_strength
|
|
247
|
-
lines.sum { |line| line.coverage.to_i }
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
# Warning to identify condition from Issue #56
|
|
251
|
-
def coverage_exceeding_source_warn
|
|
252
|
-
warn "Warning: coverage data provided by Coverage [#{coverage_data['lines'].size}] exceeds number of lines in #{filename} [#{src.size}]"
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
#
|
|
256
|
-
# Build full branches report
|
|
257
|
-
# Root branches represent the wrapper of all condition state that
|
|
258
|
-
# have inside the branches
|
|
259
|
-
#
|
|
260
|
-
# @return [Hash]
|
|
261
|
-
#
|
|
262
|
-
def build_branches_report
|
|
263
|
-
branches.reject(&:skipped?).each_with_object({}) do |branch, coverage_statistics|
|
|
264
|
-
coverage_statistics[branch.report_line] ||= []
|
|
265
|
-
coverage_statistics[branch.report_line] << branch.report
|
|
266
|
-
end
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
#
|
|
270
|
-
# Call recursive method that transform our static hash to array of objects
|
|
271
|
-
# @return [Array]
|
|
272
|
-
#
|
|
273
|
-
def build_branches
|
|
274
|
-
coverage_branch_data = coverage_data.fetch("branches", {})
|
|
275
|
-
branches = coverage_branch_data.flat_map do |condition, coverage_branches|
|
|
276
|
-
build_branches_from(condition, coverage_branches)
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
process_skipped_branches(branches)
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
def process_skipped_branches(branches)
|
|
283
|
-
return branches if no_cov_chunks.empty?
|
|
284
|
-
|
|
285
|
-
branches.each do |branch|
|
|
286
|
-
branch.skipped! if no_cov_chunks.any? { |no_cov_chunk| branch.overlaps_with?(no_cov_chunk) }
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
branches
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
# Since we are dumping to and loading from JSON, and we have arrays as keys those
|
|
293
|
-
# don't make their way back to us intact e.g. just as a string
|
|
294
|
-
#
|
|
295
|
-
# We should probably do something different here, but as it stands these are
|
|
296
|
-
# our data structures that we write so eval isn't _too_ bad.
|
|
297
|
-
#
|
|
298
|
-
# See #801
|
|
299
|
-
#
|
|
300
|
-
def restore_ruby_data_structure(structure)
|
|
301
|
-
# Tests use the real data structures (except for integration tests) so no need to
|
|
302
|
-
# put them through here.
|
|
303
|
-
return structure if structure.is_a?(Array)
|
|
304
|
-
|
|
305
|
-
# rubocop:disable Security/Eval
|
|
306
|
-
eval structure
|
|
307
|
-
# rubocop:enable Security/Eval
|
|
308
|
-
end
|
|
309
|
-
|
|
310
|
-
def build_branches_from(condition, branches)
|
|
311
|
-
# the format handed in from the coverage data is like this:
|
|
312
|
-
#
|
|
313
|
-
# [:then, 4, 6, 6, 6, 10]
|
|
314
|
-
#
|
|
315
|
-
# which is [type, id, start_line, start_col, end_line, end_col]
|
|
316
|
-
_condition_type, _condition_id, condition_start_line, * = restore_ruby_data_structure(condition)
|
|
317
|
-
|
|
318
|
-
branches.map do |branch_data, hit_count|
|
|
319
|
-
branch_data = restore_ruby_data_structure(branch_data)
|
|
320
|
-
build_branch(branch_data, hit_count, condition_start_line)
|
|
321
|
-
end
|
|
322
|
-
end
|
|
323
|
-
|
|
324
|
-
def build_branch(branch_data, hit_count, condition_start_line)
|
|
325
|
-
type, _id, start_line, _start_col, end_line, _end_col = branch_data
|
|
326
|
-
|
|
327
|
-
SourceFile::Branch.new(
|
|
328
|
-
start_line: start_line,
|
|
329
|
-
end_line: end_line,
|
|
330
|
-
coverage: hit_count,
|
|
331
|
-
inline: start_line == condition_start_line,
|
|
332
|
-
type: type
|
|
333
|
-
)
|
|
173
|
+
def missed_methods
|
|
174
|
+
@missed_methods ||= methods.select(&:missed?)
|
|
334
175
|
end
|
|
335
176
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
missed: missed_lines.size
|
|
342
|
-
)
|
|
343
|
-
}
|
|
177
|
+
# DEPRECATED: use `covered_percent(:method)`.
|
|
178
|
+
def methods_coverage_percent
|
|
179
|
+
SimpleCov::Deprecation.warn("`SimpleCov::SourceFile#methods_coverage_percent` is deprecated. " \
|
|
180
|
+
"Use `covered_percent(:method)`.")
|
|
181
|
+
covered_percent(:method)
|
|
344
182
|
end
|
|
345
183
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
covered: covered_branches.size,
|
|
350
|
-
missed: missed_branches.size
|
|
351
|
-
)
|
|
352
|
-
}
|
|
184
|
+
# Whether this file was added via track_files but never loaded/required.
|
|
185
|
+
def not_loaded?
|
|
186
|
+
!@loaded
|
|
353
187
|
end
|
|
354
188
|
end
|
|
355
189
|
end
|