simplecov 0.17.1 → 0.18.0

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -1
  3. data/CODE_OF_CONDUCT.md +76 -0
  4. data/README.md +265 -76
  5. data/doc/alternate-formatters.md +5 -0
  6. data/lib/simplecov.rb +225 -61
  7. data/lib/simplecov/combine.rb +30 -0
  8. data/lib/simplecov/combine/branches_combiner.rb +32 -0
  9. data/lib/simplecov/combine/files_combiner.rb +24 -0
  10. data/lib/simplecov/combine/lines_combiner.rb +43 -0
  11. data/lib/simplecov/combine/results_combiner.rb +60 -0
  12. data/lib/simplecov/command_guesser.rb +6 -3
  13. data/lib/simplecov/configuration.rb +110 -9
  14. data/lib/simplecov/coverage_statistics.rb +56 -0
  15. data/lib/simplecov/defaults.rb +4 -2
  16. data/lib/simplecov/file_list.rb +66 -13
  17. data/lib/simplecov/filter.rb +2 -1
  18. data/lib/simplecov/formatter/multi_formatter.rb +2 -2
  19. data/lib/simplecov/formatter/simple_formatter.rb +4 -4
  20. data/lib/simplecov/last_run.rb +3 -1
  21. data/lib/simplecov/lines_classifier.rb +2 -2
  22. data/lib/simplecov/profiles.rb +9 -7
  23. data/lib/simplecov/result.rb +39 -6
  24. data/lib/simplecov/result_adapter.rb +30 -0
  25. data/lib/simplecov/result_merger.rb +18 -11
  26. data/lib/simplecov/simulate_coverage.rb +29 -0
  27. data/lib/simplecov/source_file.rb +222 -125
  28. data/lib/simplecov/source_file/branch.rb +84 -0
  29. data/lib/simplecov/source_file/line.rb +72 -0
  30. data/lib/simplecov/useless_results_remover.rb +16 -0
  31. data/lib/simplecov/version.rb +1 -1
  32. metadata +32 -166
  33. data/lib/simplecov/jruby_fix.rb +0 -44
  34. data/lib/simplecov/railtie.rb +0 -9
  35. data/lib/simplecov/railties/tasks.rake +0 -13
  36. data/lib/simplecov/raw_coverage.rb +0 -41
@@ -18,7 +18,7 @@ module SimpleCov
18
18
  @filter_argument = filter_argument
19
19
  end
20
20
 
21
- def matches?(_)
21
+ def matches?(_source_file)
22
22
  raise "The base filter class is not intended for direct use"
23
23
  end
24
24
 
@@ -29,6 +29,7 @@ module SimpleCov
29
29
 
30
30
  def self.build_filter(filter_argument)
31
31
  return filter_argument if filter_argument.is_a?(SimpleCov::Filter)
32
+
32
33
  class_for_argument(filter_argument).new(filter_argument)
33
34
  end
34
35
 
@@ -8,8 +8,8 @@ module SimpleCov
8
8
  formatters.map do |formatter|
9
9
  begin
10
10
  formatter.new.format(result)
11
- rescue => e
12
- STDERR.puts("Formatter #{formatter} failed with #{e.class}: #{e.message} (#{e.backtrace.first})")
11
+ rescue StandardError => e
12
+ warn("Formatter #{formatter} failed with #{e.class}: #{e.message} (#{e.backtrace.first})")
13
13
  nil
14
14
  end
15
15
  end
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- #
4
- # A ridiculously simple formatter for SimpleCov results.
5
- #
6
3
  module SimpleCov
7
4
  module Formatter
5
+ #
6
+ # A ridiculously simple formatter for SimpleCov results.
7
+ #
8
8
  class SimpleFormatter
9
9
  # Takes a SimpleCov::Result and generates a string out of it
10
10
  def format(result)
11
- output = "".dup
11
+ output = +""
12
12
  result.groups.each do |name, files|
13
13
  output << "Group: #{name}\n"
14
14
  output << "=" * 40
@@ -11,9 +11,11 @@ module SimpleCov
11
11
 
12
12
  def read
13
13
  return nil unless File.exist?(last_run_path)
