deep-cover-core 0.6.3.pre
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 +7 -0
- data/.rspec +4 -0
- data/.rspec_all +3 -0
- data/.rubocop.yml +1 -0
- data/Gemfile +11 -0
- data/deep_cover_core.gemspec +46 -0
- data/lib/deep-cover.rb +3 -0
- data/lib/deep_cover/analyser/base.rb +104 -0
- data/lib/deep_cover/analyser/branch.rb +41 -0
- data/lib/deep_cover/analyser/covered_code_source.rb +21 -0
- data/lib/deep_cover/analyser/function.rb +14 -0
- data/lib/deep_cover/analyser/node.rb +54 -0
- data/lib/deep_cover/analyser/per_char.rb +38 -0
- data/lib/deep_cover/analyser/per_line.rb +41 -0
- data/lib/deep_cover/analyser/ruby25_like_branch.rb +211 -0
- data/lib/deep_cover/analyser/statement.rb +33 -0
- data/lib/deep_cover/analyser/stats.rb +54 -0
- data/lib/deep_cover/analyser/subset.rb +27 -0
- data/lib/deep_cover/analyser.rb +23 -0
- data/lib/deep_cover/auto_run.rb +71 -0
- data/lib/deep_cover/autoload_tracker.rb +215 -0
- data/lib/deep_cover/backports.rb +22 -0
- data/lib/deep_cover/base.rb +117 -0
- data/lib/deep_cover/basics.rb +22 -0
- data/lib/deep_cover/builtin_takeover.rb +10 -0
- data/lib/deep_cover/config.rb +104 -0
- data/lib/deep_cover/config_setter.rb +33 -0
- data/lib/deep_cover/core_ext/autoload_overrides.rb +112 -0
- data/lib/deep_cover/core_ext/coverage_replacement.rb +61 -0
- data/lib/deep_cover/core_ext/exec_callbacks.rb +27 -0
- data/lib/deep_cover/core_ext/instruction_sequence_load_iseq.rb +32 -0
- data/lib/deep_cover/core_ext/load_overrides.rb +19 -0
- data/lib/deep_cover/core_ext/require_overrides.rb +28 -0
- data/lib/deep_cover/coverage/analysis.rb +42 -0
- data/lib/deep_cover/coverage/persistence.rb +84 -0
- data/lib/deep_cover/coverage.rb +125 -0
- data/lib/deep_cover/covered_code.rb +145 -0
- data/lib/deep_cover/custom_requirer.rb +187 -0
- data/lib/deep_cover/flag_comment_associator.rb +68 -0
- data/lib/deep_cover/load.rb +66 -0
- data/lib/deep_cover/memoize.rb +48 -0
- data/lib/deep_cover/module_override.rb +39 -0
- data/lib/deep_cover/node/arguments.rb +51 -0
- data/lib/deep_cover/node/assignments.rb +273 -0
- data/lib/deep_cover/node/base.rb +155 -0
- data/lib/deep_cover/node/begin.rb +27 -0
- data/lib/deep_cover/node/block.rb +61 -0
- data/lib/deep_cover/node/branch.rb +32 -0
- data/lib/deep_cover/node/case.rb +113 -0
- data/lib/deep_cover/node/collections.rb +31 -0
- data/lib/deep_cover/node/const.rb +12 -0
- data/lib/deep_cover/node/def.rb +40 -0
- data/lib/deep_cover/node/empty_body.rb +32 -0
- data/lib/deep_cover/node/exceptions.rb +79 -0
- data/lib/deep_cover/node/if.rb +73 -0
- data/lib/deep_cover/node/keywords.rb +86 -0
- data/lib/deep_cover/node/literals.rb +100 -0
- data/lib/deep_cover/node/loops.rb +74 -0
- data/lib/deep_cover/node/mixin/can_augment_children.rb +65 -0
- data/lib/deep_cover/node/mixin/check_completion.rb +18 -0
- data/lib/deep_cover/node/mixin/child_can_be_empty.rb +27 -0
- data/lib/deep_cover/node/mixin/executed_after_children.rb +15 -0
- data/lib/deep_cover/node/mixin/execution_location.rb +66 -0
- data/lib/deep_cover/node/mixin/filters.rb +47 -0
- data/lib/deep_cover/node/mixin/flow_accounting.rb +71 -0
- data/lib/deep_cover/node/mixin/has_child.rb +145 -0
- data/lib/deep_cover/node/mixin/has_child_handler.rb +75 -0
- data/lib/deep_cover/node/mixin/has_tracker.rb +46 -0
- data/lib/deep_cover/node/mixin/is_statement.rb +20 -0
- data/lib/deep_cover/node/mixin/rewriting.rb +35 -0
- data/lib/deep_cover/node/mixin/wrapper.rb +15 -0
- data/lib/deep_cover/node/module.rb +66 -0
- data/lib/deep_cover/node/root.rb +20 -0
- data/lib/deep_cover/node/send.rb +161 -0
- data/lib/deep_cover/node/short_circuit.rb +42 -0
- data/lib/deep_cover/node/splat.rb +15 -0
- data/lib/deep_cover/node/variables.rb +16 -0
- data/lib/deep_cover/node.rb +23 -0
- data/lib/deep_cover/parser_ext/range.rb +21 -0
- data/lib/deep_cover/problem_with_diagnostic.rb +63 -0
- data/lib/deep_cover/reporter/base.rb +68 -0
- data/lib/deep_cover/reporter/html/base.rb +14 -0
- data/lib/deep_cover/reporter/html/index.rb +59 -0
- data/lib/deep_cover/reporter/html/site.rb +68 -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 +291 -0
- data/lib/deep_cover/reporter/html/template/assets/deep_cover.css.sass +336 -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.rb +15 -0
- data/lib/deep_cover/reporter/istanbul.rb +184 -0
- data/lib/deep_cover/reporter/text.rb +58 -0
- data/lib/deep_cover/reporter/tree/util.rb +86 -0
- data/lib/deep_cover/reporter.rb +10 -0
- data/lib/deep_cover/tools/blank.rb +25 -0
- data/lib/deep_cover/tools/builtin_coverage.rb +55 -0
- data/lib/deep_cover/tools/camelize.rb +13 -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/execute_sample.rb +34 -0
- data/lib/deep_cover/tools/format.rb +18 -0
- data/lib/deep_cover/tools/format_char_cover.rb +19 -0
- data/lib/deep_cover/tools/format_generated_code.rb +27 -0
- data/lib/deep_cover/tools/indent_string.rb +26 -0
- data/lib/deep_cover/tools/merge.rb +16 -0
- data/lib/deep_cover/tools/number_lines.rb +22 -0
- data/lib/deep_cover/tools/our_coverage.rb +11 -0
- data/lib/deep_cover/tools/profiling.rb +68 -0
- data/lib/deep_cover/tools/render_template.rb +13 -0
- data/lib/deep_cover/tools/require_relative_dir.rb +12 -0
- data/lib/deep_cover/tools/scan_match_datas.rb +10 -0
- data/lib/deep_cover/tools/silence_warnings.rb +18 -0
- data/lib/deep_cover/tools/slice.rb +9 -0
- data/lib/deep_cover/tools/strip_heredoc.rb +18 -0
- data/lib/deep_cover/tools/truncate_backtrace.rb +32 -0
- data/lib/deep_cover/tools.rb +22 -0
- data/lib/deep_cover/tracker_bucket.rb +50 -0
- data/lib/deep_cover/tracker_hits_per_path.rb +35 -0
- data/lib/deep_cover/tracker_storage.rb +76 -0
- data/lib/deep_cover/tracker_storage_per_path.rb +34 -0
- data/lib/deep_cover/version.rb +5 -0
- data/lib/deep_cover.rb +22 -0
- metadata +329 -0
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'literals'
|
4
|
+
require_relative 'branch'
|
5
|
+
|
6
|
+
module DeepCover
|
7
|
+
class Node
|
8
|
+
class SendBase < Node
|
9
|
+
has_child receiver: [Node, nil]
|
10
|
+
has_child message: Symbol
|
11
|
+
has_extra_children arguments: Node
|
12
|
+
executed_loc_keys :dot, :selector_begin, :selector_end, :operator
|
13
|
+
|
14
|
+
def loc_hash
|
15
|
+
hash = super.dup
|
16
|
+
selector = hash.delete(:selector)
|
17
|
+
|
18
|
+
# Special case for foo[bar]=baz, but not for foo.[]= bar, baz: we split selector into begin and end
|
19
|
+
if base_node.location.dot == nil && [:[], :[]=].include?(message)
|
20
|
+
hash[:selector_begin] = selector.resize(1)
|
21
|
+
hash[:selector_end] = Parser::Source::Range.new(selector.source_buffer, selector.end_pos - 1, selector.end_pos)
|
22
|
+
else
|
23
|
+
hash.delete(:dot) if type == :safe_send # Hack. API to get a Parser::AST::Send::Map without the dot is crappy.
|
24
|
+
hash[:selector_begin] = selector
|
25
|
+
end
|
26
|
+
|
27
|
+
hash
|
28
|
+
end
|
29
|
+
|
30
|
+
# Rules must be ordered inner-most first
|
31
|
+
def rewriting_rules
|
32
|
+
rules = super
|
33
|
+
if need_parentheses?
|
34
|
+
range = arguments.last.expression.with(begin_pos: loc_hash[:selector_begin].end_pos)
|
35
|
+
rules.unshift [range, '(%{node})']
|
36
|
+
end
|
37
|
+
rules
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# In different circumstances, we need ().
|
43
|
+
# Deal with ambiguous cases where a method is hidden by a local. Ex:
|
44
|
+
# foo 42, 'hello' #=> Works
|
45
|
+
# foo (42), 'hello' #=> Simplification of what DeepCover would generate, still works
|
46
|
+
# foo = 1; foo 42, 'hello' #=> works
|
47
|
+
# foo = 1; foo (42), 'hello' #=> syntax error.
|
48
|
+
# foo = 1; foo((42), 'hello') #=> works
|
49
|
+
# Deal with do/end block. Ex:
|
50
|
+
# x.foo 42, 43 # => ok
|
51
|
+
# x.foo (42), 43 # => ok
|
52
|
+
# x.foo ((42)), 43 # => ok
|
53
|
+
# x.foo 42, 43 do ; end # => ok
|
54
|
+
# x.foo (42), 43 do ; end # => ok
|
55
|
+
# x.foo ((42)), 43 do ; end # => parse error!
|
56
|
+
def need_parentheses?
|
57
|
+
true unless
|
58
|
+
arguments.empty? || # No issue when no arguments
|
59
|
+
loc_hash[:selector_end] || # No issue with foo[bar]= and such
|
60
|
+
loc_hash[:operator] || # No issue with foo.bar=
|
61
|
+
(receiver && !loc_hash[:dot]) || # No issue with foo + bar
|
62
|
+
loc_hash[:begin] # Ok if has parentheses
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class Send < SendBase
|
67
|
+
check_completion
|
68
|
+
end
|
69
|
+
|
70
|
+
class CsendInnerSend < SendBase
|
71
|
+
has_tracker :completion
|
72
|
+
include ExecutedAfterChildren
|
73
|
+
|
74
|
+
def has_block?
|
75
|
+
parent.has_block?
|
76
|
+
end
|
77
|
+
|
78
|
+
def rewrite
|
79
|
+
# All the rest of the rewriting logic is in Csend
|
80
|
+
'%{node};%{completion_tracker};' unless has_block?
|
81
|
+
end
|
82
|
+
|
83
|
+
def flow_completion_count
|
84
|
+
return parent.parent.flow_completion_count if has_block?
|
85
|
+
completion_tracker_hits
|
86
|
+
end
|
87
|
+
|
88
|
+
def loc_hash
|
89
|
+
# This is only a partial Send, the receiver param and the dot are actually handled by the parent Csend.
|
90
|
+
h = super.dup
|
91
|
+
h[:expression] = h[:expression].with(begin_pos: h[:selector_begin].begin_pos)
|
92
|
+
h
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
class Csend < Node
|
97
|
+
# The overall rewriting goal is this:
|
98
|
+
# temp = *receiver*;
|
99
|
+
# if nil != temp
|
100
|
+
# TRACK_my_NOT_NIL
|
101
|
+
# temp = temp&.*actual_send*{block}
|
102
|
+
# TRACK_actual_send_COMPLETION
|
103
|
+
# t
|
104
|
+
# else
|
105
|
+
# nil
|
106
|
+
# end
|
107
|
+
# This is split across the children and the CsendInnerSend
|
108
|
+
include Branch
|
109
|
+
has_tracker :not_nil
|
110
|
+
has_child receiver: Node,
|
111
|
+
rewrite: '(%{local}=%{node};if nil != %{local};%{not_nil_tracker};%{local}=%{local}'
|
112
|
+
REWRITE_SUFFIX = '%{node};%{local};else;nil;end)'
|
113
|
+
|
114
|
+
has_child actual_send: {safe_send: CsendInnerSend},
|
115
|
+
flow_entry_count: :not_nil_tracker_hits
|
116
|
+
|
117
|
+
def initialize(base_node, base_children: base_node.children, **) # rubocop:disable Naming/UncommunicativeMethodParamName [#5436]
|
118
|
+
send_without_receiver = base_node.updated(:safe_send, [nil, *base_node.children.drop(1)])
|
119
|
+
base_children = [base_children.first, send_without_receiver]
|
120
|
+
super
|
121
|
+
end
|
122
|
+
|
123
|
+
executed_loc_keys :dot
|
124
|
+
|
125
|
+
def has_block?
|
126
|
+
parent.is_a?(Block) && parent.child_index_to_name(index) == :call
|
127
|
+
end
|
128
|
+
|
129
|
+
def rewrite
|
130
|
+
REWRITE_SUFFIX unless has_block?
|
131
|
+
end
|
132
|
+
|
133
|
+
def execution_count
|
134
|
+
receiver.flow_completion_count
|
135
|
+
end
|
136
|
+
|
137
|
+
def message
|
138
|
+
actual_send.message
|
139
|
+
end
|
140
|
+
|
141
|
+
def branches
|
142
|
+
[TrivialBranch.new(condition: receiver, other_branch: actual_send),
|
143
|
+
actual_send,
|
144
|
+
]
|
145
|
+
end
|
146
|
+
|
147
|
+
def branches_summary(of_branches = branches)
|
148
|
+
of_branches.map do |jump|
|
149
|
+
jump == actual_send ? 'safe send' : 'nil shortcut'
|
150
|
+
end.join(' and ')
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
class MatchWithLvasgn < Node
|
155
|
+
check_completion
|
156
|
+
has_child receiver: Regexp
|
157
|
+
has_child compare_to: Node
|
158
|
+
# TODO: test
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'branch'
|
4
|
+
|
5
|
+
module DeepCover
|
6
|
+
class Node
|
7
|
+
class ShortCircuit < Node
|
8
|
+
include Branch
|
9
|
+
has_tracker :conditional
|
10
|
+
has_child lhs: Node
|
11
|
+
has_child conditional: Node, flow_entry_count: :conditional_tracker_hits,
|
12
|
+
rewrite: '(%{conditional_tracker};%{node})'
|
13
|
+
|
14
|
+
def branches
|
15
|
+
[
|
16
|
+
conditional,
|
17
|
+
TrivialBranch.new(condition: lhs, other_branch: conditional),
|
18
|
+
]
|
19
|
+
end
|
20
|
+
|
21
|
+
def branches_summary(of_branches = branches)
|
22
|
+
of_branches.map do |jump|
|
23
|
+
if jump == conditional
|
24
|
+
'right-hand side'
|
25
|
+
else
|
26
|
+
"#{type == :and ? 'falsy' : 'truthy'} shortcut"
|
27
|
+
end
|
28
|
+
end.join(' and ')
|
29
|
+
end
|
30
|
+
|
31
|
+
def operator
|
32
|
+
loc_hash[:operator].source.to_sym
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class And < ShortCircuit
|
37
|
+
end
|
38
|
+
|
39
|
+
class Or < ShortCircuit
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeepCover
|
4
|
+
class Node
|
5
|
+
class Splat < Node
|
6
|
+
check_completion inner: '[%{node}]', outer: '*%{node}'
|
7
|
+
has_child receiver: Node
|
8
|
+
end
|
9
|
+
|
10
|
+
class Kwsplat < Node
|
11
|
+
check_completion inner: '{%{node}}', outer: '**%{node}'
|
12
|
+
has_child receiver: Node
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeepCover
|
4
|
+
class Node
|
5
|
+
class Variable < Node
|
6
|
+
has_child var_name: Symbol
|
7
|
+
end
|
8
|
+
Ivar = Lvar = Cvar = Gvar = BackRef = Variable
|
9
|
+
|
10
|
+
# $1
|
11
|
+
class NthRef < Node
|
12
|
+
has_child n: Integer
|
13
|
+
# TODO
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeepCover
|
4
|
+
bootstrap
|
5
|
+
load_parser
|
6
|
+
|
7
|
+
class Node
|
8
|
+
# Reopened in base
|
9
|
+
CLASSES = []
|
10
|
+
def self.inherited(parent)
|
11
|
+
CLASSES << parent
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
require_relative_dir 'node/mixin'
|
16
|
+
require_relative 'node/base'
|
17
|
+
require_relative_dir 'node'
|
18
|
+
|
19
|
+
Node.include Memoize
|
20
|
+
Node::CLASSES.freeze.each do |klass|
|
21
|
+
klass.memoize :flow_entry_count, :flow_completion_count, :execution_count, :loc_hash
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Parser::Source::Range
|
4
|
+
def succ
|
5
|
+
adjust(begin_pos: +1, end_pos: +1)
|
6
|
+
end
|
7
|
+
|
8
|
+
def wrap_rwhitespace(whitespaces: /\A\s+/)
|
9
|
+
whitespace = @source_buffer.slice(end_pos..-1)[whitespaces] || ''
|
10
|
+
adjust(end_pos: whitespace.size)
|
11
|
+
end
|
12
|
+
|
13
|
+
def wrap_rwhitespace_and_comments(whitespaces: /\A\s+/)
|
14
|
+
current = wrap_rwhitespace(whitespaces: whitespaces)
|
15
|
+
while @source_buffer.slice(current.end_pos) == '#'
|
16
|
+
comment = @source_buffer.slice(current.end_pos..-1)[/\A[^\n]+/] || ''
|
17
|
+
current = current.adjust(end_pos: comment.size).wrap_rwhitespace(whitespaces: whitespaces)
|
18
|
+
end
|
19
|
+
current
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeepCover
|
4
|
+
class ProblemWithDiagnostic < StandardError
|
5
|
+
attr_reader :covered_code, :line_range, :original_exception
|
6
|
+
|
7
|
+
def initialize(covered_code, line_range, original_exception = nil)
|
8
|
+
@covered_code = covered_code
|
9
|
+
if line_range.respond_to? :last_line
|
10
|
+
@line_range = line_range.line..line_range.last_line
|
11
|
+
else
|
12
|
+
@line_range = line_range
|
13
|
+
end
|
14
|
+
@original_exception = original_exception
|
15
|
+
end
|
16
|
+
|
17
|
+
def message
|
18
|
+
msg = []
|
19
|
+
msg << 'You found a problem with DeepCover!'
|
20
|
+
msg << 'Please open an issue at https://github.com/deep-cover/deep-cover/issues'
|
21
|
+
msg << 'and include the following diagnostic information:'
|
22
|
+
extra = begin
|
23
|
+
diagnostic_information_lines.map { |line| "| #{line}" }
|
24
|
+
rescue ProblemWithDiagnostic
|
25
|
+
["Oh no! We're in deep trouble!!!"]
|
26
|
+
rescue Exception => e
|
27
|
+
["Oh no! Even diagnostics are failing: #{e}\n#{e.backtrace}"]
|
28
|
+
end
|
29
|
+
msg.concat(extra)
|
30
|
+
msg.join("\n")
|
31
|
+
end
|
32
|
+
|
33
|
+
def diagnostic_information_lines
|
34
|
+
lines = []
|
35
|
+
lines << "Source file: #{covered_code.path}"
|
36
|
+
lines << "Line numbers: #{line_range}"
|
37
|
+
lines << 'Source lines around location:'
|
38
|
+
lines.concat(source_lines.map { |line| " #{line}" })
|
39
|
+
if original_exception
|
40
|
+
lines << 'Original exception:'
|
41
|
+
lines << " #{original_exception.class}: #{original_exception.message}"
|
42
|
+
backtrace = Tools.truncate_backtrace(original_exception)
|
43
|
+
lines.concat(backtrace.map { |line| " #{line}" })
|
44
|
+
end
|
45
|
+
lines
|
46
|
+
end
|
47
|
+
|
48
|
+
def source_lines(nb_context_line: 7)
|
49
|
+
first_index = line_range.begin - nb_context_line - buffer.first_line
|
50
|
+
first_index = 0 if first_index < 0
|
51
|
+
last_index = line_range.end + nb_context_line - buffer.first_line
|
52
|
+
last_index = 0 if last_index < 0
|
53
|
+
|
54
|
+
lines = buffer.source_lines[first_index..last_index]
|
55
|
+
|
56
|
+
Tools.number_lines(lines, lineno: buffer.first_line, bad_linenos: line_range.to_a)
|
57
|
+
end
|
58
|
+
|
59
|
+
def buffer
|
60
|
+
covered_code.buffer
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeepCover
|
4
|
+
module Reporter
|
5
|
+
require_relative 'tree/util'
|
6
|
+
|
7
|
+
class Base
|
8
|
+
include Memoize
|
9
|
+
memoize :map, :tree, :root_path
|
10
|
+
|
11
|
+
attr_reader :options
|
12
|
+
|
13
|
+
def initialize(coverage, **options)
|
14
|
+
@coverage = coverage
|
15
|
+
@options = options
|
16
|
+
end
|
17
|
+
|
18
|
+
def analysis
|
19
|
+
@analysis ||= Coverage::Analysis.new(@coverage.covered_codes, **options)
|
20
|
+
end
|
21
|
+
|
22
|
+
def each(&block)
|
23
|
+
return to_enum :each unless block_given?
|
24
|
+
@coverage.each do |covered_code|
|
25
|
+
yield relative_path(covered_code.path), covered_code
|
26
|
+
end
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
# Same as populate, but also yields data, which is either the analysis data (for leaves)
|
31
|
+
# of the sum of the children (for subtrees)
|
32
|
+
def populate_stats
|
33
|
+
return to_enum(__method__) unless block_given?
|
34
|
+
Tree::Util.populate_from_map(
|
35
|
+
tree: tree,
|
36
|
+
map: map,
|
37
|
+
merge: ->(child_data) { Tools.merge(*child_data, :+) }
|
38
|
+
) do |full_path, partial_path, data, children|
|
39
|
+
yield relative_path(full_path), relative_path(partial_path), data, children
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def relative_path(path)
|
46
|
+
path = path.to_s
|
47
|
+
path = path.slice(root_path.length + 1..-1) if path.start_with?(root_path)
|
48
|
+
path
|
49
|
+
end
|
50
|
+
|
51
|
+
def root_path
|
52
|
+
return '' if tree.size > 1
|
53
|
+
path = tree.first.first
|
54
|
+
root = File.dirname(path)
|
55
|
+
root = File.dirname(root) if File.basename(path) == 'dir'
|
56
|
+
root
|
57
|
+
end
|
58
|
+
|
59
|
+
def map
|
60
|
+
analysis.stat_map.transform_keys(&:path).transform_keys(&:to_s)
|
61
|
+
end
|
62
|
+
|
63
|
+
def tree
|
64
|
+
Tree::Util.paths_to_tree(map.keys)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeepCover
|
4
|
+
module Reporter::HTML::Base
|
5
|
+
include Tools::ContentTag
|
6
|
+
def setup
|
7
|
+
DeepCover::DEFAULTS.keys.map do |setting|
|
8
|
+
value = options[setting]
|
9
|
+
value = value.join(', ') if value.respond_to? :join
|
10
|
+
content_tag :span, value, class: setting
|
11
|
+
end.join
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeepCover
|
4
|
+
require_relative 'base'
|
5
|
+
|
6
|
+
module Reporter
|
7
|
+
class HTML::Index < Struct.new(:base)
|
8
|
+
include HTML::Base
|
9
|
+
extend Forwardable
|
10
|
+
def_delegators :base, :analysis, :options, :populate_stats
|
11
|
+
|
12
|
+
def stats_to_data
|
13
|
+
populate_stats do |full_path, partial_path, data, children|
|
14
|
+
data = transform_data(data)
|
15
|
+
if children.empty?
|
16
|
+
{
|
17
|
+
text: %{<a href="#{full_path}.html">#{partial_path}</a>},
|
18
|
+
data: data,
|
19
|
+
}
|
20
|
+
else
|
21
|
+
{
|
22
|
+
text: partial_path,
|
23
|
+
data: data,
|
24
|
+
children: children,
|
25
|
+
state: {opened: true},
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def columns
|
32
|
+
_covered_code, analyser_map = analysis.analyser_map.first
|
33
|
+
analyser_map ||= []
|
34
|
+
columns = analyser_map.flat_map do |type, analyser|
|
35
|
+
[{
|
36
|
+
value: type,
|
37
|
+
header: analyser.class.human_name,
|
38
|
+
}, {
|
39
|
+
value: :"#{type}_percent",
|
40
|
+
header: '%',
|
41
|
+
},
|
42
|
+
]
|
43
|
+
end
|
44
|
+
columns.unshift(width: 400, header: 'Path')
|
45
|
+
columns
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# {per_char: Stat, ...} => {per_char: {ignored: ...}, per_char_percent: 55.55, ...}
|
51
|
+
def transform_data(data)
|
52
|
+
Tools.merge(
|
53
|
+
data.transform_values(&:to_h),
|
54
|
+
*data.map { |type, stat| {:"#{type}_percent" => stat.percent_covered} }
|
55
|
+
)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeepCover
|
4
|
+
require_relative 'base'
|
5
|
+
require_relative 'index'
|
6
|
+
require_relative 'source'
|
7
|
+
|
8
|
+
module Reporter::HTML
|
9
|
+
class Site < Reporter::Base
|
10
|
+
include Memoize
|
11
|
+
memoize :analysis
|
12
|
+
|
13
|
+
def path
|
14
|
+
Pathname(options[:output])
|
15
|
+
end
|
16
|
+
|
17
|
+
def save
|
18
|
+
clear
|
19
|
+
save_assets
|
20
|
+
save_index
|
21
|
+
save_pages
|
22
|
+
end
|
23
|
+
|
24
|
+
def clear
|
25
|
+
path.mkpath
|
26
|
+
path.rmtree
|
27
|
+
path.mkpath
|
28
|
+
end
|
29
|
+
|
30
|
+
def compile_stylesheet(source, dest)
|
31
|
+
css = Sass::Engine.for_file(source, style: :expanded).to_css
|
32
|
+
File.write(dest, css)
|
33
|
+
end
|
34
|
+
|
35
|
+
def render_index
|
36
|
+
Tools.render_template(:index, Index.new(self))
|
37
|
+
end
|
38
|
+
|
39
|
+
def save_index
|
40
|
+
path.join('index.html').write(render_index)
|
41
|
+
end
|
42
|
+
|
43
|
+
def save_assets
|
44
|
+
require 'fileutils'
|
45
|
+
src = "#{__dir__}/template/assets"
|
46
|
+
dest = path.join('assets')
|
47
|
+
FileUtils.cp_r(src, dest)
|
48
|
+
dest.join('deep_cover.css.sass').delete
|
49
|
+
end
|
50
|
+
|
51
|
+
def render_source(partial_path, covered_code)
|
52
|
+
Tools.render_template(:source, Source.new(analysis.analyser_map.fetch(covered_code), partial_path))
|
53
|
+
end
|
54
|
+
|
55
|
+
def save_pages
|
56
|
+
each do |partial_path, covered_code|
|
57
|
+
dest = path.join("#{partial_path}.html")
|
58
|
+
dest.dirname.mkpath
|
59
|
+
dest.write(render_source(partial_path, covered_code))
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.save(coverage, output:, **options)
|
64
|
+
Site.new(coverage, output: output, **options).save
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeepCover
|
4
|
+
module Reporter
|
5
|
+
require_relative 'base'
|
6
|
+
|
7
|
+
class HTML::Source < Struct.new(:analyser_map, :partial_path)
|
8
|
+
include Tools::Covered
|
9
|
+
|
10
|
+
def initialize(analyser_map, partial_path)
|
11
|
+
raise ArgumentError unless analyser_map.values.all? { |a| a.is_a?(Analyser) }
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
include HTML::Base
|
16
|
+
|
17
|
+
def format_source
|
18
|
+
lines = convert_source.split("\n")
|
19
|
+
lines.map { |line| content_tag(:td, line) }
|
20
|
+
rows = lines.map.with_index do |line, i|
|
21
|
+
nb = content_tag(:td, i + 1, id: "L#{i + 1}", class: :nb)
|
22
|
+
content_tag(:tr, nb + content_tag(:td, line))
|
23
|
+
end
|
24
|
+
content_tag(:table, rows.join, class: :source)
|
25
|
+
end
|
26
|
+
|
27
|
+
def convert_source
|
28
|
+
@rewriter = Parser::Source::TreeRewriter.new(covered_code.buffer)
|
29
|
+
insert_node_tags
|
30
|
+
insert_branch_tags
|
31
|
+
html_escape
|
32
|
+
@rewriter.process
|
33
|
+
end
|
34
|
+
|
35
|
+
def root_path
|
36
|
+
Pathname('.').relative_path_from(Pathname(partial_path).dirname)
|
37
|
+
end
|
38
|
+
|
39
|
+
def stats
|
40
|
+
cells = analyser_map.map do |type, analyser|
|
41
|
+
data = analyser.stats
|
42
|
+
f = ->(kind) { content_tag(:span, data.public_send(kind), class: kind, title: kind) }
|
43
|
+
[content_tag(:th, analyser.class.human_name, class: type),
|
44
|
+
content_tag(:td, "#{f[:executed]} #{f[:ignored] if data.ignored > 0} / #{f[:potentially_executable]}", class: type),
|
45
|
+
]
|
46
|
+
end
|
47
|
+
rows = cells.transpose.map { |line| content_tag(:tr, line.join) }
|
48
|
+
content_tag(:table, rows.join)
|
49
|
+
end
|
50
|
+
|
51
|
+
def analyser
|
52
|
+
analyser_map[:per_char]
|
53
|
+
end
|
54
|
+
|
55
|
+
def covered_code
|
56
|
+
analyser.covered_code
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
RUNS_CLASS = Hash.new('run').merge!(0 => 'not-run', nil => 'ignored')
|
62
|
+
RUNS_TITLE = Hash.new { |k, runs| "#{runs}x" }.merge!(0 => 'never run', nil => 'ignored')
|
63
|
+
|
64
|
+
def node_span(node, kind)
|
65
|
+
runs = analyser.node_runs(node)
|
66
|
+
%{<span class="node-#{node.type} kind-#{kind} #{RUNS_CLASS[runs]}" title="#{RUNS_TITLE[runs]}">}
|
67
|
+
end
|
68
|
+
|
69
|
+
def insert_node_tags
|
70
|
+
analyser.each_node do |node|
|
71
|
+
h = node.executed_loc_hash
|
72
|
+
h.each do |kind, range|
|
73
|
+
wrap(range, node_span(node, kind), '</span>')
|
74
|
+
end
|
75
|
+
exp = node.expression
|
76
|
+
if (exp.nil? || exp.empty?) && !analyser.node_covered?(node) && !node.parent.is_a?(Node::Branch) # Not executed empty bodies must show!
|
77
|
+
replace(exp, icon(:empty, 'empty node never run'))
|
78
|
+
wrap(exp, node_span(node, :empty))
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
ICONS = {
|
84
|
+
fork: 'code-fork',
|
85
|
+
empty: 'code',
|
86
|
+
}.freeze
|
87
|
+
def icon(type, title)
|
88
|
+
%{<i class="#{type}-icon fa fa-#{ICONS[type]}" aria-hidden="true" title="#{title}"></i>}
|
89
|
+
end
|
90
|
+
|
91
|
+
def fork_span(node, kind, id, title: nil, klass: nil)
|
92
|
+
runs = analyser_map[:branch].node_runs(node)
|
93
|
+
title ||= RUNS_TITLE[runs]
|
94
|
+
%{<span class="fork fork-#{kind} fork-#{RUNS_CLASS[runs]} #{klass}" data-fork-id="#{id}">#{icon(:fork, title)}}
|
95
|
+
end
|
96
|
+
|
97
|
+
def insert_branch_tags
|
98
|
+
analyser_map[:branch].each_node.with_index do |node, id|
|
99
|
+
node.branches.each do |branch|
|
100
|
+
exp = branch.expression
|
101
|
+
wrap(exp, fork_span(branch, :branch, id), '</span>') if exp
|
102
|
+
end
|
103
|
+
runs = analyser_map[:branch].node_runs(node)
|
104
|
+
if !covered?(runs) && analyser.node_covered?(node)
|
105
|
+
jumps_missing = node.branches.reject { |jump| analyser.node_covered?(jump) }
|
106
|
+
title = "#{node.branches_summary(jumps_missing)} not covered"
|
107
|
+
klass = 'fork-with-uncovered-branches'
|
108
|
+
end
|
109
|
+
wrap(node.expression, fork_span(node, :whole, id, title: title, klass: klass))
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def replace(range, with)
|
114
|
+
@rewriter.replace(range, with)
|
115
|
+
end
|
116
|
+
|
117
|
+
def wrap(range, before, after = '</span>')
|
118
|
+
line = @rewriter.source_buffer.line_range(range.first_line)
|
119
|
+
pinned = range.with(end_pos: [range, line].map(&:end_pos).min)
|
120
|
+
@rewriter.wrap(pinned, before, after)
|
121
|
+
end
|
122
|
+
|
123
|
+
def html_escape
|
124
|
+
buffer = analyser.covered_code.buffer
|
125
|
+
source = buffer.source
|
126
|
+
{'<' => '<', '>' => '>', '&' => '&'}.each do |char, escaped|
|
127
|
+
source.scan(char) do
|
128
|
+
m = Regexp.last_match
|
129
|
+
range = Parser::Source::Range.new(buffer, m.begin(0), m.end(0))
|
130
|
+
@rewriter.replace(range, escaped)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
Binary file
|
Binary file
|