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.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +1058 -508
  4. data/doc/alternate-formatters.md +0 -5
  5. data/doc/commercial-services.md +5 -5
  6. data/exe/simplecov +11 -0
  7. data/lib/minitest/simplecov_plugin.rb +13 -5
  8. data/lib/simplecov/autostart.rb +11 -0
  9. data/lib/simplecov/cli/clean.rb +47 -0
  10. data/lib/simplecov/cli/coverage.rb +93 -0
  11. data/lib/simplecov/cli/diff.rb +152 -0
  12. data/lib/simplecov/cli/dotfile.rb +105 -0
  13. data/lib/simplecov/cli/merge.rb +118 -0
  14. data/lib/simplecov/cli/open.rb +50 -0
  15. data/lib/simplecov/cli/report.rb +89 -0
  16. data/lib/simplecov/cli/run.rb +36 -0
  17. data/lib/simplecov/cli/serve.rb +149 -0
  18. data/lib/simplecov/cli/uncovered.rb +108 -0
  19. data/lib/simplecov/cli.rb +150 -0
  20. data/lib/simplecov/color.rb +79 -0
  21. data/lib/simplecov/combine/branches_combiner.rb +28 -5
  22. data/lib/simplecov/combine/files_combiner.rb +47 -1
  23. data/lib/simplecov/combine/lines_combiner.rb +19 -17
  24. data/lib/simplecov/combine/methods_combiner.rb +49 -0
  25. data/lib/simplecov/combine/results_combiner.rb +7 -5
  26. data/lib/simplecov/command_guesser.rb +46 -32
  27. data/lib/simplecov/configuration/coverage.rb +175 -0
  28. data/lib/simplecov/configuration/coverage_criteria.rb +158 -0
  29. data/lib/simplecov/configuration/filters.rb +197 -0
  30. data/lib/simplecov/configuration/formatting.rb +129 -0
  31. data/lib/simplecov/configuration/ignored_entries.rb +63 -0
  32. data/lib/simplecov/configuration/merging.rb +164 -0
  33. data/lib/simplecov/configuration/thresholds.rb +178 -0
  34. data/lib/simplecov/configuration.rb +105 -412
  35. data/lib/simplecov/coverage_statistics.rb +15 -9
  36. data/lib/simplecov/coverage_violations.rb +149 -0
  37. data/lib/simplecov/defaults.rb +29 -22
  38. data/lib/simplecov/deprecation.rb +45 -0
  39. data/lib/simplecov/directive.rb +167 -0
  40. data/lib/simplecov/exit_codes/exit_code_handling.rb +8 -2
  41. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +19 -57
  42. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +45 -0
  43. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +17 -27
  44. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +41 -0
  45. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +38 -21
  46. data/lib/simplecov/exit_codes.rb +15 -0
  47. data/lib/simplecov/exit_handling.rb +158 -0
  48. data/lib/simplecov/file_list.rb +70 -19
  49. data/lib/simplecov/filter.rb +69 -24
  50. data/lib/simplecov/formatter/base.rb +102 -0
  51. data/lib/simplecov/formatter/html_formatter/public/application.css +1 -0
  52. data/lib/simplecov/formatter/html_formatter/public/application.js +18 -0
  53. data/lib/simplecov/formatter/html_formatter/public/favicon_green.png +0 -0
  54. data/lib/simplecov/formatter/html_formatter/public/favicon_red.png +0 -0
  55. data/lib/simplecov/formatter/html_formatter/public/favicon_yellow.png +0 -0
  56. data/lib/simplecov/formatter/html_formatter/public/index.html +56 -0
  57. data/lib/simplecov/formatter/html_formatter.rb +85 -0
  58. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +90 -0
  59. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +128 -0
  60. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +99 -0
  61. data/lib/simplecov/formatter/json_formatter.rb +89 -0
  62. data/lib/simplecov/formatter/multi_formatter.rb +11 -7
  63. data/lib/simplecov/formatter/simple_formatter.rb +11 -11
  64. data/lib/simplecov/formatter.rb +4 -0
  65. data/lib/simplecov/last_run.rb +10 -3
  66. data/lib/simplecov/lines_classifier.rb +25 -13
  67. data/lib/simplecov/load_global_config.rb +9 -4
  68. data/lib/simplecov/parallel_adapters/base.rb +61 -0
  69. data/lib/simplecov/parallel_adapters/generic.rb +44 -0
  70. data/lib/simplecov/parallel_adapters/parallel_tests.rb +93 -0
  71. data/lib/simplecov/parallel_adapters.rb +83 -0
  72. data/lib/simplecov/parallel_coordination.rb +129 -0
  73. data/lib/simplecov/process.rb +52 -12
  74. data/lib/simplecov/profiles/bundler_filter.rb +1 -1
  75. data/lib/simplecov/profiles/hidden_filter.rb +1 -1
  76. data/lib/simplecov/profiles/rails.rb +24 -10
  77. data/lib/simplecov/profiles/root_filter.rb +6 -5
  78. data/lib/simplecov/profiles/strict.rb +32 -0
  79. data/lib/simplecov/profiles/test_frameworks.rb +1 -4
  80. data/lib/simplecov/profiles.rb +32 -3
  81. data/lib/simplecov/result/missing_source_files_reporter.rb +49 -0
  82. data/lib/simplecov/result/source_file_builder.rb +51 -0
  83. data/lib/simplecov/result.rb +108 -19
  84. data/lib/simplecov/result_adapter.rb +105 -8
  85. data/lib/simplecov/result_merger/legacy_format_adapter.rb +28 -0
  86. data/lib/simplecov/result_merger/resultset_file.rb +38 -0
  87. data/lib/simplecov/result_merger/resultset_store.rb +50 -0
  88. data/lib/simplecov/result_merger.rb +59 -91
  89. data/lib/simplecov/result_processing.rb +179 -0
  90. data/lib/simplecov/simulate_coverage.rb +54 -8
  91. data/lib/simplecov/source_file/branch.rb +1 -3
  92. data/lib/simplecov/source_file/branch_builder.rb +114 -0
  93. data/lib/simplecov/source_file/builder_context.rb +28 -0
  94. data/lib/simplecov/source_file/line.rb +9 -4
  95. data/lib/simplecov/source_file/line_builder.rb +43 -0
  96. data/lib/simplecov/source_file/method.rb +52 -0
  97. data/lib/simplecov/source_file/method_builder.rb +60 -0
  98. data/lib/simplecov/source_file/ruby_data_parser.rb +88 -0
  99. data/lib/simplecov/source_file/skip_chunks.rb +75 -0
  100. data/lib/simplecov/source_file/source_loader.rb +63 -0
  101. data/lib/simplecov/source_file/statistics.rb +57 -0
  102. data/lib/simplecov/source_file.rb +66 -232
  103. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +49 -0
  104. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +247 -0
  105. data/lib/simplecov/static_coverage_extractor/method_collector.rb +55 -0
  106. data/lib/simplecov/static_coverage_extractor/value_position.rb +82 -0
  107. data/lib/simplecov/static_coverage_extractor/visitor.rb +196 -0
  108. data/lib/simplecov/static_coverage_extractor.rb +110 -0
  109. data/lib/simplecov/useless_results_remover.rb +16 -7
  110. data/lib/simplecov/version.rb +1 -1
  111. data/lib/simplecov-html.rb +4 -0
  112. data/lib/simplecov.rb +178 -380
  113. data/lib/simplecov_json_formatter.rb +4 -0
  114. data/schemas/coverage-v1.0.schema.json +306 -0
  115. data/schemas/coverage.schema.json +306 -0
  116. data/sig/simplecov.rbs +1638 -0
  117. metadata +94 -57
  118. data/CHANGELOG.md +0 -191
  119. 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