14
+
14
15
  json = File.read(last_run_path)
15
16
  return nil if json.strip.empty?
16
- JSON.parse(json)
17
+
18
+ JSON.parse(json, symbolize_names: true)
17
19
  end
18
20
 
19
21
  def write(json)
@@ -8,8 +8,8 @@ module SimpleCov
8
8
  RELEVANT = 0
9
9
  NOT_RELEVANT = nil
10
10
 
11
- WHITESPACE_LINE = /^\s*$/
12
- COMMENT_LINE = /^\s*#/
11
+ WHITESPACE_LINE = /^\s*$/.freeze
12
+ COMMENT_LINE = /^\s*#/.freeze
13
13
  WHITESPACE_OR_COMMENT_LINE = Regexp.union(WHITESPACE_LINE, COMMENT_LINE)
14
14
 
15
15
  def self.no_cov_line
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- #
4
- # Profiles are SimpleCov configuration procs that can be easily
5
- # loaded using SimpleCov.start :rails and defined using
6
- # SimpleCov.profiles.define :foo do
7
- # # SimpleCov configuration here, same as in SimpleCov.configure
8
- # end
9
- #
10
3
  module SimpleCov
4
+ #
5
+ # Profiles are SimpleCov configuration procs that can be easily
6
+ # loaded using SimpleCov.start :rails and defined using
7
+ # SimpleCov.profiles.define :foo do
8
+ # # SimpleCov configuration here, same as in SimpleCov.configure
9
+ # end
10
+ #
11
11
  class Profiles < Hash
12
12
  #
13
13
  # Define a SimpleCov profile:
@@ -18,6 +18,7 @@ module SimpleCov
18
18
  def define(name, &blk)
19
19
  name = name.to_sym
20
20
  raise "SimpleCov Profile '#{name}' is already defined" unless self[name].nil?
21
+
21
22
  self[name] = blk
22
23
  end
23
24
 
@@ -27,6 +28,7 @@ module SimpleCov
27
28
  def load(name)
28
29
  name = name.to_sym
29
30
  raise "Could not find SimpleCov Profile called '#{name}'" unless key?(name)
31
+
30
32
  SimpleCov.configure(&self[name])
31
33
  end
32
34
  end
@@ -5,7 +5,7 @@ require "forwardable"
5
5
 
6
6
  module SimpleCov
7
7
  #
8
- # A simplecov code coverage result, initialized from the Hash Ruby 1.9's built-in coverage
8
+ # A simplecov code coverage result, initialized from the Hash Ruby's built-in coverage
9
9
  # library generates (Coverage.result).
10
10
  #
11
11
  class Result
@@ -20,15 +20,16 @@ module SimpleCov
20
20
  # Explicitly set the command name that was used for this coverage result. Defaults to SimpleCov.command_name
21
21
  attr_writer :command_name
22
22
 
23
- def_delegators :files, :covered_percent, :covered_percentages, :least_covered_file, :covered_strength, :covered_lines, :missed_lines
23
+ def_delegators :files, :covered_percent, :covered_percentages, :least_covered_file, :covered_strength, :covered_lines, :missed_lines, :total_branches, :covered_branches, :missed_branches, :coverage_statistics
24
24
  def_delegator :files, :lines_of_code, :total_lines
25
25
 
26
26
  # Initialize a new SimpleCov::Result from given Coverage.result (a Hash of filenames each containing an array of
27
27
  # coverage data)
28
28
  def initialize(original_result)
