rspec-tracer 0.3.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,38 @@
1
+ <div class="report_container" id="<%= title_id %>">
2
+ <h2>
3
+ <span class="group_name"><%= title %></span>
4
+ (
5
+ <span class="blue">
6
+ <strong><%= flaky_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
+ <th>Result</th>
21
+ <th>Run At</th>
22
+ </tr>
23
+ </thead>
24
+
25
+ <tbody>
26
+ <% flaky_examples.each do |example| %>
27
+ <tr>
28
+ <td><%= example[:id] %></td>
29
+ <td><%= example[:description] %></td>
30
+ <td><%= example[:location] %></td>
31
+ <td><strong class="<%= example_result_css_class(example[:result]) %>"><%= example[:result] %></strong></td>
32
+ <td width="8%"><%= example[:last_run] %></td>
33
+ </tr>
34
+ <% end %>
35
+ </tbody>
36
+ </table>
37
+ </div>
38
+ </div>
@@ -19,6 +19,9 @@
19
19
 
20
20
  <div id="content">
21
21
  <%= formatted_examples('Examples', examples.values) %>
22
+ <% unless flaky_examples.empty? %>
23
+ <%= formatted_flaky_examples('Flaky Examples', flaky_examples) %>
24
+ <% end %>
22
25
  <%= formatted_examples_dependency('Examples Dependency', examples_dependency) %>
23
26
  <%= formatted_files_dependency("Files Dependency", files_dependency) %>
24
27
  </div>
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'git'
4
+
5
+ module RSpecTracer
6
+ module RemoteCache
7
+ class Cache
8
+ class CacheDownloadError < StandardError; end
9
+
10
+ class CacheUploadError < StandardError; end
11
+
12
+ class LocalCacheNotFoundError < StandardError; end
13
+
14
+ CACHE_FILES_PER_TEST_SUITE = 8
15
+
16
+ def initialize
17
+ @s3_uri = ENV['RSPEC_TRACER_S3_URI']
18
+ @aws_s3 = if ENV.fetch('LOCAL_AWS', 'false') == 'true'
19
+ 'awslocal'
20
+ else
21
+ 'aws'
22
+ end
23
+ end
24
+
25
+ def download
26
+ if @s3_uri.nil?
27
+ puts 'S3 URI is not configured'
28
+
29
+ return
30
+ end
31
+
32
+ prepare_for_download
33
+
34
+ if @cache_sha.nil?
35
+ puts 'Could not find a suitable cache sha to download'
36
+
37
+ return
38
+ end
39
+
40
+ download_files
41
+
42
+ puts "Downloaded cache from #{@download_prefix} to #{@download_path}"
43
+ rescue StandardError => e
44
+ puts "Errored: #{e.message}"
45
+ end
46
+
47
+ def upload
48
+ if @s3_uri.nil?
49
+ puts 'S3 URI is not configured'
50
+
51
+ return
52
+ end
53
+
54
+ prepare_for_upload
55
+ upload_files
56
+
57
+ puts "Uploaded cache from #{@upload_path} to #{@upload_prefix}"
58
+ rescue CacheUploadError => e
59
+ puts "Errored: #{e.message}"
60
+ end
61
+
62
+ private
63
+
64
+ def prepare_for_download
65
+ @test_suite_id = ENV['TEST_SUITE_ID']
66
+ @test_suites = ENV['TEST_SUITES']
67
+
68
+ if @test_suite_id.nil? ^ @test_suites.nil?
69
+ raise(
70
+ CacheDownloadError,
71
+ 'Both the enviornment variables TEST_SUITE_ID and TEST_SUITES are not set'
72
+ )
73
+ end
74
+
75
+ @git = RSpecTracer::RemoteCache::Git.new
76
+ @git.prepare_for_download
77
+
78
+ generate_cached_files_count_and_regex
79
+
80
+ @cache_sha = nearest_cache_sha
81
+ end
82
+
83
+ def generate_cached_files_count_and_regex
84
+ if @test_suites.nil?
85
+ @last_run_files_count = 1
86
+ @last_run_files_regex = '/%<ref>s/last_run.json$'
87
+ @cached_files_count = CACHE_FILES_PER_TEST_SUITE
88
+ @cached_files_regex = '/%<ref>s/[0-9a-f]{32}/.+.json'
89
+ else
90
+ @test_suites = @test_suites.to_i
91
+ @test_suites_regex = (1..@test_suites).to_a.join('|')
92
+
93
+ @last_run_files_count = @test_suites
94
+ @last_run_files_regex = "/%<ref>s/(#{@test_suites_regex})/last_run.json$"
95
+ @cached_files_count = CACHE_FILES_PER_TEST_SUITE * @test_suites.to_i
96
+ @cached_files_regex = "/%<ref>s/(#{@test_suites_regex})/[0-9a-f]{32}/.+.json$"
97
+ end
98
+ end
99
+
100
+ def nearest_cache_sha
101
+ @git.ref_list.detect do |ref|
102
+ prefix = "#{@s3_uri}/#{ref}/"
103
+
104
+ puts "Testing prefix #{prefix}"
105
+
106
+ objects = `#{@aws_s3} s3 ls #{prefix} --recursive`.chomp.split("\n")
107
+
108
+ last_run_regex = Regexp.new(format(@last_run_files_regex, ref: ref))
109
+
110
+ next if objects.count { |object| object.match?(last_run_regex) } != @last_run_files_count
111
+
112
+ cache_regex = Regexp.new(format(@cached_files_regex, ref: ref))
113
+
114
+ objects.count { |object| object.match?(cache_regex) } == @cached_files_count
115
+ end
116
+ end
117
+
118
+ def download_files
119
+ @download_prefix = "#{@s3_uri}/#{@cache_sha}/#{@test_suite_id}/".sub(%r{/+$}, '/')
120
+ @download_path = RSpecTracer.cache_path
121
+
122
+ raise CacheDownloadError, 'Failed to download cache files' unless system(
123
+ @aws_s3, 's3', 'cp',
124
+ File.join(@download_prefix, 'last_run.json'),
125
+ @download_path,
126
+ out: File::NULL, err: File::NULL
127
+ )
128
+
129
+ @run_id = last_run_id
130
+
131
+ return if system(
132
+ @aws_s3, 's3', 'cp',
133
+ File.join(@download_prefix, @run_id),
134
+ File.join(@download_path, @run_id),
135
+ '--recursive',
136
+ out: File::NULL, err: File::NULL
137
+ )
138
+
139
+ FileUtils.rm_rf(@download_path)
140
+
141
+ raise CacheDownloadError, 'Failed to download cache files'
142
+ end
143
+
144
+ def prepare_for_upload
145
+ @git = RSpecTracer::RemoteCache::Git.new
146
+ @test_suite_id = ENV['TEST_SUITE_ID']
147
+ @upload_prefix = if @test_suite_id.nil?
148
+ "#{@s3_uri}/#{@git.branch_ref}/"
149
+ else
150
+ "#{@s3_uri}/#{@git.branch_ref}/#{@test_suite_id}/"
151
+ end
152
+
153
+ @upload_path = RSpecTracer.cache_path
154
+ @run_id = last_run_id
155
+ end
156
+
157
+ def upload_files
158
+ return if system(
159
+ @aws_s3, 's3', 'cp',
160
+ File.join(@upload_path, 'last_run.json'),
161
+ @upload_prefix,
162
+ out: File::NULL, err: File::NULL
163
+ ) && system(
164
+ @aws_s3, 's3', 'cp',
165
+ File.join(@upload_path, @run_id),
166
+ File.join(@upload_prefix, @run_id),
167
+ '--recursive',
168
+ out: File::NULL, err: File::NULL
169
+ )
170
+
171
+ raise CacheUploadError, 'Failed to upload cache files'
172
+ end
173
+
174
+ def last_run_id
175
+ file_name = File.join(RSpecTracer.cache_path, 'last_run.json')
176
+
177
+ return unless File.file?(file_name)
178
+
179
+ run_id = JSON.parse(File.read(file_name))['run_id']
180
+
181
+ raise LocalCacheNotFoundError, 'Could not find any local cache to upload' if run_id.nil?
182
+
183
+ run_id
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecTracer
4
+ module RemoteCache
5
+ class Git
6
+ class GitOperationError < StandardError; end
7
+
8
+ attr_reader :branch_ref, :ref_list
9
+
10
+ def initialize
11
+ fetch_head_ref
12
+ fetch_branch_ref
13
+ end
14
+
15
+ def prepare_for_download
16
+ fetch_unreachable_refs
17
+ fetch_ancestry_refs
18
+ fetch_ordered_refs
19
+ end
20
+
21
+ private
22
+
23
+ def fetch_head_ref
24
+ @head_ref = `git rev-parse HEAD`.chomp
25
+
26
+ raise GitOperationError, 'Could not find HEAD commit sha' unless $CHILD_STATUS.success?
27
+ end
28
+
29
+ def fetch_branch_ref
30
+ @merged_parents = []
31
+ @ignored_refs = []
32
+
33
+ unless merged?
34
+ @branch_ref = @head_ref
35
+
36
+ return
37
+ end
38
+
39
+ @ignored_refs << @head_ref
40
+
41
+ fetch_merged_parents
42
+ fetch_merged_branch_ref
43
+ end
44
+
45
+ def merged?
46
+ system('git', 'rev-parse', 'HEAD^2', out: File::NULL, err: File::NULL)
47
+ end
48
+
49
+ def fetch_merged_parents
50
+ first_parent = `git rev-parse HEAD^1`.chomp
51
+ @merged_parents << first_parent if $CHILD_STATUS.success?
52
+
53
+ second_parent = `git rev-parse HEAD^2`.chomp
54
+ @merged_parents << second_parent if $CHILD_STATUS.success?
55
+
56
+ raise GitOperationError, 'Could not find merged commit parents' if @merged_parents.length != 2
57
+ end
58
+
59
+ def fetch_merged_branch_ref
60
+ @origin_head_ref = `git rev-parse origin/HEAD`.chomp
61
+ @branch_ref = nil
62
+
63
+ if @merged_parents.first != @origin_head_ref
64
+ @branch_ref = @head_ref
65
+ @ignored_refs = []
66
+
67
+ return
68
+ end
69
+
70
+ @branch_ref = @merged_parents.last
71
+ @ignored_refs = @ignored_refs.to_set | `git rev-list #{@branch_ref}..origin/HEAD`.chomp.split
72
+
73
+ raise GitOperationError, 'Could not find ignored refs' unless $CHILD_STATUS.success?
74
+ end
75
+
76
+ def fetch_unreachable_refs
77
+ command = <<-COMMAND.strip.gsub(/\s+/, ' ')
78
+ git fsck
79
+ --no-progress
80
+ --unreachable
81
+ --connectivity-only #{@branch_ref}
82
+ | awk '/commit/ { print $3 }'
83
+ | head -n 25
84
+ COMMAND
85
+
86
+ @unreachable_refs = `#{command}`.chomp.split
87
+
88
+ raise GitOperationError, 'Could not find unreachable refs' unless $CHILD_STATUS.success?
89
+ end
90
+
91
+ def fetch_ancestry_refs
92
+ @ancestry_refs = `git rev-list --max-count=25 #{@branch_ref}`.chomp.split
93
+
94
+ raise GitOperationError, 'Could not find ancestry refs' unless $CHILD_STATUS.success?
95
+ end
96
+
97
+ def fetch_ordered_refs
98
+ unordered_refs = (@unreachable_refs.to_set | @ancestry_refs) - @ignored_refs
99
+
100
+ command = <<-COMMAND.strip.gsub(/\s+/, ' ')
101
+ git rev-list
102
+ --topo-order
103
+ --no-walk=sorted
104
+ #{unordered_refs.to_a.join(' ')}
105
+ COMMAND
106
+
107
+ @ref_list = `#{command}`.chomp.split
108
+
109
+ raise GitOperationError, 'Could not find refs to download cache' unless $CHILD_STATUS.success?
110
+ end
111
+ end
112
+ end
113
+ end
@@ -2,8 +2,9 @@
2
2
 
3
3
  module RSpecTracer
4
4
  class Reporter
5
- attr_reader :all_examples, :pending_examples, :all_files, :dependency,
6
- :reverse_dependency, :examples_coverage, :last_run
5
+ attr_reader :all_examples, :possibly_flaky_examples, :flaky_examples, :pending_examples,
6
+ :all_files, :modified_files, :deleted_files, :dependency, :reverse_dependency,
7
+ :examples_coverage, :last_run
7
8
 
8
9
  def initialize
9
10
  initialize_examples
@@ -21,6 +22,7 @@ module RSpecTracer
21
22
  end
22
23
 
23
24
  def on_example_passed(example_id, result)
25
+ @passed_examples << example_id
24
26
  @all_examples[example_id][:execution_result] = formatted_execution_result(result)
25
27
  end
26
28
 
@@ -44,6 +46,14 @@ module RSpecTracer
44
46
  end
45
47
  end
46
48
 
49
+ def register_possibly_flaky_example(example_id)
50
+ @possibly_flaky_examples << example_id
51
+ end
52
+
53
+ def register_flaky_example(example_id)
54
+ @flaky_examples << example_id
55
+ end
56
+
47
57
  def register_failed_example(example_id)
48
58
  @failed_examples << example_id
49
59
  end
@@ -52,6 +62,10 @@ module RSpecTracer
52
62
  @pending_examples << example_id
53
63
  end
54
64
 
65
+ def example_passed?(example_id)
66
+ @passed_examples.include?(example_id)
67
+ end
68
+
55
69
  def example_skipped?(example_id)
56
70
  @skipped_examples.include?(example_id)
57
71
  end
@@ -126,6 +140,8 @@ module RSpecTracer
126
140
  end
127
141
 
128
142
  def write_reports
143
+ starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
144
+
129
145
  @run_id = Digest::MD5.hexdigest(@all_examples.keys.sort.to_json)
130
146
  @cache_dir = File.join(RSpecTracer.cache_path, @run_id)
131
147
 
@@ -133,6 +149,7 @@ module RSpecTracer
133
149
 
134
150
  %i[
135
151
  all_examples
152
+ flaky_examples
136
153
  failed_examples
137
154
  pending_examples
138
155
  all_files
@@ -142,13 +159,19 @@ module RSpecTracer
142
159
  last_run
143
160
  ].each { |report_type| send("write_#{report_type}_report") }
144
161
 
145
- puts "RSpec tracer reports generated to #{@cache_dir}"
162
+ ending = Process.clock_gettime(Process::CLOCK_MONOTONIC)
163
+ elpased = RSpecTracer::TimeFormatter.format_time(ending - starting)
164
+
165
+ puts "RSpec tracer reports written to #{@cache_dir} (took #{elpased})"
146
166
  end
147
167
 
148
168
  private
149
169
 
150
170
  def initialize_examples
151
171
  @all_examples = {}
172
+ @passed_examples = Set.new
173
+ @possibly_flaky_examples = Set.new
174
+ @flaky_examples = Set.new
152
175
  @failed_examples = Set.new
153
176
  @skipped_examples = Set.new
154
177
  @pending_examples = Set.new
@@ -206,50 +229,56 @@ module RSpecTracer
206
229
  def write_all_examples_report
207
230
  file_name = File.join(@cache_dir, 'all_examples.json')
208
231
 
209
- File.write(file_name, JSON.pretty_generate(@all_examples))
232
+ File.write(file_name, JSON.generate(@all_examples))
233
+ end
234
+
235
+ def write_flaky_examples_report
236
+ file_name = File.join(@cache_dir, 'flaky_examples.json')
237
+
238
+ File.write(file_name, JSON.generate(@flaky_examples.to_a))
210
239
  end
211
240
 
212
241
  def write_failed_examples_report
213
242
  file_name = File.join(@cache_dir, 'failed_examples.json')
214
243
 
215
- File.write(file_name, JSON.pretty_generate(@failed_examples.to_a))
244
+ File.write(file_name, JSON.generate(@failed_examples.to_a))
216
245
  end
217
246
 
218
247
  def write_pending_examples_report
219
248
  file_name = File.join(@cache_dir, 'pending_examples.json')
220
249
 
221
- File.write(file_name, JSON.pretty_generate(@pending_examples.to_a))
250
+ File.write(file_name, JSON.generate(@pending_examples.to_a))
222
251
  end
223
252
 
224
253
  def write_all_files_report
225
254
  file_name = File.join(@cache_dir, 'all_files.json')
226
255
 
227
- File.write(file_name, JSON.pretty_generate(@all_files))
256
+ File.write(file_name, JSON.generate(@all_files))
228
257
  end
229
258
 
230
259
  def write_dependency_report
231
260
  file_name = File.join(@cache_dir, 'dependency.json')
232
261
 
233
- File.write(file_name, JSON.pretty_generate(@dependency))
262
+ File.write(file_name, JSON.generate(@dependency))
234
263
  end
235
264
 
236
265
  def write_reverse_dependency_report
237
266
  file_name = File.join(@cache_dir, 'reverse_dependency.json')
238
267
 
239
- File.write(file_name, JSON.pretty_generate(@reverse_dependency))
268
+ File.write(file_name, JSON.generate(@reverse_dependency))
240
269
  end
241
270
 
242
271
  def write_examples_coverage_report
243
272
  file_name = File.join(@cache_dir, 'examples_coverage.json')
244
273
 
245
- File.write(file_name, JSON.pretty_generate(@examples_coverage))
274
+ File.write(file_name, JSON.generate(@examples_coverage))
246
275
  end
247
276
 
248
277
  def write_last_run_report
249
278
  file_name = File.join(RSpecTracer.cache_path, 'last_run.json')
250
279
  last_run_data = @last_run.merge(run_id: @run_id, timestamp: Time.now.utc)
251
280
 
252
- File.write(file_name, JSON.pretty_generate(last_run_data))
281
+ File.write(file_name, JSON.generate(last_run_data))
253
282
  end
254
283
  end
255
284
  end