- # We intentionally read source code lazily to
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
- def coverage_statistics
33
- @coverage_statistics ||=
34
- {
35
- **line_coverage_statistics,
36
- **branch_coverage_statistics
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 ||= build_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. 0 if the file has no coverage lines
80
- def covered_percent
81
- coverage_statistics[:line]&.percent
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[:line]&.strength
103
+ def covered_strength(criterion = :line)
104
+ coverage_statistics(criterion)&.strength
86
105
  end
87
106
 
88
107
  def no_lines?
89
- lines.length.zero? || (lines.length == never_lines.size)
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 ||= build_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
- coverage_statistics[:branch]&.percent
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 ||= build_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
- # Select the covered branches
124
- # Here we user tree schema because some conditions like case may have additional
125
- # else that is not in declared inside the code but given by default by coverage report
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).select { |_type, count| count.zero? }.any?
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
- def ensure_remove_undefs(file_lines)
215
- # invalid/undef replace are technically not really necessary but nice to
216
- # have and work around a JRuby incompatibility. Also moved here from
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 build_lines
230
- coverage_exceeding_source_warn if coverage_data["lines"].size > src.size
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 process_skipped_lines(lines)
238
- # the array the lines are kept in is 0-based whereas the line numbers in the nocov
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
- def line_coverage_statistics
337
- {
338
- line: CoverageStatistics.new(
339
- total_strength: lines_strength,
340
- covered: covered_lines.size,
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
- def branch_coverage_statistics
347
- {
348
- branch: CoverageStatistics.new(
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