29
- @original_result = original_result.freeze
30
- @files = SimpleCov::FileList.new(original_result.map do |filename, coverage|
31
- SimpleCov::SourceFile.new(filename, coverage) if File.file?(filename)
29
+ result = adapt_result(original_result)
30
+ @original_result = result.freeze
31
+ @files = SimpleCov::FileList.new(result.map do |filename, coverage|
32
+ SimpleCov::SourceFile.new(filename, JSON.parse(JSON.dump(coverage))) if File.file?(filename)
32
33
  end.compact.sort_by(&:filename))
33
34
  filter!
34
35
  end
@@ -61,13 +62,20 @@ module SimpleCov
61
62
 
62
63
  # Returns a hash representation of this Result that can be used for marshalling it into JSON
63
64
  def to_hash
64
- {command_name => {"coverage" => coverage, "timestamp" => created_at.to_i}}
65
+ {
66
+ command_name => {
67
+ "coverage" => coverage,
68
+ "timestamp" => created_at.to_i
69
+ }
70
+ }
65
71
  end
66
72
 
67
73
  # Loads a SimpleCov::Result#to_hash dump
68
74
  def self.from_hash(hash)
69
75
  command_name, data = hash.first
76
+
70
77
  result = SimpleCov::Result.new(data["coverage"])
78
+
71
79
  result.command_name = command_name
72
80
  result.created_at = Time.at(data["timestamp"])
73
81
  result
@@ -75,6 +83,31 @@ module SimpleCov
75
83
 
76
84
  private
77
85
 
86
+ # We changed the format of the raw result data in simplecov, as people are likely
87
+ # to have "old" resultsets lying around (but not too old so that they're still
88
+ # considered we can adapt them).
89
+ # See https://github.com/colszowka/simplecov/pull/824#issuecomment-576049747
90
+ def adapt_result(result)
91
+ if pre_simplecov_0_18_result?(result)
92
+ adapt_pre_simplecov_0_18_result(result)
93
+ else
94
+ result
95
+ end
96
+ end
97
+
98
+ # pre 0.18 coverage data pointed from file directly to an array of line coverage
99
+ def pre_simplecov_0_18_result?(result)
100
+ _key, data = result.first
101
+
102
+ data.is_a?(Array)
103
+ end
104
+
105
+ def adapt_pre_simplecov_0_18_result(result)
106
+ result.map do |file_path, line_coverage_data|
107
+ [file_path, {"lines" => line_coverage_data}]
108
+ end.to_h
109
+ end
110
+
78
111
  def coverage
79
112
  keys = original_result.keys & filenames
80
113
  Hash[keys.zip(original_result.values_at(*keys))]
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ #
5
+ # Responsible for adapting the format of the coverage result whether it's default or with statistics
6
+ #
7
+ class ResultAdapter
8
+ attr_reader :result
9
+
10
+ def initialize(result)
11
+ @result = result
12
+ end
13
+
14
+ def self.call(*args)
15
+ new(*args).adapt
16
+ end
17
+
18
+ def adapt
19
+ return unless result
20
+
21
+ result.each_with_object({}) do |(file_name, cover_statistic), adapted_result|
22
+ if cover_statistic.is_a?(Array)
23
+ adapted_result.merge!(file_name => {"lines" => cover_statistic})
24
+ else
25
+ adapted_result.merge!(file_name => cover_statistic)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -2,12 +2,12 @@
2
2
 
3
3
  require "json"
4
4
 
5
- #
6
- # Singleton that is responsible for caching, loading and merging
7
- # SimpleCov::Results into a single result for coverage analysis based
8
- # upon multiple test suites.
9
- #
10
5
  module SimpleCov
6
+ #
7
+ # Singleton that is responsible for caching, loading and merging
8
+ # SimpleCov::Results into a single result for coverage analysis based
9
+ # upon multiple test suites.
10
+ #
11
11
  module ResultMerger
12
12
  class << self
13
13
  # The path to the .resultset.json cache file
@@ -27,7 +27,7 @@ module SimpleCov
27
27
  if data
28
28
  begin
29
29
  JSON.parse(data) || {}
30
- rescue
30
+ rescue StandardError
31
31
  {}
32
32
  end
33
33
  else
@@ -40,8 +40,10 @@ module SimpleCov
40
40
  def stored_data
41
41
  synchronize_resultset do
42
42
  return unless File.exist?(resultset_path)
43
+
43
44
  data = File.read(resultset_path)
44
45
  return if data.nil? || data.length < 2
46
+
45
47
  data
46
48
  end
47
49
  end
@@ -55,19 +57,24 @@ module SimpleCov
55
57
  resultset.each do |command_name, data|
56
58
  result = SimpleCov::Result.from_hash(command_name => data)
57
59
  # Only add result if the timeout is above the configured threshold
58
- if (Time.now - result.created_at) < SimpleCov.merge_timeout
59
- results << result
60
- end
60
+ results << result if (Time.now - result.created_at) < SimpleCov.merge_timeout
61
61
  end
62
62
  results
63
63
  end
64
64
 
65
+ def merge_and_store(*results)
66
+ result = merge_results(*results)
67
+ store_result(result) if result
68
+ result
69
+ end
70
+
65
71
  # Merge two or more SimpleCov::Results into a new one with merged
66
72
  # coverage data and the command_name for the result consisting of a join
67
73
  # on all source result's names
68
74
  def merge_results(*results)
69
- merged = SimpleCov::RawCoverage.merge_results(*results.map(&:original_result))
70
- result = SimpleCov::Result.new(merged)
75
+ parsed_results = JSON.parse(JSON.dump(results.map(&:original_result)))
76
+ combined_result = SimpleCov::Combine::ResultsCombiner.combine(*parsed_results)
77
+ result = SimpleCov::Result.new(combined_result)
71
78
  # Specify the command name
72
79
  result.command_name = results.map(&:command_name).sort.join(", ")
73
80
  result
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ #
5
+ # Responsible for producing file coverage metrics.
6
+ #
7
+ module SimulateCoverage
8
+ module_function
9
+
10
+ #
11
+ # Simulate normal file coverage report on
12
+ # ruby 2.5 and return similar hash with lines and branches keys
13
+ #
14
+ # Happens when a file wasn't required but still tracked.
15
+ #
16
+ # @return [Hash]
17
+ #
18
+ def call(absolute_path)
19
+ lines = File.foreach(absolute_path)
20
+
21
+ {
22
+ "lines" => LinesClassifier.new.classify(lines),
23
+ # we don't want to parse branches ourselves...
24
+ # requiring files can have side effects and we don't want to trigger that
25
+ "branches" => {}
26
+ }
27
+ end
28
+ end
29
+ end
@@ -6,80 +6,14 @@ module SimpleCov
6
6
  # source lines and featuring helpers to interpret that data.
7
7
  #
8
8
  class SourceFile
9
- # Representation of a single line in a source file including
10
- # this specific line's source code, line_number and code coverage,
11
- # with the coverage being either nil (coverage not applicable, e.g. comment
12
- # line), 0 (line not covered) or >1 (the amount of times the line was
13
- # executed)
14
- class Line
15
- # The source code for this line. Aliased as :source
16
- attr_reader :src
17
- # The line number in the source file. Aliased as :line, :number
18
- attr_reader :line_number
19
- # The coverage data for this line: either nil (never), 0 (missed) or >=1 (times covered)
20
- attr_reader :coverage
21
- # Whether this line was skipped
22
- attr_reader :skipped
23
-
24
- # Lets grab some fancy aliases, shall we?
25
- alias source src
26
- alias line line_number
27
- alias number line_number
28
-
29
- def initialize(src, line_number, coverage)
30
- raise ArgumentError, "Only String accepted for source" unless src.is_a?(String)
31
- raise ArgumentError, "Only Integer accepted for line_number" unless line_number.is_a?(Integer)
32
- raise ArgumentError, "Only Integer and nil accepted for coverage" unless coverage.is_a?(Integer) || coverage.nil?
33
- @src = src
34
- @line_number = line_number
35
- @coverage = coverage
36
- @skipped = false
37
- end
38
-
39
- # Returns true if this is a line that should have been covered, but was not
40
- def missed?
41
- !never? && !skipped? && coverage.zero?
42
- end
43
-
44
- # Returns true if this is a line that has been covered
45
- def covered?
46
- !never? && !skipped? && coverage > 0
47
- end
48
-
49
- # Returns true if this line is not relevant for coverage
50
- def never?
51
- !skipped? && coverage.nil?
52
- end
53
-
54
- # Flags this line as skipped
55
- def skipped!
56
- @skipped = true
57
- end
58
-
59
- # Returns true if this line was skipped, false otherwise. Lines are skipped if they are wrapped with
60
- # # :nocov: comment lines.
61
- def skipped?
62
- !!skipped
63
- end
64
-
65
- # The status of this line - either covered, missed, skipped or never. Useful i.e. for direct use
66
- # as a css class in report generation
67
- def status
68
- return "skipped" if skipped?
69
- return "never" if never?
70
- return "missed" if missed?
71
- return "covered" if covered?
72
- end
73
- end
74
-
75
9
  # The full path to this source file (e.g. /User/colszowka/projects/simplecov/lib/simplecov/source_file.rb)
76
10
  attr_reader :filename
77
11
  # The array of coverage data received from the Coverage.result
78
- attr_reader :coverage
12
+ attr_reader :coverage_data
79
13
 
80
- def initialize(filename, coverage)
14
+ def initialize(filename, coverage_data)
81
15
  @filename = filename
82
- @coverage = coverage
16
+ @coverage_data = coverage_data
83
17
  end
84
18
 
85
19
  # The path to this source file relative to the projects directory
@@ -95,6 +29,14 @@ module SimpleCov
95
29
  end
96
30
  alias source src
97
31
 
32
+ def coverage_statistics
33
+ @coverage_statistics ||=
34
+ {
35
+ **line_coverage_statistics,
36
+ **branch_coverage_statistics
37
+ }
38
+ end
39
+
98
40
  # Returns all source lines for this file as instances of SimpleCov::SourceFile::Line,
99
41
  # and thus including coverage data. Aliased as :source_lines
100
42
  def lines
@@ -102,19 +44,31 @@ module SimpleCov
102
44
  end
103
45
  alias source_lines lines
104
46
 
105
- def build_lines
106
- coverage_exceeding_source_warn if coverage.size > src.size
47
+ # Returns all covered lines as SimpleCov::SourceFile::Line
48
+ def covered_lines
49
+ @covered_lines ||= lines.select(&:covered?)
50
+ end
107
51
 
108
- lines = src.map.with_index(1) do |src, i|
109
- SimpleCov::SourceFile::Line.new(src, i, coverage[i - 1])
110
- end
52
+ # Returns all lines that should have been, but were not covered
53
+ # as instances of SimpleCov::SourceFile::Line
54
+ def missed_lines
55
+ @missed_lines ||= lines.select(&:missed?)
56
+ end
111
57
 
112
- process_skipped_lines(lines)
58
+ # Returns all lines that are not relevant for coverage as
59
+ # SimpleCov::SourceFile::Line instances
60
+ def never_lines
61
+ @never_lines ||= lines.select(&:never?)
113
62
  end
114
63
 
115
- # Warning to identify condition from Issue #56
116
- def coverage_exceeding_source_warn
117
- $stderr.puts "Warning: coverage data provided by Coverage [#{coverage.size}] exceeds number of lines in #{filename} [#{src.size}]"
64
+ # Returns all lines that were skipped as SimpleCov::SourceFile::Line instances
65
+ def skipped_lines
66
+ @skipped_lines ||= lines.select(&:skipped?)
67
+ end
68
+
69
+ # Returns the number of relevant lines (covered + missed)
70
+ def lines_of_code
71
+ coverage_statistics[:line]&.total
118
72
  end
119
73
 
120
74
  # Access SimpleCov::SourceFile::Line source lines by line number
@@ -122,82 +76,225 @@ module SimpleCov
122
76
  lines[number - 1]
123
77
  end
124
78
 
125
- # The coverage for this file in percent. 0 if the file has no relevant lines
79
+ # The coverage for this file in percent. 0 if the file has no coverage lines
126
80
  def covered_percent
127
- return 100.0 if no_lines?
128
-
129
- return 0.0 if relevant_lines.zero?
130
-
131
- Float(covered_lines.size * 100.0 / relevant_lines.to_f)
81
+ coverage_statistics[:line]&.percent
132
82
  end
133
83
 
134
84
  def covered_strength
135
- return 0.0 if relevant_lines.zero?
136
-
137
- round_float(lines_strength / relevant_lines.to_f, 1)
85
+ coverage_statistics[:line]&.strength
138
86
  end
139
87
 
140
88
  def no_lines?
141
89
  lines.length.zero? || (lines.length == never_lines.size)
142
90
  end
143
91
 
144
- def lines_strength
145
- lines.map(&:coverage).compact.reduce(:+)
146
- end
147
-
148
92
  def relevant_lines
149
93
  lines.size - never_lines.size - skipped_lines.size
150
94
  end
151
95
 
152
- # Returns all covered lines as SimpleCov::SourceFile::Line
153
- def covered_lines
154
- @covered_lines ||= lines.select(&:covered?)
96
+ #
97
+ # Return all the branches inside current source file
98
+ def branches
99
+ @branches ||= build_branches
155
100
  end
156
101
 
157
- # Returns all lines that should have been, but were not covered
158
- # as instances of SimpleCov::SourceFile::Line
159
- def missed_lines
160
- @missed_lines ||= lines.select(&:missed?)
102
+ def no_branches?
103
+ total_branches.empty?
161
104
  end
162
105
 
163
- # Returns all lines that are not relevant for coverage as
164
- # SimpleCov::SourceFile::Line instances
165
- def never_lines
166
- @never_lines ||= lines.select(&:never?)
106
+ def branches_coverage_percent
107
+ coverage_statistics[:branch]&.percent
167
108
  end
168
109
 
169
- # Returns all lines that were skipped as SimpleCov::SourceFile::Line instances
170
- def skipped_lines
171
- @skipped_lines ||= lines.select(&:skipped?)
110
+ #
111
+ # Return the relevant branches to source file
112
+ def total_branches
113
+ @total_branches ||= covered_branches + missed_branches
172
114
  end
173
115
 
174
- # Returns the number of relevant lines (covered + missed)
175
- def lines_of_code
176
- covered_lines.size + missed_lines.size
116
+ #
117
+ # Return hash with key of line number and branch coverage count as value
118
+ def branches_report
119
+ @branches_report ||= build_branches_report
120
+ end
121
+
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
+ #
129
+ def covered_branches
130
+ @covered_branches ||= branches.select(&:covered?)
131
+ end
132
+
133
+ #
134
+ # Select the missed branches with coverage equal to zero
135
+ #
136
+ # @return [Array]
137
+ #
138
+ def missed_branches
139
+ @missed_branches ||= branches.select(&:missed?)
140
+ end
141
+
142
+ def branches_for_line(line_number)
143
+ branches_report.fetch(line_number, [])
144
+ end
145
+
146
+ #
147
+ # Check if any branches missing on given line number
148
+ #
149
+ # @param [Integer] line_number
150
+ #
151
+ # @return [Boolean]
152
+ #
153
+ 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, _index| LinesClassifier.no_cov_line?(line) }
166
+
167
+ warn "uneven number of nocov comments detected" if no_cov_lines.size.odd?
168
+
169
+ no_cov_lines.each_slice(2).map do |(_line_start, index_start), (_line_end, index_end)|
170
+ index_start..index_end
171
+ end
172
+ end
173
+
174
+ def build_lines
175
+ coverage_exceeding_source_warn if coverage_data["lines"].size > src.size
176
+ lines = src.map.with_index(1) do |src, i|
177
+ SimpleCov::SourceFile::Line.new(src, i, coverage_data["lines"][i - 1])
178
+ end
179
+ process_skipped_lines(lines)
177
180
  end
