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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +67 -1
- data/CODE_OF_CONDUCT.md +76 -0
- data/README.md +265 -76
- data/doc/alternate-formatters.md +5 -0
- data/lib/simplecov.rb +225 -61
- data/lib/simplecov/combine.rb +30 -0
- data/lib/simplecov/combine/branches_combiner.rb +32 -0
- data/lib/simplecov/combine/files_combiner.rb +24 -0
- data/lib/simplecov/combine/lines_combiner.rb +43 -0
- data/lib/simplecov/combine/results_combiner.rb +60 -0
- data/lib/simplecov/command_guesser.rb +6 -3
- data/lib/simplecov/configuration.rb +110 -9
- data/lib/simplecov/coverage_statistics.rb +56 -0
- data/lib/simplecov/defaults.rb +4 -2
- data/lib/simplecov/file_list.rb +66 -13
- data/lib/simplecov/filter.rb +2 -1
- data/lib/simplecov/formatter/multi_formatter.rb +2 -2
- data/lib/simplecov/formatter/simple_formatter.rb +4 -4
- data/lib/simplecov/last_run.rb +3 -1
- data/lib/simplecov/lines_classifier.rb +2 -2
- data/lib/simplecov/profiles.rb +9 -7
- data/lib/simplecov/result.rb +39 -6
- data/lib/simplecov/result_adapter.rb +30 -0
- data/lib/simplecov/result_merger.rb +18 -11
- data/lib/simplecov/simulate_coverage.rb +29 -0
- data/lib/simplecov/source_file.rb +222 -125
- data/lib/simplecov/source_file/branch.rb +84 -0
- data/lib/simplecov/source_file/line.rb +72 -0
- data/lib/simplecov/useless_results_remover.rb +16 -0
- data/lib/simplecov/version.rb +1 -1
- metadata +32 -166
- data/lib/simplecov/jruby_fix.rb +0 -44
- data/lib/simplecov/railtie.rb +0 -9
- data/lib/simplecov/railties/tasks.rake +0 -13
- data/lib/simplecov/raw_coverage.rb +0 -41
data/lib/simplecov/filter.rb
CHANGED
@@ -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
|
-
|
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 = ""
|
11
|
+
output = +""
|
12
12
|
result.groups.each do |name, files|
|
13
13
|
output << "Group: #{name}\n"
|
14
14
|
output << "=" * 40
|
data/lib/simplecov/last_run.rb
CHANGED
@@ -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
|
data/lib/simplecov/profiles.rb
CHANGED
@@ -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
|
data/lib/simplecov/result.rb
CHANGED
@@ -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
|
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
|
-
|
30
|
-
@
|
31
|
-
|
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
|
-
{
|
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
|
-
|
70
|
-
|
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 :
|
12
|
+
attr_reader :coverage_data
|
79
13
|
|
80
|
-
def initialize(filename,
|
14
|
+
def initialize(filename, coverage_data)
|
81
15
|
@filename = filename
|
82
|
-
@
|
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
|
-
|
106
|
-
|
47
|
+
# Returns all covered lines as SimpleCov::SourceFile::Line
|
48
|
+
def covered_lines
|
49
|
+
@covered_lines ||= lines.select(&:covered?)
|
50
|
+
end
|
107
51
|
|
108
|
-
|
109
|
-
|
110
|
-
|
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
|
-
|
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
|
-
#
|
116
|
-
def
|
117
|
-
|
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
|
79
|
+
# The coverage for this file in percent. 0 if the file has no coverage lines
|
126
80
|
def covered_percent
|
127
|
-
|
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
|
-
|
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
|
-
#
|
153
|
-
|
154
|
-
|
96
|
+
#
|
97
|
+
# Return all the branches inside current source file
|
98
|
+
def branches
|
99
|
+
@branches ||= build_branches
|
155
100
|
end
|
156
101
|
|
157
|
-
|
158
|
-
|
159
|
-
def missed_lines
|
160
|
-
@missed_lines ||= lines.select(&:missed?)
|
102
|
+
def no_branches?
|
103
|
+
total_branches.empty?
|
161
104
|
end
|
162
105
|
|
163
|
-
|
164
|
-
|
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
|
-
#
|
170
|
-
|
171
|
-
|
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
|
-
#
|
175
|
-
|
176
|
-
|
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
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
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
|
-
|
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
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
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
|