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.
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