178
181
 
179
- # Will go through all source files and mark lines that are wrapped within # :nocov: comment blocks
180
- # as skipped.
181
182
  def process_skipped_lines(lines)
182
- skipping = false
183
-
184
- lines.each do |line|
185
- if SimpleCov::LinesClassifier.no_cov_line?(line.src)
186
- skipping = !skipping
187
- line.skipped!
188
- elsif skipping
189
- line.skipped!
190
- end
183
+ # the array the lines are kept in is 0-based whereas the line numbers in the nocov
184
+ # chunks are 1-based and are expected to be like this in other parts (and it's also
185
+ # arguably more understandable)
186
+ no_cov_chunks.each { |chunk| lines[(chunk.begin - 1)..(chunk.end - 1)].each(&:skipped!) }
187
+
188
+ lines
189
+ end
190
+
191
+ def lines_strength
192
+ lines.map(&:coverage).compact.reduce(:+)
193
+ end
194
+
195
+ # Warning to identify condition from Issue #56
196
+ def coverage_exceeding_source_warn
197
+ warn "Warning: coverage data provided by Coverage [#{coverage_data['lines'].size}] exceeds number of lines in #{filename} [#{src.size}]"
198
+ end
199
+
200
+ #
201
+ # Build full branches report
202
+ # Root branches represent the wrapper of all condition state that
203
+ # have inside the branches
204
+ #
205
+ # @return [Hash]
206
+ #
207
+ def build_branches_report
208
+ branches.reject(&:skipped?).each_with_object({}) do |branch, coverage_statistics|
209
+ coverage_statistics[branch.report_line] ||= []
210
+ coverage_statistics[branch.report_line] << branch.report
191
211
  end
