rspec-tracer 0.9.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -8,35 +8,40 @@ module RSpecTracer
8
8
  class Reporter
9
9
  attr_reader :last_run, :examples, :flaky_examples, :examples_dependency, :files_dependency
10
10
 
11
- def initialize
12
- @reporter = RSpecTracer.runner.reporter
13
-
14
- format_last_run
15
- format_examples
16
- format_flaky_examples
17
- format_examples_dependency
18
- format_files_dependency
11
+ def initialize(report_dir, reporter)
12
+ @report_dir = report_dir
13
+ @reporter = reporter
19
14
  end
20
15
 
21
16
  def generate_report
22
17
  starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
23
18
 
24
- copy_assets
19
+ prepare
25
20
 
26
- file_name = File.join(RSpecTracer.report_path, 'index.html')
21
+ file_name = File.join(@report_dir, 'index.html')
27
22
 
28
23
  File.open(file_name, 'wb') do |file|
29
24
  file.puts(template('layout').result(binding))
30
25
  end
31
26
 
32
27
  ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
33
- elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
28
+ elapsed = RSpecTracer::TimeFormatter.format_time(ending - starting)
34
29
 
35
- puts "RSpecTracer generated HTML report to #{file_name} (took #{elpased})"
30
+ puts "RSpecTracer generated HTML report to #{file_name} (took #{elapsed})"
36
31
  end
37
32
 
38
33
  private
39
34
 
35
+ def prepare
36
+ format_last_run
37
+ format_examples
38
+ format_duplicate_examples
39
+ format_flaky_examples
40
+ format_examples_dependency
41
+ format_files_dependency
42
+ copy_assets
43
+ end
44
+
40
45
  def copy_assets
41
46
  Dir[File.join(File.dirname(__FILE__), 'public/*')].each do |path|
42
47
  FileUtils.cp_r(path, asset_output_path)
@@ -46,6 +51,7 @@ module RSpecTracer
46
51
  def format_last_run
47
52
  @last_run = @reporter.last_run.slice(
48
53
  :actual_count,
54
+ :duplicate_examples,
49
55
  :failed_examples,
50
56
  :pending_examples,
51
57
  :skipped_examples
@@ -60,13 +66,39 @@ module RSpecTracer
60
66
  id: example_id,
61
67
  description: example[:full_description],
62
68
  location: example_location(example[:rerun_file_name], example[:rerun_line_number]),
63
- status: example[:run_reason] || 'Skipped',
69
+ status: example[:run_reason] || 'Skipped'
70
+ }.merge(example_result(example_id, example))
71
+ end
72
+ end
73
+
74
+ def example_result(example_id, example)
75
+ if example[:execution_result].nil?
76
+ {
77
+ result: @reporter.example_interrupted?(example_id) ? 'Interrupted' : '_',
78
+ last_run: '_'
79
+ }
80
+ else
81
+ {
64
82
  result: example[:execution_result][:status].capitalize,
65
83
  last_run: example_run_local_time(example[:execution_result][:finished_at])
66
84
  }
67
85
  end
68
86
  end
69
87
 
88
+ def format_duplicate_examples
89
+ @duplicate_examples = []
90
+
91
+ @reporter.duplicate_examples.each_pair do |example_id, examples|
92
+ examples.each do |example|
93
+ @duplicate_examples << {
94
+ id: example_id,
95
+ description: example[:full_description],
96
+ location: example_location(example[:rerun_file_name], example[:rerun_line_number])
97
+ }
98
+ end
99
+ end
100
+ end
101
+
70
102
  def format_flaky_examples
71
103
  @flaky_examples = @examples.slice(*@reporter.flaky_examples).values
72
104
  end
@@ -122,7 +154,7 @@ module RSpecTracer
122
154
 
123
155
  def asset_output_path
124
156
  @asset_output_path ||= begin
