deep-cover 0.1.16 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +3 -8
- data/.travis.yml +4 -4
- data/CHANGELOG.md +10 -0
- data/Gemfile +3 -1
- data/README.md +9 -4
- data/Rakefile +6 -3
- data/deep_cover.gemspec +2 -2
- data/lib/deep_cover.rb +10 -0
- data/lib/deep_cover/analyser.rb +1 -2
- data/lib/deep_cover/analyser/base.rb +32 -0
- data/lib/deep_cover/analyser/branch.rb +19 -4
- data/lib/deep_cover/analyser/node.rb +52 -0
- data/lib/deep_cover/analyser/per_char.rb +18 -1
- data/lib/deep_cover/analyser/stats.rb +54 -0
- data/lib/deep_cover/backports.rb +1 -0
- data/lib/deep_cover/base.rb +17 -1
- data/lib/deep_cover/builtin_takeover.rb +5 -0
- data/lib/deep_cover/cli/deep_cover.rb +2 -1
- data/lib/deep_cover/cli/instrumented_clone_reporter.rb +12 -10
- data/lib/deep_cover/config.rb +22 -8
- data/lib/deep_cover/core_ext/coverage_replacement.rb +22 -18
- data/lib/deep_cover/coverage.rb +3 -203
- data/lib/deep_cover/coverage/analysis.rb +36 -0
- data/lib/deep_cover/coverage/base.rb +91 -0
- data/lib/deep_cover/coverage/istanbul.rb +34 -0
- data/lib/deep_cover/coverage/persistence.rb +93 -0
- data/lib/deep_cover/covered_code.rb +12 -22
- data/lib/deep_cover/custom_requirer.rb +6 -2
- data/lib/deep_cover/node/base.rb +1 -1
- data/lib/deep_cover/node/case.rb +13 -2
- data/lib/deep_cover/node/exceptions.rb +2 -2
- data/lib/deep_cover/node/if.rb +21 -2
- data/lib/deep_cover/node/mixin/flow_accounting.rb +1 -0
- data/lib/deep_cover/node/send.rb +9 -2
- data/lib/deep_cover/node/short_circuit.rb +10 -0
- data/lib/deep_cover/parser_ext/range.rb +4 -4
- data/lib/deep_cover/reporter/html.rb +15 -0
- data/lib/deep_cover/reporter/html/base.rb +14 -0
- data/lib/deep_cover/reporter/html/index.rb +78 -0
- data/lib/deep_cover/reporter/html/site.rb +78 -0
- data/lib/deep_cover/reporter/html/source.rb +136 -0
- data/lib/deep_cover/reporter/html/template/assets/32px.png +0 -0
- data/lib/deep_cover/reporter/html/template/assets/40px.png +0 -0
- data/lib/deep_cover/reporter/html/template/assets/deep_cover.css.sass +338 -0
- data/lib/deep_cover/reporter/html/template/assets/jquery-3.2.1.min.js +4 -0
- data/lib/deep_cover/reporter/html/template/assets/jquery-3.2.1.min.map +1 -0
- data/lib/deep_cover/reporter/html/template/assets/jstree.css +1108 -0
- data/lib/deep_cover/reporter/html/template/assets/jstree.js +8424 -0
- data/lib/deep_cover/reporter/html/template/assets/jstreetable.js +1069 -0
- data/lib/deep_cover/reporter/html/template/assets/throbber.gif +0 -0
- data/lib/deep_cover/reporter/html/template/index.html.erb +75 -0
- data/lib/deep_cover/reporter/html/template/source.html.erb +35 -0
- data/lib/deep_cover/reporter/html/tree.rb +55 -0
- data/lib/deep_cover/tools/content_tag.rb +11 -0
- data/lib/deep_cover/tools/covered.rb +9 -0
- data/lib/deep_cover/tools/merge.rb +16 -0
- data/lib/deep_cover/tools/render_template.rb +13 -0
- data/lib/deep_cover/tools/transform_keys.rb +9 -0
- data/lib/deep_cover/version.rb +1 -1
- metadata +33 -7
- data/lib/deep_cover/analyser/ignore_uncovered.rb +0 -21
- data/lib/deep_cover/analyser/optionally_covered.rb +0 -19
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeepCover
|
4
|
+
module Coverage::Istanbul
|
5
|
+
def to_istanbul(**options)
|
6
|
+
map do |covered_code|
|
7
|
+
next {} unless covered_code.has_executed?
|
8
|
+
covered_code.to_istanbul(**options)
|
9
|
+
end.inject(:merge)
|
10
|
+
end
|
11
|
+
|
12
|
+
def output_istanbul(dir: '.', name: '.nyc_output', **options)
|
13
|
+
path = Pathname.new(dir).expand_path.join(name)
|
14
|
+
path.mkpath
|
15
|
+
path.each_child(&:delete)
|
16
|
+
path.join('deep_cover.json').write(JSON.pretty_generate(to_istanbul(**options)))
|
17
|
+
path
|
18
|
+
end
|
19
|
+
|
20
|
+
def report_istanbul(output: nil, **options)
|
21
|
+
dir = output_istanbul(**options).dirname
|
22
|
+
unless [nil, '', 'false'].include? output
|
23
|
+
output = File.expand_path(output)
|
24
|
+
html = "--reporter=html --report-dir='#{output}'"
|
25
|
+
if options[:open]
|
26
|
+
html += " && open '#{output}/index.html'"
|
27
|
+
else
|
28
|
+
msg = "\nHTML coverage written to: '#{output}/index.html'"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
`cd #{dir} && nyc report --reporter=text #{html}` + msg.to_s
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeepCover
|
4
|
+
require 'securerandom'
|
5
|
+
class Coverage::Persistence
|
6
|
+
# rubocop:disable Security/MarshalLoad
|
7
|
+
BASENAME = 'coverage.dc'
|
8
|
+
TRACKER_TEMPLATE = 'trackers%{unique}.dct'
|
9
|
+
|
10
|
+
attr_reader :dir_path
|
11
|
+
def initialize(dest_path, dirname)
|
12
|
+
@dir_path = Pathname(dest_path).join(dirname).expand_path
|
13
|
+
end
|
14
|
+
|
15
|
+
def load(with_trackers: true)
|
16
|
+
saved?
|
17
|
+
load_trackers if with_trackers
|
18
|
+
load_coverage
|
19
|
+
end
|
20
|
+
|
21
|
+
def save(coverage)
|
22
|
+
create_if_needed
|
23
|
+
delete_trackers
|
24
|
+
save_coverage(coverage)
|
25
|
+
end
|
26
|
+
|
27
|
+
def save_trackers(global)
|
28
|
+
saved?
|
29
|
+
trackers = eval(global) # rubocop:disable Security/Eval
|
30
|
+
# Some testing involves more than one process, some of which don't run any of our covered code.
|
31
|
+
# Don't save anything if that's the case
|
32
|
+
return if trackers.nil?
|
33
|
+
basename = format(TRACKER_TEMPLATE, unique: SecureRandom.urlsafe_base64)
|
34
|
+
dir_path.join(basename).binwrite(Marshal.dump(
|
35
|
+
version: DeepCover::VERSION,
|
36
|
+
global: global,
|
37
|
+
trackers: trackers,
|
38
|
+
))
|
39
|
+
end
|
40
|
+
|
41
|
+
def saved?
|
42
|
+
raise "Can't find folder '#{dir_path}'" unless dir_path.exist?
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def create_if_needed
|
49
|
+
dir_path.mkpath
|
50
|
+
end
|
51
|
+
|
52
|
+
def save_coverage(coverage)
|
53
|
+
dir_path.join(BASENAME).binwrite(Marshal.dump(
|
54
|
+
version: DeepCover::VERSION,
|
55
|
+
coverage: coverage,
|
56
|
+
))
|
57
|
+
end
|
58
|
+
|
59
|
+
def load_coverage
|
60
|
+
Marshal.load(dir_path.join(BASENAME).binread).tap do |version: raise, coverage: raise|
|
61
|
+
raise "dump version mismatch: #{version}, currently #{DeepCover::VERSION}" unless version == DeepCover::VERSION
|
62
|
+
return coverage
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def load_trackers
|
67
|
+
tracker_files.each do |full_path|
|
68
|
+
Marshal.load(full_path.binread).tap do |version: raise, global: raise, trackers: raise|
|
69
|
+
raise "dump version mismatch: #{version}, currently #{DeepCover::VERSION}" unless version == DeepCover::VERSION
|
70
|
+
merge_trackers(eval("#{global} ||= {}"), trackers) # rubocop:disable Security/Eval
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def merge_trackers(hash, to_merge)
|
76
|
+
hash.merge!(to_merge) do |_key, current, to_add|
|
77
|
+
unless current.empty? || current.size == to_add.size
|
78
|
+
warn "Merging trackers of different sizes: #{current.size} vs #{to_add.size}"
|
79
|
+
end
|
80
|
+
to_add.zip(current).map { |a, b| a + b }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def tracker_files
|
85
|
+
basename = format(TRACKER_TEMPLATE, unique: '*')
|
86
|
+
Pathname.glob(dir_path.join(basename))
|
87
|
+
end
|
88
|
+
|
89
|
+
def delete_trackers
|
90
|
+
tracker_files.each(&:delete)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -12,11 +12,11 @@ module DeepCover
|
|
12
12
|
raise 'Must provide either path or source' unless path || source
|
13
13
|
|
14
14
|
@buffer = ::Parser::Source::Buffer.new(path, lineno)
|
15
|
-
@buffer.source = source
|
15
|
+
@buffer.source = source || File.read(path)
|
16
16
|
@tracker_count = 0
|
17
17
|
@tracker_global = tracker_global
|
18
18
|
@local_var = local_var
|
19
|
-
@name = name || (
|
19
|
+
@name = name.to_s || (path ? File.basename(path) : '(source)')
|
20
20
|
@covered_source = instrument_source
|
21
21
|
end
|
22
22
|
|
@@ -39,28 +39,23 @@ module DeepCover
|
|
39
39
|
|
40
40
|
def execute_code(binding: DeepCover::GLOBAL_BINDING.dup)
|
41
41
|
return if has_executed?
|
42
|
-
global[nb] = Array.new(@tracker_count, 0)
|
43
42
|
eval(@covered_source, binding, @buffer.name || '<raw_code>', lineno) # rubocop:disable Security/Eval
|
44
43
|
self
|
45
44
|
end
|
46
45
|
|
47
46
|
def cover
|
48
|
-
|
49
|
-
global[nb]
|
47
|
+
global[nb] ||= Array.new(@tracker_count, 0)
|
50
48
|
end
|
51
49
|
|
52
50
|
def line_coverage(**options)
|
53
|
-
must_have_executed
|
54
51
|
Analyser::PerLine.new(self, **options).results
|
55
52
|
end
|
56
53
|
|
57
54
|
def to_istanbul(**options)
|
58
|
-
must_have_executed
|
59
55
|
Reporter::Istanbul.new(self, **options).convert
|
60
56
|
end
|
61
57
|
|
62
58
|
def char_cover(**options)
|
63
|
-
must_have_executed
|
64
59
|
Analyser::PerChar.new(self, **options).results
|
65
60
|
end
|
66
61
|
|
@@ -103,18 +98,13 @@ module DeepCover
|
|
103
98
|
end
|
104
99
|
|
105
100
|
def instrument_source
|
106
|
-
rewriter = ::Parser::Source::
|
101
|
+
rewriter = ::Parser::Source::TreeRewriter.new(@buffer)
|
107
102
|
covered_ast.each_node(:postorder) do |node|
|
108
103
|
node.rewriting_rules.each do |range, rule|
|
109
104
|
prefix, _node, suffix = rule.partition('%{node}')
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
end
|
114
|
-
unless suffix.empty?
|
115
|
-
suffix = yield suffix, node, range.end, :suffix if block_given?
|
116
|
-
rewriter.insert_after_multi range, suffix
|
117
|
-
end
|
105
|
+
prefix = yield prefix, node, range.begin, :prefix if block_given? && !prefix.empty?
|
106
|
+
suffix = yield suffix, node, range.end, :suffix if block_given? && !suffix.empty?
|
107
|
+
rewriter.wrap(range, prefix, suffix)
|
118
108
|
end
|
119
109
|
end
|
120
110
|
rewriter.process
|
@@ -126,21 +116,21 @@ module DeepCover
|
|
126
116
|
|
127
117
|
def freeze
|
128
118
|
unless frozen? # Guard against reentrance
|
129
|
-
must_have_executed
|
130
119
|
super
|
131
120
|
root.each_node(&:freeze)
|
132
121
|
end
|
133
122
|
self
|
134
123
|
end
|
135
124
|
|
125
|
+
def inspect
|
126
|
+
%{#<DeepCover::CoveredCode "#{name}">}
|
127
|
+
end
|
128
|
+
alias_method :to_s, :inspect
|
129
|
+
|
136
130
|
protected
|
137
131
|
|
138
132
|
def global
|
139
133
|
@@globals[tracker_global]
|
140
134
|
end
|
141
|
-
|
142
|
-
def must_have_executed
|
143
|
-
raise "cover for #{buffer.name} not available, file wasn't executed" unless has_executed?
|
144
|
-
end
|
145
135
|
end
|
146
136
|
end
|
@@ -5,6 +5,8 @@
|
|
5
5
|
module DeepCover
|
6
6
|
class CustomRequirer
|
7
7
|
class LoadPathsSubset
|
8
|
+
attr_reader :last_lookup_path
|
9
|
+
|
8
10
|
def initialize(load_paths: raise, lookup_paths: raise)
|
9
11
|
@original_load_paths = load_paths
|
10
12
|
@cached_load_paths_subset = []
|
@@ -27,7 +29,7 @@ module DeepCover
|
|
27
29
|
|
28
30
|
# E.g. '/a/b' => true when a lookup path is '/a/'
|
29
31
|
def within_lookup?(full_path)
|
30
|
-
@lookup_paths.any? { |p| full_path.start_with? p }
|
32
|
+
@lookup_paths.any? { |p| full_path.start_with?(p) && @last_lookup_path = p }
|
31
33
|
end
|
32
34
|
|
33
35
|
def exist?(full_path)
|
@@ -86,6 +88,7 @@ module DeepCover
|
|
86
88
|
# It is *NOT* recommended to simply delegate to the default #require, since it
|
87
89
|
# might not be safe to run part of the code again.
|
88
90
|
def require(path)
|
91
|
+
path = path.to_s
|
89
92
|
ext = File.extname(path)
|
90
93
|
throw :use_fallback, :not_supported if ext == '.so'
|
91
94
|
path += '.rb' if ext != '.rb'
|
@@ -128,7 +131,8 @@ module DeepCover
|
|
128
131
|
|
129
132
|
def cover_and_execute(path)
|
130
133
|
begin
|
131
|
-
|
134
|
+
name = path.sub(%r{^#{@load_paths_subset.last_lookup_path}/}, '') if @load_paths_subset
|
135
|
+
covered_code = DeepCover.coverage.covered_code(path, name: name)
|
132
136
|
rescue Parser::SyntaxError => e
|
133
137
|
if e.message =~ /contains escape sequences incompatible with UTF-8/
|
134
138
|
warn "Can't cover #{path} because of incompatible encoding (see issue #9)"
|
data/lib/deep_cover/node/base.rb
CHANGED
data/lib/deep_cover/node/case.rb
CHANGED
@@ -51,10 +51,10 @@ module DeepCover
|
|
51
51
|
if (after_then = base_node.loc.begin)
|
52
52
|
after_then.end
|
53
53
|
else
|
54
|
-
base_node.loc.expression.end
|
54
|
+
base_node.loc.expression.end.succ
|
55
55
|
end
|
56
56
|
},
|
57
|
-
rewrite: '
|
57
|
+
rewrite: '%{body_entry_tracker};%{local}=nil;%{node}',
|
58
58
|
is_statement: true,
|
59
59
|
flow_entry_count: :body_entry_tracker_hits
|
60
60
|
|
@@ -89,6 +89,17 @@ module DeepCover
|
|
89
89
|
whens.map(&:body) << self.else
|
90
90
|
end
|
91
91
|
|
92
|
+
def branches_summary(of = branches)
|
93
|
+
texts = []
|
94
|
+
n = of.size
|
95
|
+
if of.include? self.else
|
96
|
+
texts << "#{'implicit ' unless has_else?}else"
|
97
|
+
n -= 1
|
98
|
+
end
|
99
|
+
texts.unshift "#{n} when clause#{'s' if n > 1}" if n > 0
|
100
|
+
texts.join(' and ')
|
101
|
+
end
|
102
|
+
|
92
103
|
def execution_count
|
93
104
|
return evaluate.flow_completion_count if evaluate
|
94
105
|
flow_entry_count
|
@@ -10,7 +10,7 @@ module DeepCover
|
|
10
10
|
has_child exception: [Node::Array, nil]
|
11
11
|
has_child assignment: [Lvasgn, nil], flow_entry_count: :entered_body_tracker_hits
|
12
12
|
has_child body: Node,
|
13
|
-
can_be_empty: -> { base_node.loc.expression.end },
|
13
|
+
can_be_empty: -> { (base_node.loc.begin || base_node.loc.expression.succ).end },
|
14
14
|
flow_entry_count: :entered_body_tracker_hits,
|
15
15
|
is_statement: true,
|
16
16
|
rewrite: '((%{entered_body_tracker};%{local}=nil;%{node}))'
|
@@ -67,7 +67,7 @@ module DeepCover
|
|
67
67
|
can_be_empty: -> { base_node.loc.expression.begin },
|
68
68
|
is_statement: true
|
69
69
|
has_child ensure: Node,
|
70
|
-
can_be_empty: -> { base_node.loc.expression.end },
|
70
|
+
can_be_empty: -> { base_node.loc.expression.end.succ },
|
71
71
|
is_statement: true,
|
72
72
|
flow_entry_count: -> { body.flow_entry_count }
|
73
73
|
|
data/lib/deep_cover/node/if.rb
CHANGED
@@ -9,21 +9,36 @@ module DeepCover
|
|
9
9
|
has_tracker :truthy
|
10
10
|
has_child condition: Node, rewrite: '((%{node}) && %{truthy_tracker})'
|
11
11
|
has_child true_branch: Node,
|
12
|
-
can_be_empty: true,
|
13
12
|
executed_loc_keys: -> { :else if style == :unless },
|
14
13
|
flow_entry_count: :truthy_tracker_hits,
|
15
14
|
is_statement: true
|
16
15
|
has_child false_branch: Node,
|
17
|
-
can_be_empty: true,
|
18
16
|
executed_loc_keys: -> { [:else, :colon] if style != :unless },
|
19
17
|
flow_entry_count: -> { condition.flow_completion_count - truthy_tracker_hits },
|
20
18
|
is_statement: true
|
21
19
|
executed_loc_keys :keyword, :question
|
22
20
|
|
21
|
+
def child_can_be_empty(child, name)
|
22
|
+
return false if name == :condition || style == :ternary
|
23
|
+
if (name == :true_branch) == (style == :if)
|
24
|
+
(base_node.loc.begin || base_node.children[0].loc.expression.succ).end
|
25
|
+
elsif has_else?
|
26
|
+
base_node.loc.else.end.succ
|
27
|
+
else
|
28
|
+
true # implicit else
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
23
32
|
def branches
|
24
33
|
[true_branch, false_branch]
|
25
34
|
end
|
26
35
|
|
36
|
+
def branches_summary(of = branches)
|
37
|
+
of.map do |jump|
|
38
|
+
"#{'implicit ' if jump.is_a?(EmptyBody) && !has_else?}#{jump == false_branch ? 'falsy' : 'truthy'} branch"
|
39
|
+
end.join(' and ')
|
40
|
+
end
|
41
|
+
|
27
42
|
def execution_count
|
28
43
|
condition.flow_completion_count
|
29
44
|
end
|
@@ -33,6 +48,10 @@ module DeepCover
|
|
33
48
|
keyword = loc_hash[:keyword]
|
34
49
|
keyword ? keyword.source.to_sym : :ternary
|
35
50
|
end
|
51
|
+
|
52
|
+
def has_else?
|
53
|
+
!!base_node.loc.to_hash[:else]
|
54
|
+
end
|
36
55
|
end
|
37
56
|
end
|
38
57
|
end
|
@@ -36,6 +36,7 @@ module DeepCover
|
|
36
36
|
end
|
37
37
|
|
38
38
|
# Returns number of times the node itself was "executed". Definition of executed depends on the node.
|
39
|
+
# For now at least, don't return `nil`, instead return `false` in `executable?`
|
39
40
|
def execution_count
|
40
41
|
flow_entry_count
|
41
42
|
end
|
data/lib/deep_cover/node/send.rb
CHANGED
@@ -58,6 +58,7 @@ module DeepCover
|
|
58
58
|
arguments.empty? || # No issue when no arguments
|
59
59
|
loc_hash[:selector_end] || # No issue with foo[bar]= and such
|
60
60
|
loc_hash[:operator] || # No issue with foo.bar=
|
61
|
+
(receiver && !loc_hash[:dot]) || # No issue with foo + bar
|
61
62
|
loc_hash[:begin] # Ok if has parentheses
|
62
63
|
end
|
63
64
|
end
|
@@ -92,10 +93,16 @@ module DeepCover
|
|
92
93
|
end
|
93
94
|
|
94
95
|
def branches
|
95
|
-
[actual_send,
|
96
|
-
|
96
|
+
[TrivialBranch.new(condition: receiver, other_branch: actual_send),
|
97
|
+
actual_send,
|
97
98
|
]
|
98
99
|
end
|
100
|
+
|
101
|
+
def branches_summary(of = branches)
|
102
|
+
of.map do |jump|
|
103
|
+
jump == actual_send ? 'safe send' : 'nil shortcut'
|
104
|
+
end.join(' and ')
|
105
|
+
end
|
99
106
|
end
|
100
107
|
|
101
108
|
class MatchWithLvasgn < Node
|
@@ -17,6 +17,16 @@ module DeepCover
|
|
17
17
|
TrivialBranch.new(condition: lhs, other_branch: conditional),
|
18
18
|
]
|
19
19
|
end
|
20
|
+
|
21
|
+
def branches_summary(of = branches)
|
22
|
+
of.map do |jump|
|
23
|
+
if jump == conditional
|
24
|
+
'left-hand side'
|
25
|
+
else
|
26
|
+
"#{type == :and ? 'falsy' : 'truthy'} shortcut"
|
27
|
+
end
|
28
|
+
end.join(' and ')
|
29
|
+
end
|
20
30
|
end
|
21
31
|
|
22
32
|
And = Or = ShortCircuit
|
@@ -1,10 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Parser::Source::Range
|
4
|
-
def with(begin_pos: @begin_pos, end_pos: @end_pos)
|
5
|
-
Parser::Source::Range.new(@source_buffer, begin_pos, end_pos)
|
6
|
-
end
|
7
|
-
|
8
4
|
# (1...10).split(2...3, 6...8) => [1...2, 3...6, 7...10]
|
9
5
|
# Assumes inner_ranges are exclusive, and included in self
|
10
6
|
def split(*inner_ranges)
|
@@ -34,4 +30,8 @@ class Parser::Source::Range
|
|
34
30
|
def strip(pattern = /\s*/)
|
35
31
|
lstrip(pattern).rstrip(pattern)
|
36
32
|
end
|
33
|
+
|
34
|
+
def succ
|
35
|
+
adjust(begin_pos: +1, end_pos: +1)
|
36
|
+
end
|
37
37
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeepCover
|
4
|
+
Reporter::HTML = Module.new
|
5
|
+
|
6
|
+
require_relative_dir 'html'
|
7
|
+
|
8
|
+
module Reporter::HTML
|
9
|
+
class << self
|
10
|
+
def report(coverage, **options)
|
11
|
+
Site.save(coverage.covered_codes, **options)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|