rspec-tracer 0.9.0 → 1.0.0

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