125
- asset_output_path = File.join(RSpecTracer.report_path, 'assets', RSpecTracer::VERSION)
157
+ asset_output_path = File.join(@report_dir, 'assets', RSpecTracer::VERSION)
126
158
 
127
159
  FileUtils.mkdir_p(asset_output_path)
128
160
 
@@ -142,6 +174,14 @@ module RSpecTracer
142
174
  template(title_id).result(current_binding)
143
175
  end
144
176
 
177
+ def formatted_duplicate_examples(title, duplicate_examples)
178
+ title_id = report_container_id(title)
179
+ current_binding = binding
180
+
181
+ current_binding.local_variable_set(:title_id, title_id)
182
+ template(title_id).result(current_binding)
183
+ end
184
+
145
185
  def formatted_flaky_examples(title, flaky_examples)
146
186
  title_id = report_container_id(title)
147
187
  current_binding = binding
@@ -176,7 +216,7 @@ module RSpecTracer
176
216
 
177
217
  def example_status_css_class(example_status)
178
218
  case example_status.split.first
179
- when 'Failed', 'Flaky'
219
+ when 'Failed', 'Flaky', 'Interrupted'
180
220
  'red'
181
221
  when 'Pending'
182
222
  'yellow'
@@ -189,7 +229,7 @@ module RSpecTracer
189
229
  case example_result
190
230
  when 'Passed'
191
231
  'green'
192
- when 'Failed'
232
+ when 'Failed', 'Interrupted'
193
233
  'red'
194
234
  when 'Pending'
195
235
  'yellow'
@@ -0,0 +1,34 @@
1
+ <div class="report_container" id="<%= title_id %>">
2
+ <h2>
3
+ <span class="group_name"><%= title %></span>
4
+ (
5
+ <span class="blue">
6
+ <strong><%= duplicate_examples.count %></strong>
7
+ </span> examples
8
+ )
9
+ </h2>
10
+
11
+ <a name="<%= title_id %>"></a>
12
+
13
+ <div class="report-table--responsive">
14
+ <table class="report-table">
15
+ <thead>
16
+ <tr>
17
+ <th>ID</th>
18
+ <th>Description</th>
19
+ <th>Location</th>
20
+ </tr>
21
+ </thead>
22
+
23
+ <tbody>
24
+ <% duplicate_examples.each do |example| %>
25
+ <tr>
26
+ <td><%= example[:id] %></td>
27
+ <td><%= example[:description] %></td>
28
+ <td><%= example[:location] %></td>
29
+ </tr>
30
+ <% end %>
31
+ </tbody>
32
+ </table>
33
+ </div>
34
+ </div>
@@ -5,6 +5,11 @@
5
5
  <span class="blue">
6
6
  <strong><%= last_run[:actual_count] %></strong>
7
7
  </span> examples,
8
+ <% if last_run[:duplicate_examples].positive? %>
9
+ <span class="blue">
10
+ <strong><%= last_run[:duplicate_examples] %></strong>
11
+ </span> duplicates,
12
+ <% end %>
8
13
  <% if last_run[:failed_examples].positive? %>
9
14
  <span class="red">
10
15
  <strong><%= last_run[:failed_examples] %></strong>
@@ -18,12 +18,15 @@
18
18
  <ul class="group_tabs"></ul>
19
19
 
20
20
  <div id="content">
21
- <%= formatted_examples('Examples', examples.values) %>
22
- <% unless flaky_examples.empty? %>
23
- <%= formatted_flaky_examples('Flaky Examples', flaky_examples) %>
21
+ <%= formatted_examples('Examples', @examples.values) %>
22
+ <% unless @duplicate_examples.empty? %>
23
+ <%= formatted_duplicate_examples('Duplicate Examples', @duplicate_examples) %>
24
24
  <% end %>