192
212
  end
193
213
 
194
- private
214
+ #
215
+ # Call recursive method that transform our static hash to array of objects
216
+ # @return [Array]
217
+ #
218
+ def build_branches
219
+ coverage_branch_data = coverage_data.fetch("branches", {})
220
+ branches = coverage_branch_data.flat_map do |condition, coverage_branches|
221
+ build_branches_from(condition, coverage_branches)
222
+ end
223
+
224
+ process_skipped_branches(branches)
225
+ end
226
+
227
+ def process_skipped_branches(branches)
228
+ return branches if no_cov_chunks.empty?
229
+
230
+ branches.each do |branch|
231
+ branch.skipped! if no_cov_chunks.any? { |no_cov_chunk| branch.overlaps_with?(no_cov_chunk) }
232
+ end
233
+
234
+ branches
235
+ end
236
+
237
+ # Since we are dumping to and loading from JSON, and we have arrays as keys those
238
+ # don't make their way back to us intact e.g. just as a string
239
+ #
240
+ # We should probably do something different here, but as it stands these are
241
+ # our data structures that we write so eval isn't _too_ bad.
242
+ #
243
+ # See #801
244
+ #
245
+ def restore_ruby_data_structure(structure)
246
+ # Tests use the real data structures (except for integration tests) so no need to
247
+ # put them through here.
248
+ return structure if structure.is_a?(Array)
249
+
250
+ # rubocop:disable Security/Eval
251
+ eval structure
252
+ # rubocop:enable Security/Eval
253
+ end
254
+
255
+ def build_branches_from(condition, branches)
256
+ # the format handed in from the coverage data is like this:
257
+ #
258
+ # [:then, 4, 6, 6, 6, 10]
259
+ #
260
+ # which is [type, id, start_line, start_col, end_line, end_col]
261
+ _condition_type, _condition_id, condition_start_line, * = restore_ruby_data_structure(condition)
262
+
263
+ branches.map do |branch_data, hit_count|
264
+ branch_data = restore_ruby_data_structure(branch_data)
265
+ build_branch(branch_data, hit_count, condition_start_line)
266
+ end
267
+ end
268
+
269
+ def build_branch(branch_data, hit_count, condition_start_line)
270
+ type, _id, start_line, _start_col, end_line, _end_col = branch_data
271
+
272
+ SourceFile::Branch.new(
273
+ start_line: start_line,
274
+ end_line: end_line,
275
+ coverage: hit_count,
276
+ inline: start_line == condition_start_line,
277
+ type: type
278
+ )
279
+ end
280
+
281
+ def line_coverage_statistics
282
+ {
283
+ line: CoverageStatistics.new(
284
+ total_strength: lines_strength,
285
+ covered: covered_lines.size,
286
+ missed: missed_lines.size
287
+ )
288
+ }
289
+ end
195
290
 
196
- # ruby 1.9 could use Float#round(places) instead
197
- # @return [Float]
198
- def round_float(float, places)
199
- factor = Float(10 * places)
200
- Float((float * factor).round / factor)
291
+ def branch_coverage_statistics
292
+ {
293
+ branch: CoverageStatistics.new(
294
+ covered: covered_branches.size,
295
+ missed: missed_branches.size
296
+ )
297
+ }
201
298
  end
202
299
  end
203
300
  end