deep-cover-core 0.6.3.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (131) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +4 -0
  3. data/.rspec_all +3 -0
  4. data/.rubocop.yml +1 -0
  5. data/Gemfile +11 -0
  6. data/deep_cover_core.gemspec +46 -0
  7. data/lib/deep-cover.rb +3 -0
  8. data/lib/deep_cover/analyser/base.rb +104 -0
  9. data/lib/deep_cover/analyser/branch.rb +41 -0
  10. data/lib/deep_cover/analyser/covered_code_source.rb +21 -0
  11. data/lib/deep_cover/analyser/function.rb +14 -0
  12. data/lib/deep_cover/analyser/node.rb +54 -0
  13. data/lib/deep_cover/analyser/per_char.rb +38 -0
  14. data/lib/deep_cover/analyser/per_line.rb +41 -0
  15. data/lib/deep_cover/analyser/ruby25_like_branch.rb +211 -0
  16. data/lib/deep_cover/analyser/statement.rb +33 -0
  17. data/lib/deep_cover/analyser/stats.rb +54 -0
  18. data/lib/deep_cover/analyser/subset.rb +27 -0
  19. data/lib/deep_cover/analyser.rb +23 -0
  20. data/lib/deep_cover/auto_run.rb +71 -0
  21. data/lib/deep_cover/autoload_tracker.rb +215 -0
  22. data/lib/deep_cover/backports.rb +22 -0
  23. data/lib/deep_cover/base.rb +117 -0
  24. data/lib/deep_cover/basics.rb +22 -0
  25. data/lib/deep_cover/builtin_takeover.rb +10 -0
  26. data/lib/deep_cover/config.rb +104 -0
  27. data/lib/deep_cover/config_setter.rb +33 -0
  28. data/lib/deep_cover/core_ext/autoload_overrides.rb +112 -0
  29. data/lib/deep_cover/core_ext/coverage_replacement.rb +61 -0
  30. data/lib/deep_cover/core_ext/exec_callbacks.rb +27 -0
  31. data/lib/deep_cover/core_ext/instruction_sequence_load_iseq.rb +32 -0
  32. data/lib/deep_cover/core_ext/load_overrides.rb +19 -0
  33. data/lib/deep_cover/core_ext/require_overrides.rb +28 -0
  34. data/lib/deep_cover/coverage/analysis.rb +42 -0
  35. data/lib/deep_cover/coverage/persistence.rb +84 -0
  36. data/lib/deep_cover/coverage.rb +125 -0
  37. data/lib/deep_cover/covered_code.rb +145 -0
  38. data/lib/deep_cover/custom_requirer.rb +187 -0
  39. data/lib/deep_cover/flag_comment_associator.rb +68 -0
  40. data/lib/deep_cover/load.rb +66 -0
  41. data/lib/deep_cover/memoize.rb +48 -0
  42. data/lib/deep_cover/module_override.rb +39 -0
  43. data/lib/deep_cover/node/arguments.rb +51 -0
  44. data/lib/deep_cover/node/assignments.rb +273 -0
  45. data/lib/deep_cover/node/base.rb +155 -0
  46. data/lib/deep_cover/node/begin.rb +27 -0
  47. data/lib/deep_cover/node/block.rb +61 -0
  48. data/lib/deep_cover/node/branch.rb +32 -0
  49. data/lib/deep_cover/node/case.rb +113 -0
  50. data/lib/deep_cover/node/collections.rb +31 -0
  51. data/lib/deep_cover/node/const.rb +12 -0
  52. data/lib/deep_cover/node/def.rb +40 -0
  53. data/lib/deep_cover/node/empty_body.rb +32 -0
  54. data/lib/deep_cover/node/exceptions.rb +79 -0
  55. data/lib/deep_cover/node/if.rb +73 -0
  56. data/lib/deep_cover/node/keywords.rb +86 -0
  57. data/lib/deep_cover/node/literals.rb +100 -0
  58. data/lib/deep_cover/node/loops.rb +74 -0
  59. data/lib/deep_cover/node/mixin/can_augment_children.rb +65 -0
  60. data/lib/deep_cover/node/mixin/check_completion.rb +18 -0
  61. data/lib/deep_cover/node/mixin/child_can_be_empty.rb +27 -0
  62. data/lib/deep_cover/node/mixin/executed_after_children.rb +15 -0
  63. data/lib/deep_cover/node/mixin/execution_location.rb +66 -0
  64. data/lib/deep_cover/node/mixin/filters.rb +47 -0
  65. data/lib/deep_cover/node/mixin/flow_accounting.rb +71 -0
  66. data/lib/deep_cover/node/mixin/has_child.rb +145 -0
  67. data/lib/deep_cover/node/mixin/has_child_handler.rb +75 -0
  68. data/lib/deep_cover/node/mixin/has_tracker.rb +46 -0
  69. data/lib/deep_cover/node/mixin/is_statement.rb +20 -0
  70. data/lib/deep_cover/node/mixin/rewriting.rb +35 -0
  71. data/lib/deep_cover/node/mixin/wrapper.rb +15 -0
  72. data/lib/deep_cover/node/module.rb +66 -0
  73. data/lib/deep_cover/node/root.rb +20 -0
  74. data/lib/deep_cover/node/send.rb +161 -0
  75. data/lib/deep_cover/node/short_circuit.rb +42 -0
  76. data/lib/deep_cover/node/splat.rb +15 -0
  77. data/lib/deep_cover/node/variables.rb +16 -0
  78. data/lib/deep_cover/node.rb +23 -0
  79. data/lib/deep_cover/parser_ext/range.rb +21 -0
  80. data/lib/deep_cover/problem_with_diagnostic.rb +63 -0
  81. data/lib/deep_cover/reporter/base.rb +68 -0
  82. data/lib/deep_cover/reporter/html/base.rb +14 -0
  83. data/lib/deep_cover/reporter/html/index.rb +59 -0
  84. data/lib/deep_cover/reporter/html/site.rb +68 -0
  85. data/lib/deep_cover/reporter/html/source.rb +136 -0
  86. data/lib/deep_cover/reporter/html/template/assets/32px.png +0 -0
  87. data/lib/deep_cover/reporter/html/template/assets/40px.png +0 -0
  88. data/lib/deep_cover/reporter/html/template/assets/deep_cover.css +291 -0
  89. data/lib/deep_cover/reporter/html/template/assets/deep_cover.css.sass +336 -0
  90. data/lib/deep_cover/reporter/html/template/assets/jquery-3.2.1.min.js +4 -0
  91. data/lib/deep_cover/reporter/html/template/assets/jquery-3.2.1.min.map +1 -0
  92. data/lib/deep_cover/reporter/html/template/assets/jstree.css +1108 -0
  93. data/lib/deep_cover/reporter/html/template/assets/jstree.js +8424 -0
  94. data/lib/deep_cover/reporter/html/template/assets/jstreetable.js +1069 -0
  95. data/lib/deep_cover/reporter/html/template/assets/throbber.gif +0 -0
  96. data/lib/deep_cover/reporter/html/template/index.html.erb +75 -0
  97. data/lib/deep_cover/reporter/html/template/source.html.erb +35 -0
  98. data/lib/deep_cover/reporter/html.rb +15 -0
  99. data/lib/deep_cover/reporter/istanbul.rb +184 -0
  100. data/lib/deep_cover/reporter/text.rb +58 -0
  101. data/lib/deep_cover/reporter/tree/util.rb +86 -0
  102. data/lib/deep_cover/reporter.rb +10 -0
  103. data/lib/deep_cover/tools/blank.rb +25 -0
  104. data/lib/deep_cover/tools/builtin_coverage.rb +55 -0
  105. data/lib/deep_cover/tools/camelize.rb +13 -0
  106. data/lib/deep_cover/tools/content_tag.rb +11 -0
  107. data/lib/deep_cover/tools/covered.rb +9 -0
  108. data/lib/deep_cover/tools/execute_sample.rb +34 -0
  109. data/lib/deep_cover/tools/format.rb +18 -0
  110. data/lib/deep_cover/tools/format_char_cover.rb +19 -0
  111. data/lib/deep_cover/tools/format_generated_code.rb +27 -0
  112. data/lib/deep_cover/tools/indent_string.rb +26 -0
  113. data/lib/deep_cover/tools/merge.rb +16 -0
  114. data/lib/deep_cover/tools/number_lines.rb +22 -0
  115. data/lib/deep_cover/tools/our_coverage.rb +11 -0
  116. data/lib/deep_cover/tools/profiling.rb +68 -0
  117. data/lib/deep_cover/tools/render_template.rb +13 -0
  118. data/lib/deep_cover/tools/require_relative_dir.rb +12 -0
  119. data/lib/deep_cover/tools/scan_match_datas.rb +10 -0
  120. data/lib/deep_cover/tools/silence_warnings.rb +18 -0
  121. data/lib/deep_cover/tools/slice.rb +9 -0
  122. data/lib/deep_cover/tools/strip_heredoc.rb +18 -0
  123. data/lib/deep_cover/tools/truncate_backtrace.rb +32 -0
  124. data/lib/deep_cover/tools.rb +22 -0
  125. data/lib/deep_cover/tracker_bucket.rb +50 -0
  126. data/lib/deep_cover/tracker_hits_per_path.rb +35 -0
  127. data/lib/deep_cover/tracker_storage.rb +76 -0
  128. data/lib/deep_cover/tracker_storage_per_path.rb +34 -0
  129. data/lib/deep_cover/version.rb +5 -0
  130. data/lib/deep_cover.rb +22 -0
  131. 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
+ {'<' => '&lt;', '>' => '&gt;', '&' => '&amp;'}.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