25
- <%= formatted_examples_dependency('Examples Dependency', examples_dependency) %>
26
- <%= formatted_files_dependency("Files Dependency", files_dependency) %>
25
+ <% unless @flaky_examples.empty? %>
26
+ <%= formatted_flaky_examples('Flaky Examples', @flaky_examples) %>
27
+ <% end %>
28
+ <%= formatted_examples_dependency('Examples Dependency', @examples_dependency) %>
29
+ <%= formatted_files_dependency("Files Dependency", @files_dependency) %>
27
30
  </div>
28
31
 
29
32
  <div id="footer">
@@ -3,7 +3,7 @@
3
3
  module RSpecTracer
4
4
  module RemoteCache
5
5
  class Validator
6
- CACHE_FILES_PER_TEST_SUITE = 8
6
+ CACHE_FILES_PER_TEST_SUITE = 11
7
7
 
8
8
  def initialize
9
9
  @test_suite_id = ENV['TEST_SUITE_ID']
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecTracer
4
+ class ReportGenerator
5
+ def initialize(reporter, cache)
6
+ @reporter = reporter
7
+ @cache = cache
8
+ end
9
+
10
+ def reverse_dependency_report
11
+ reverse_dependency = Hash.new do |examples, file_name|
12
+ examples[file_name] = {
13
+ example_count: 0,
14
+ examples: Hash.new(0)
15
+ }
16
+ end
17
+
18
+ @reporter.dependency.each_pair do |example_id, files|
19
+ next if @reporter.interrupted_examples.include?(example_id)
20
+
21
+ example_file = @reporter.all_examples[example_id][:rerun_file_name]
22
+
23
+ files.each do |file_name|
24
+ reverse_dependency[file_name][:example_count] += 1
25
+ reverse_dependency[file_name][:examples][example_file] += 1
26
+ end
27
+ end
28
+
29
+ reverse_dependency.transform_values! do |data|
30
+ {
31
+ example_count: data[:example_count],
32
+ examples: data[:examples].sort_by { |file_name, count| [-count, file_name] }.to_h
33
+ }
34
+ end
35
+
36
+ reverse_dependency.sort_by { |file_name, data| [-data[:example_count], file_name] }.to_h
37
+ end
38
+
39
+ def generate_report
40
+ generate_last_run_report
41
+ generate_examples_status_report
42
+
43
+ %i[all_files all_examples dependency examples_coverage reverse_dependency].each do |report_type|
44
+ starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
45
+
46
+ send("generate_#{report_type}_report")
47
+
48
+ ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
49
+ elapsed = RSpecTracer::TimeFormatter.format_time(ending - starting)
50
+
51
+ puts "RSpec tracer generated #{report_type.to_s.tr('_', ' ')} report (took #{elapsed})" if RSpecTracer.verbose?
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def generate_last_run_report
58
+ @reporter.last_run = {
59
+ pid: RSpecTracer.pid,
60
+ actual_count: RSpec.world.example_count + @reporter.skipped_examples.count,
61
+ example_count: RSpec.world.example_count,
62
+ duplicate_examples: @reporter.duplicate_examples.sum { |_, examples| examples.count },
63
+ interrupted_examples: @reporter.interrupted_examples.count,
64
+ failed_examples: @reporter.failed_examples.count,
65
+ skipped_examples: @reporter.skipped_examples.count,
66
+ pending_examples: @reporter.pending_examples.count,
67
+ flaky_examples: @reporter.flaky_examples.count
68
+ }
69
+ end
70
+
71
+ def generate_examples_status_report
72
+ starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
73
+
74
+ generate_flaky_examples_report
75
+ generate_failed_examples_report
76
+ generate_pending_examples_report
77
+
78
+ ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
79
+ elapsed = RSpecTracer::TimeFormatter.format_time(ending - starting)
80
+
81
+ puts "RSpec tracer generated flaky, failed, and pending examples report (took #{elapsed})" if RSpecTracer.verbose?
82
+ end
83
+
84
+ def generate_flaky_examples_report
85
+ @reporter.possibly_flaky_examples.each do |example_id|
86
+ next if @reporter.example_deleted?(example_id)
87
+ next unless @cache.flaky_examples.include?(example_id) ||
88
+ @reporter.example_passed?(example_id)
89
+
90
+ @reporter.register_flaky_example(example_id)
91
+ end
92
+ end
93
+
94
+ def generate_failed_examples_report
95
+ @cache.failed_examples.each do |example_id|
96
+ next if @reporter.example_deleted?(example_id) ||
97
+ @reporter.all_examples.key?(example_id)
98
+
99
+ @reporter.register_failed_example(example_id)
100
+ end
101
+ end
102
+
103
+ def generate_pending_examples_report
104
+ @cache.pending_examples.each do |example_id|
105
+ next if @reporter.example_deleted?(example_id) ||
106
+ @reporter.all_examples.key?(example_id)
107
+
108
+ @reporter.register_pending_example(example_id)
109
+ end
110
+ end
111
+
112
+ def generate_all_files_report
113
+ @cache.all_files.each_pair do |file_name, data|
114
+ next if @reporter.all_files.key?(file_name) ||
115
+ @reporter.file_deleted?(file_name)
116
+
117
+ @reporter.all_files[file_name] = data
118
+ end
119
+ end
120
+
121
+ def generate_all_examples_report
122
+ @cache.all_examples.each_pair do |example_id, data|
123
+ next if @reporter.all_examples.key?(example_id) ||
124
+ @reporter.example_deleted?(example_id)
125
+
126
+ @reporter.all_examples[example_id] = data
127
+ end
128
+ end
129
+
130
+ def generate_dependency_report
131
+ @cache.dependency.each_pair do |example_id, data|
132
+ next if @reporter.dependency.key?(example_id) ||
133
+ @reporter.example_deleted?(example_id)
134
+
135
+ @reporter.dependency[example_id] = data.reject do |file_name|
136
+ @reporter.file_deleted?(file_name)
137
+ end
138
+ end
139
+
140
+ @reporter.dependency.transform_values!(&:to_a)
141
+ end
142
+
143
+ def generate_examples_coverage_report
144
+ @cache.cached_examples_coverage.each_pair do |example_id, data|
145
+ next if @reporter.examples_coverage.key?(example_id) ||
146
+ @reporter.example_deleted?(example_id)
147
+
148
+ @reporter.examples_coverage[example_id] = data.reject do |file_name|
149
+ @reporter.file_deleted?(file_name)
150
+ end
151
+ end
152
+ end
153
+
154
+ def generate_reverse_dependency_report
155
+ @reporter.reverse_dependency = reverse_dependency_report
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecTracer
4
+ class ReportMerger
5
+ attr_reader :all_examples, :duplicate_examples, :interrupted_examples,
6
+ :flaky_examples, :failed_examples, :pending_examples, :skipped_examples,
7
+ :all_files, :dependency, :reverse_dependency, :examples_coverage, :last_run
8
+
9
+ def initialize
10
+ @last_run = {}
11
+ @all_examples = {}
12
+ @duplicate_examples = {}
13
+ @interrupted_examples = Set.new
14
+ @flaky_examples = Set.new
15
+ @failed_examples = Set.new
16
+ @pending_examples = Set.new
17
+ @skipped_examples = Set.new
18
+ @all_files = {}
19
+ @dependency = Hash.new { |hash, key| hash[key] = Set.new }
20
+ @reverse_dependency = {}
21
+ @examples_coverage = {}
22
+ end
23
+
24
+ def merge(reports_dir)
25
+ reports_dir.each do |report_dir|
26
+ next unless File.directory?(report_dir)
27
+
28
+ merge_cache(load_cache(report_dir))
29
+ merge_last_run_report(File.dirname(report_dir))
30
+ end
31
+
32
+ @dependency.transform_values!(&:to_a)
33
+
34
+ @reverse_dependency = RSpecTracer::ReportGenerator.new(self, nil).reverse_dependency_report
35
+ end
36
+
37
+ private
38
+
39
+ def load_cache(cache_dir)
40
+ cache = RSpecTracer::Cache.new
41
+
42
+ cache.send(:load_all_examples_cache, cache_dir, discard_run_reason: false)
43
+ cache.send(:load_duplicate_examples_cache, cache_dir)
44
+ cache.send(:load_interrupted_examples_cache, cache_dir)
45
+ cache.send(:load_flaky_examples_cache, cache_dir)
46
+ cache.send(:load_failed_examples_cache, cache_dir)
47
+ cache.send(:load_pending_examples_cache, cache_dir)
48
+ cache.send(:load_skipped_examples_cache, cache_dir)
49
+ cache.send(:load_all_files_cache, cache_dir)
50
+ cache.send(:load_dependency_cache, cache_dir)
51
+ cache.send(:load_examples_coverage_cache, cache_dir)
52
+
53
+ cache
54
+ end
55
+
56
+ def merge_cache(cache)
57
+ @all_examples.merge!(cache.all_examples) { |_, v1, v2| v1[:run_reason] ? v1 : v2 }
58
+ @duplicate_examples.merge!(cache.duplicate_examples) { |_, v1, v2| v1 + v2 }
59
+ @interrupted_examples.merge(cache.interrupted_examples)
60
+ @flaky_examples.merge(cache.flaky_examples)
61
+ @failed_examples.merge(cache.failed_examples)
62
+ @pending_examples.merge(cache.pending_examples)
63
+ @skipped_examples.merge(cache.skipped_examples)
64
+ @all_files.merge!(cache.all_files)
65
+ @dependency.merge!(cache.dependency) { |_, v1, v2| v1.merge(v2) }
66
+ @examples_coverage.merge!(cache.examples_coverage) do |_, v1, v2|
67
+ v1.merge(v2) { |_, v3, v4| v3.merge(v4) { |_, v5, v6| v5 + v6 } }
68
+ end
69
+ end
70
+
71
+ def merge_last_run_report(cache_dir)
72
+ file_name = File.join(cache_dir, 'last_run.json')
73
+ cached_last_run = JSON.parse(File.read(file_name), symbolize_names: true)
74
+ cached_last_run[:pid] = [cached_last_run[:pid]]
75
+
76
+ cached_last_run.delete_if { |key, _| %i[run_id timestamp].include?(key) }
77
+
78
+ @last_run.merge!(cached_last_run) { |_, v1, v2| v1 + v2 }
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecTracer
4
+ class ReportWriter
5
+ def initialize(report_dir, reporter)
6
+ @report_dir = report_dir
7
+ @reporter = reporter
8
+ end
9
+
10
+ def write_report
11
+ starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
12
+
13
+ @run_id = Digest::MD5.hexdigest(@reporter.all_examples.keys.sort.to_json)
14
+ @cache_dir = File.join(@report_dir, @run_id)
15
+
16
+ FileUtils.mkdir_p(@cache_dir)
17
+
18
+ write_all_examples_report
19
+ write_duplicate_examples_report
20
+ write_interrupted_examples_report
21
+ write_flaky_examples_report
22
+ write_failed_examples_report
23
+ write_pending_examples_report
24
+ write_skipped_examples_report
25
+ write_all_files_report
26
+ write_dependency_report
27
+ write_reverse_dependency_report
28
+ write_examples_coverage_report
29
+ write_last_run_report
30
+
31
+ ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
32
+ elapsed = RSpecTracer::TimeFormatter.format_time(ending - starting)
33
+
34
+ puts "RSpec tracer reports written to #{@cache_dir} (took #{elapsed})"
35
+ end
36
+
37
+ def print_duplicate_examples
38
+ return if @reporter.duplicate_examples.empty?
39
+
40
+ total = @reporter.duplicate_examples.sum { |_, examples| examples.count }
41
+
42
+ puts '=' * 80
43
+ puts ' IMPORTANT NOTICE -- RSPEC TRACER COULD NOT IDENTIFY SOME EXAMPLES UNIQUELY'
44
+ puts '=' * 80
45
+ puts "RSpec tracer could not uniquely identify the following #{total} examples:"
46
+
47
+ justify = ' ' * 2
48
+ nested_justify = justify * 3
49
+
50
+ @reporter.duplicate_examples.each_pair do |example_id, examples|
51
+ puts "#{justify}- Example ID: #{example_id} (#{examples.count} examples)"
52
+
53
+ examples.each do |example|
54
+ description = example[:full_description].strip
55
+ file_name = example[:rerun_file_name].sub(%r{^/}, '')
56
+ line_number = example[:rerun_line_number]
57
+ location = "#{file_name}:#{line_number}"
58
+
59
+ puts "#{nested_justify}* #{description} (#{location})"
60
+ end
61
+ end
62
+
63
+ puts
64
+ end
65
+
66
+ private
67
+
68
+ def write_all_examples_report
69
+ file_name = File.join(@cache_dir, 'all_examples.json')
70
+
71
+ File.write(file_name, JSON.pretty_generate(@reporter.all_examples))
72
+ end
73
+
74
+ def write_duplicate_examples_report
75
+ file_name = File.join(@cache_dir, 'duplicate_examples.json')
76
+
77
+ File.write(file_name, JSON.pretty_generate(@reporter.duplicate_examples))
78
+ end
79
+
80
+ def write_interrupted_examples_report
81
+ file_name = File.join(@cache_dir, 'interrupted_examples.json')
82
+
83
+ File.write(file_name, JSON.pretty_generate(@reporter.interrupted_examples.sort.to_a))
84
+ end
85
+
86
+ def write_flaky_examples_report
87
+ file_name = File.join(@cache_dir, 'flaky_examples.json')
88
+
89
+ File.write(file_name, JSON.pretty_generate(@reporter.flaky_examples.sort.to_a))
90
+ end
91
+
92
+ def write_failed_examples_report
93
+ file_name = File.join(@cache_dir, 'failed_examples.json')
94
+
95
+ File.write(file_name, JSON.pretty_generate(@reporter.failed_examples.sort.to_a))
96
+ end
97
+
98
+ def write_pending_examples_report
99
+ file_name = File.join(@cache_dir, 'pending_examples.json')
100
+
101
+ File.write(file_name, JSON.pretty_generate(@reporter.pending_examples.sort.to_a))
102
+ end
103
+
104
+ def write_skipped_examples_report
105
+ file_name = File.join(@cache_dir, 'skipped_examples.json')
106
+
107
+ File.write(file_name, JSON.pretty_generate(@reporter.skipped_examples.sort.to_a))
108
+ end
109
+
110
+ def write_all_files_report
111
+ file_name = File.join(@cache_dir, 'all_files.json')
112
+
113
+ File.write(file_name, JSON.pretty_generate(@reporter.all_files))
114
+ end
115
+
116
+ def write_dependency_report
117
+ file_name = File.join(@cache_dir, 'dependency.json')
118
+
119
+ File.write(file_name, JSON.pretty_generate(@reporter.dependency))
120
+ end
121
+
122
+ def write_reverse_dependency_report
123
+ file_name = File.join(@cache_dir, 'reverse_dependency.json')
124
+
125
+ File.write(file_name, JSON.pretty_generate(@reporter.reverse_dependency))
126
+ end
127
+
128
+ def write_examples_coverage_report
129
+ file_name = File.join(@cache_dir, 'examples_coverage.json')
130
+
131
+ File.write(file_name, JSON.pretty_generate(@reporter.examples_coverage))
132
+ end
133
+
134
+ def write_last_run_report
135
+ file_name = File.join(@report_dir, 'last_run.json')
136
+ last_run_data = @reporter.last_run.merge(run_id: @run_id, timestamp: Time.now.utc)
137
+
138
+ File.write(file_name, JSON.pretty_generate(last_run_data))
139
+ end
140
+ end
141
+ end