rspecq 0.3.0 → 0.7.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 89dbfa98d1eaceb06c39d41ab85e7fa6923d0c87e9a15b9cbfaf7399ff2aaff3
4
- data.tar.gz: b7cd028440e6eb03401dc623c7ee0fc0fe74f6ffa12a25ecc23d0cf54e6acd1e
3
+ metadata.gz: e8bcc40eeeccb4d94e795f524f7f6bce73454a25ebb1907c58523e7e775740a7
4
+ data.tar.gz: 93d9aa04b32ef60c430c505fb7cd4d147ca2e106be442ebd5f6bd18221db4841
5
5
  SHA512:
6
- metadata.gz: a43f0630e8a02a001132f45c9f68cacf7edae8e90487112e640eb611e7d1345f68ad0ab163ae01c91cb38ebc879e98cda9b54044c0ee676293c7aa3bf7c17942
7
- data.tar.gz: bf98027dc02ac56d02cc258700f5efa766c40d4d69c55c529d311083965d1fe4f76423b05fddb35a37496bb6eb3c11a8460a8e76f94897c4bb38a744b2fb40df
6
+ metadata.gz: 80b3eea41c65a3f759c895c0aaf9895c67b4fd2d4c57acb3b8d193fa2522e654735eed33f96a1800a34d5471e5ed83768c2d2c1a31c8a6975c16673f3aaffa79
7
+ data.tar.gz: dfe2e954fc0d83be7da5bd4c3b24c236950ac6f088ec6a46284954c5e49609c8dea6e15222ccbf5d9d1be353f28671a61e68a04d90bd4b19fb791b2acae685fd
data/CHANGELOG.md CHANGED
@@ -4,6 +4,39 @@ Breaking changes are prefixed with a "[BREAKING]" label.
4
4
 
5
5
  ## master (unreleased)
6
6
 
7
+ ## 0.7.1 (2021-04-08)
8
+
9
+ - New env variable RSPECQ_REPORTER_RERUN_COMMAND_SKIP. When set, the reporter
10
+ does not include the flaky test's rerun command.
11
+
12
+ ## 0.7.0 (2021-04-01)
13
+
14
+ - New cli parameter `reproduction`.
15
+ When passed, primary worker publishes the queue in the same order as passed
16
+ in the command.
17
+ - Reporter now includes a reproduction command for flaky tests.
18
+
19
+ ## 0.6.0 (2021-03-23)
20
+
21
+ - New cli parameter `seed`.
22
+ The seed is passed to the RSpec command.
23
+
24
+ ## 0.5.0 (2021-02-05)
25
+
26
+ ### Added
27
+
28
+ - New cli parameter `queue_wait_timeout`.
29
+ It configured the time a queue can wait to be ready. The env equivalent
30
+ is `RSPECQ_QUEUE_WAIT_TIMEOUT`. [#51](https://github.com/skroutz/rspecq/pull/51)
31
+
32
+ ## 0.4.0 (2020-10-07)
33
+
34
+ ### Added
35
+
36
+ - Builds can be configured to terminate after a specified number of failures,
37
+ using the `--fail-fast` option.
38
+
39
+
7
40
  ## 0.3.0 (2020-10-05)
8
41
 
9
42
  ### Added
@@ -19,7 +52,7 @@ Breaking changes are prefixed with a "[BREAKING]" label.
19
52
  ## 0.2.2 (2020-09-10)
20
53
 
21
54
  ### Fixed
22
- - Worker would fail if application code was writing to stderr
55
+ - Worker would fail if application code was writing to stderr
23
56
  [[#35](https://github.com/skroutz/rspecq/pull/35)]
24
57
 
25
58
  ## 0.2.1 (2020-09-09)
data/README.md CHANGED
@@ -26,8 +26,8 @@ and [ci-queue](https://github.com/Shopify/ci-queue).
26
26
  - Handles intermittent worker failures (e.g. network hiccups, faulty hardware etc.)
27
27
  by detecting non-responsive workers and requeing their jobs. See [*Worker failures*](#worker-failures)
28
28
  - Sentry integration for monitoring build-level events. See [*Sentry integration*](#sentry-integration).
29
- - [PLANNED] StatsD integration for various build-level metrics and insights.
30
29
  See [#2](https://github.com/skroutz/rspecq/issues/2).
30
+ - Automatic termination of builds after a certain amount of failures. See [*Fail-fast*](#fail-fast).
31
31
 
32
32
  ## Usage
33
33
 
@@ -64,6 +64,7 @@ USAGE:
64
64
  OPTIONS:
65
65
  -b, --build ID A unique identifier for the build. Should be common among workers participating in the same build.
66
66
  -w, --worker ID An identifier for the worker. Workers participating in the same build should have distinct IDs.
67
+ --seed SEED The RSpec seed. Passing the seed can be helpful in many ways i.e reproduction and testing.
67
68
  -r, --redis HOST --redis is deprecated. Use --redis-host or --redis-url instead. Redis host to connect to (default: 127.0.0.1).
68
69
  --redis-host HOST Redis host to connect to (default: 127.0.0.1).
69
70
  --redis-url URL Redis URL to connect to (e.g.: redis://127.0.0.1:6379/0).
@@ -73,10 +74,37 @@ OPTIONS:
73
74
  Exits with a non-zero status code if there were any failures.
74
75
  --report-timeout N Fail if build is not finished after N seconds. Only applicable if --report is enabled (default: 3600).
75
76
  --max-requeues N Retry failed examples up to N times before considering them legit failures (default: 3).
77
+ --queue-wait-timeout N Time to wait for a queue to be ready before considering it failed (default: 30).
78
+ --fail-fast N Abort build with a non-zero status code after N failed examples.
79
+ --reproduction Enable reproduction mode: Publish files and examples in the exact order given in the command. Incompatible with --timings.
76
80
  -h, --help Show this message.
77
81
  -v, --version Print the version and exit.
78
82
  ```
79
83
 
84
+ You can set most options using ENV variables:
85
+
86
+ ```shell
87
+ $ RSPECQ_BUILD=123 RSPECQ_WORKDER=foo1 rspecq spec/
88
+ ```
89
+
90
+ ### Supported ENV variables
91
+
92
+ | Name | Desc |
93
+ | --- | --- |
94
+ | `RSPECQ_BUILD` | Build ID |
95
+ | `RSPECQ_WORKER` | Worker ID |
96
+ | `RSPECQ_SEED` | RSpec seed |
97
+ | `RSPECQ_REDIS` | Redis HOST |
98
+ | `RSPECQ_UPDATE_TIMINGS` | Timings |
99
+ | `RSPECQ_FILE_SPLIT_THRESHOLD` | File split threshold |
100
+ | `RSPECQ_REPORT` | Report |
101
+ | `RSPECQ_REPORT_TIMEOUT` | Report Timeout |
102
+ | `RSPECQ_MAX_REQUEUES` | Max requests |
103
+ | `RSPECQ_QUEUE_WAIT_TIMEOUT` | Queue wait timeout |
104
+ | `RSPECQ_REDIS_URL` | Redis URL |
105
+ | `RSPECQ_FAIL_FAST` | Fail fast |
106
+ | `RSPECQ_REPORTER_RERUN_COMMAND_SKIP` | Do not report flaky test's rerun command |
107
+
80
108
  ### Sentry integration
81
109
 
82
110
  RSpecQ can optionally emit build events to a
@@ -88,7 +116,6 @@ This is convenient for monitoring important warnings/errors that may impact
88
116
  build times, such as the fact that no previous timings were found and
89
117
  therefore job scheduling was effectively random for a particular build.
90
118
 
91
-
92
119
  ## How it works
93
120
 
94
121
  The core design is almost identical to ci-queue so please refer to its
@@ -115,7 +142,7 @@ For example, a single file may need 10 minutes to run while all other
115
142
  files finish after 8 minutes. This would cause all but one workers to be
116
143
  sitting idle for 2 minutes.
117
144
 
118
- To overcome this issue, RSpecQ can splits files which their execution time is
145
+ To overcome this issue, RSpecQ can split files which their execution time is
119
146
  above a certain threshold (set with the `--file-split-threshold` option)
120
147
  and instead schedule them as individual examples.
121
148
 
@@ -133,6 +160,16 @@ final report.
133
160
  Flaky tests are also detected and printed as such in the final report. They are
134
161
  also emitted to Sentry (see [Sentry integration](#sentry-integration)).
135
162
 
163
+ ### Fail-fast
164
+
165
+ In order to prevent large suites running for a long time with a lot of
166
+ failures, a threshold can be set to control the number of failed examples that
167
+ will render the build unsuccessful. This is in par with RSpec's
168
+ [--fail-fast](https://relishapp.com/rspec/rspec-core/docs/command-line/fail-fast-option).
169
+
170
+ This feature is disabled by default, and can be controlled via the
171
+ `--fail-fast` command line option.
172
+
136
173
  ### Worker failures
137
174
 
138
175
  It's not uncommon for CI processes to encounter unrecoverable failures for
@@ -204,6 +241,13 @@ To enable verbose output in the tests:
204
241
  $ RSPECQ_DEBUG=1 bundle exec rake
205
242
  ```
206
243
 
244
+ ## Redis
245
+
246
+ RSpecQ by design doesn't expire its keys from Redis. It is left to the user
247
+ to configure the Redis server to do so; see
248
+ [Using Redis as an LRU cache](https://redis.io/topics/lru-cache) for more info.
249
+
250
+ You can do this from a configuration file or with `redis-cli`.
207
251
 
208
252
  ## License
209
253
 
data/Rakefile CHANGED
@@ -2,7 +2,7 @@ require "rake/testtask"
2
2
 
3
3
  Rake::TestTask.new do |t|
4
4
  t.libs << "test"
5
- t.test_files = FileList['test/test_*.rb']
5
+ t.test_files = FileList["test/test_*.rb"]
6
6
  t.verbose = true
7
7
  end
8
8
 
data/bin/rspecq CHANGED
@@ -2,9 +2,11 @@
2
2
  require "optparse"
3
3
  require "rspecq"
4
4
 
5
- DEFAULT_REDIS_HOST = "127.0.0.1"
5
+ DEFAULT_REDIS_HOST = "127.0.0.1".freeze
6
6
  DEFAULT_REPORT_TIMEOUT = 3600 # 1 hour
7
7
  DEFAULT_MAX_REQUEUES = 3
8
+ DEFAULT_QUEUE_WAIT_TIMEOUT = 30
9
+ DEFAULT_FAIL_FAST = 0
8
10
 
9
11
  def env_set?(var)
10
12
  ["1", "true"].include?(ENV[var])
@@ -36,9 +38,14 @@ OptionParser.new do |o|
36
38
  opts[:worker] = v
37
39
  end
38
40
 
41
+ o.on("--seed SEED", "The RSpec seed. Passing the seed can be helpful in " \
42
+ "many ways i.e reproduction and testing.") do |v|
43
+ opts[:seed] = v
44
+ end
45
+
39
46
  o.on("-r", "--redis HOST", "Redis host to connect to " \
40
47
  "(default: #{DEFAULT_REDIS_HOST}).") do |v|
41
- puts "--redis is deprecated. Use --redis-host or --redis-url instead"
48
+ puts "DEPRECATION: --redis is deprecated. Use --redis-host or --redis-url instead"
42
49
  opts[:redis_host] = v
43
50
  end
44
51
 
@@ -65,7 +72,7 @@ OptionParser.new do |o|
65
72
 
66
73
  o.on("--report", "Enable reporter mode: do not pull tests off the queue; " \
67
74
  "instead print build progress and exit when it's " \
68
- "finished.\n#{o.summary_indent*9} " \
75
+ "finished.\n#{o.summary_indent * 9} " \
69
76
  "Exits with a non-zero status code if there were any " \
70
77
  "failures.") do |v|
71
78
  opts[:report] = v
@@ -83,6 +90,23 @@ OptionParser.new do |o|
83
90
  opts[:max_requeues] = v
84
91
  end
85
92
 
93
+ o.on("--queue-wait-timeout N", Integer, "Time to wait for a queue to be " \
94
+ "ready before considering it failed " \
95
+ "(default: #{DEFAULT_QUEUE_WAIT_TIMEOUT}).") do |v|
96
+ opts[:queue_wait_timeout] = v
97
+ end
98
+
99
+ o.on("--fail-fast N", Integer, "Abort build with a non-zero status code " \
100
+ "after N failed examples.") do |v|
101
+ opts[:fail_fast] = v
102
+ end
103
+
104
+ o.on("--reproduction", "Enable reproduction mode: run rspec on the given files " \
105
+ "and examples in the exact order they are given. Incompatible with " \
106
+ "--timings.") do |v|
107
+ opts[:reproduction] = v
108
+ end
109
+
86
110
  o.on_tail("-h", "--help", "Show this message.") do
87
111
  puts o
88
112
  exit
@@ -96,16 +120,22 @@ end.parse!
96
120
 
97
121
  opts[:build] ||= ENV["RSPECQ_BUILD"]
98
122
  opts[:worker] ||= ENV["RSPECQ_WORKER"]
123
+ opts[:seed] ||= ENV["RSPECQ_SEED"]
99
124
  opts[:redis_host] ||= ENV["RSPECQ_REDIS"] || DEFAULT_REDIS_HOST
100
125
  opts[:timings] ||= env_set?("RSPECQ_UPDATE_TIMINGS")
101
- opts[:file_split_threshold] ||= Integer(ENV["RSPECQ_FILE_SPLIT_THRESHOLD"] || 9999999)
126
+ opts[:file_split_threshold] ||= Integer(ENV["RSPECQ_FILE_SPLIT_THRESHOLD"] || 9_999_999)
102
127
  opts[:report] ||= env_set?("RSPECQ_REPORT")
103
128
  opts[:report_timeout] ||= Integer(ENV["RSPECQ_REPORT_TIMEOUT"] || DEFAULT_REPORT_TIMEOUT)
104
129
  opts[:max_requeues] ||= Integer(ENV["RSPECQ_MAX_REQUEUES"] || DEFAULT_MAX_REQUEUES)
130
+ opts[:queue_wait_timeout] ||= Integer(ENV["RSPECQ_QUEUE_WAIT_TIMEOUT"] || DEFAULT_QUEUE_WAIT_TIMEOUT)
105
131
  opts[:redis_url] ||= ENV["RSPECQ_REDIS_URL"]
132
+ opts[:fail_fast] ||= Integer(ENV["RSPECQ_FAIL_FAST"] || DEFAULT_FAIL_FAST)
133
+ opts[:reproduction] ||= env_set?("RSPECQ_REPRODUCTION")
106
134
 
135
+ # rubocop:disable Style/RaiseArgs, Layout/EmptyLineAfterGuardClause
107
136
  raise OptionParser::MissingArgument.new(:build) if opts[:build].nil?
108
137
  raise OptionParser::MissingArgument.new(:worker) if !opts[:report] && opts[:worker].nil?
138
+ # rubocop:enable Style/RaiseArgs, Layout/EmptyLineAfterGuardClause
109
139
 
110
140
  redis_opts = {}
111
141
 
@@ -120,6 +150,7 @@ if opts[:report]
120
150
  build_id: opts[:build],
121
151
  timeout: opts[:report_timeout],
122
152
  redis_opts: redis_opts,
153
+ queue_wait_timeout: opts[:queue_wait_timeout]
123
154
  )
124
155
 
125
156
  reporter.report
@@ -130,9 +161,13 @@ else
130
161
  redis_opts: redis_opts
131
162
  )
132
163
 
133
- worker.files_or_dirs_to_run = ARGV[0] if ARGV[0]
164
+ worker.files_or_dirs_to_run = ARGV if ARGV.any?
134
165
  worker.populate_timings = opts[:timings]
135
166
  worker.file_split_threshold = opts[:file_split_threshold]
136
167
  worker.max_requeues = opts[:max_requeues]
168
+ worker.queue_wait_timeout = opts[:queue_wait_timeout]
169
+ worker.fail_fast = opts[:fail_fast]
170
+ worker.seed = Integer(opts[:seed]) if opts[:seed]
171
+ worker.reproduction = opts[:reproduction]
137
172
  worker.work
138
173
  end
@@ -1,12 +1,18 @@
1
1
  module RSpecQ
2
2
  module Formatters
3
+ # Persists failed examples information (i.e. message and backtrace), so
4
+ # that they can be reported to the end user by the Reporter.
5
+ #
6
+ # Also persists non-example error information (e.g. a syntax error that
7
+ # in a spec file).
3
8
  class FailureRecorder
4
- def initialize(queue, job, max_requeues)
9
+ def initialize(queue, job, max_requeues, worker_id)
5
10
  @queue = queue
6
11
  @job = job
7
12
  @colorizer = RSpec::Core::Formatters::ConsoleCodes
8
13
  @non_example_error_recorded = false
9
14
  @max_requeues = max_requeues
15
+ @worker_id = worker_id
10
16
  end
11
17
 
12
18
  # Here we're notified about errors occuring outside of examples.
@@ -25,7 +31,7 @@ module RSpecQ
25
31
  def example_failed(notification)
26
32
  example = notification.example
27
33
 
28
- if @queue.requeue_job(example.id, @max_requeues)
34
+ if @queue.requeue_job(example, @max_requeues, @worker_id)
29
35
  # HACK: try to avoid picking the job we just requeued; we want it
30
36
  # to be picked up by a different worker
31
37
  sleep 0.5
@@ -33,16 +39,19 @@ module RSpecQ
33
39
  end
34
40
 
35
41
  presenter = RSpec::Core::Formatters::ExceptionPresenter.new(
36
- example.exception, example)
42
+ example.exception, example
43
+ )
37
44
 
38
45
  msg = presenter.fully_formatted(nil, @colorizer)
39
46
  msg << "\n"
40
47
  msg << @colorizer.wrap(
41
- "bin/rspec #{example.location_rerun_argument}",
42
- RSpec.configuration.failure_color)
48
+ "bin/rspec --seed #{RSpec.configuration.seed} #{example.location_rerun_argument}",
49
+ RSpec.configuration.failure_color
50
+ )
43
51
 
44
52
  msg << @colorizer.wrap(
45
- " # #{example.full_description}", RSpec.configuration.detail_color)
53
+ " # #{example.full_description}", RSpec.configuration.detail_color
54
+ )
46
55
 
47
56
  @queue.record_example_failure(notification.example.id, msg)
48
57
  end
@@ -1,5 +1,8 @@
1
1
  module RSpecQ
2
2
  module Formatters
3
+ # Persists each job's timing (in seconds). Those timings are used when
4
+ # determining the ordering in which jobs are scheduled (slower jobs will
5
+ # be enqueued first).
3
6
  class JobTimingRecorder
4
7
  def initialize(queue, job)
5
8
  @queue = queue
@@ -14,4 +14,3 @@ module RSpecQ
14
14
  end
15
15
  end
16
16
  end
17
-
data/lib/rspecq/queue.rb CHANGED
@@ -51,8 +51,12 @@ module RSpecQ
51
51
  REQUEUE_JOB = <<~LUA.freeze
52
52
  local key_queue_unprocessed = KEYS[1]
53
53
  local key_requeues = KEYS[2]
54
+ local key_requeued_job_original_worker = KEYS[3]
55
+ local key_job_location = KEYS[4]
54
56
  local job = ARGV[1]
55
57
  local max_requeues = ARGV[2]
58
+ local original_worker = ARGV[3]
59
+ local location = ARGV[4]
56
60
 
57
61
  local requeued_times = redis.call('hget', key_requeues, job)
58
62
  if requeued_times and requeued_times >= max_requeues then
@@ -60,7 +64,9 @@ module RSpecQ
60
64
  end
61
65
 
62
66
  redis.call('lpush', key_queue_unprocessed, job)
67
+ redis.call('hset', key_requeued_job_original_worker, job, original_worker)
63
68
  redis.call('hincrby', key_requeues, job, 1)
69
+ redis.call('hset', key_job_location, job, location)
64
70
 
65
71
  return true
66
72
  LUA
@@ -77,8 +83,9 @@ module RSpecQ
77
83
  end
78
84
 
79
85
  # NOTE: jobs will be processed from head to tail (lpop)
80
- def publish(jobs)
86
+ def publish(jobs, fail_fast = 0)
81
87
  @redis.multi do
88
+ @redis.hset(key_queue_config, "fail_fast", fail_fast)
82
89
  @redis.rpush(key_queue_unprocessed, jobs)
83
90
  @redis.set(key_queue_status, STATUS_READY)
84
91
  end.first
@@ -116,6 +123,7 @@ module RSpecQ
116
123
  @redis.multi do
117
124
  @redis.hdel(key_queue_running, @worker_id)
118
125
  @redis.sadd(key_queue_processed, job)
126
+ @redis.rpush(key("queue", "jobs_per_worker", @worker_id), job)
119
127
  end
120
128
  end
121
129
 
@@ -124,16 +132,41 @@ module RSpecQ
124
132
  #
125
133
  # Returns nil if the job hit the requeue limit and therefore was not
126
134
  # requeued and should be considered a failure.
127
- def requeue_job(job, max_requeues)
135
+ def requeue_job(example, max_requeues, original_worker_id)
128
136
  return false if max_requeues.zero?
129
137
 
138
+ job = example.id
139
+ location = example.location_rerun_argument
140
+
130
141
  @redis.eval(
131
142
  REQUEUE_JOB,
132
- keys: [key_queue_unprocessed, key_requeues],
133
- argv: [job, max_requeues],
143
+ keys: [key_queue_unprocessed, key_requeues, key("requeued_job_original_worker"), key("job_location")],
144
+ argv: [job, max_requeues, original_worker_id, location]
134
145
  )
135
146
  end
136
147
 
148
+ def save_worker_seed(worker, seed)
149
+ @redis.hset(key("worker_seed"), worker, seed)
150
+ end
151
+
152
+ def job_location(job)
153
+ @redis.hget(key("job_location"), job)
154
+ end
155
+
156
+ def failed_job_worker(job)
157
+ redis.hget(key("requeued_job_original_worker"), job)
158
+ end
159
+
160
+ def job_rerun_command(job)
161
+ worker = failed_job_worker(job)
162
+ jobs = redis.lrange(key("queue", "jobs_per_worker", worker), 0, -1)
163
+ seed = redis.hget(key("worker_seed"), worker)
164
+
165
+ "DISABLE_SPRING=1 DISABLE_BOOTSNAP=1 bin/rspecq --build 1 " \
166
+ "--worker foo --seed #{seed} --max-requeues 0 --fail-fast 1 " \
167
+ "--reproduction #{jobs.join(' ')}"
168
+ end
169
+
137
170
  def record_example_failure(example_id, message)
138
171
  @redis.hset(key_failures, example_id, message)
139
172
  end
@@ -209,9 +242,10 @@ module RSpecQ
209
242
  @redis.get(key_queue_status) == STATUS_READY
210
243
  end
211
244
 
212
- def wait_until_published(timeout=30)
245
+ def wait_until_published(timeout = 30)
213
246
  (timeout * 10).times do
214
247
  return if published?
248
+
215
249
  sleep 0.1
216
250
  end
217
251
 
@@ -232,7 +266,9 @@ module RSpecQ
232
266
  # after being retried). Must be called after the build is complete,
233
267
  # otherwise an exception will be raised.
234
268
  def flaky_jobs
235
- raise "Queue is not yet exhausted" if !exhausted?
269
+ if !exhausted? && !build_failed_fast?
270
+ raise "Queue is not yet exhausted"
271
+ end
236
272
 
237
273
  requeued = @redis.hkeys(key_requeues)
238
274
 
@@ -241,11 +277,38 @@ module RSpecQ
241
277
  requeued - @redis.hkeys(key_failures)
242
278
  end
243
279
 
280
+ # Returns the number of failures that will trigger the build to fail-fast.
281
+ # Returns 0 if this feature is disabled and nil if the Queue is not yet
282
+ # published
283
+ def fail_fast
284
+ return nil unless published?
285
+
286
+ @fail_fast ||= Integer(@redis.hget(key_queue_config, "fail_fast"))
287
+ end
288
+
289
+ # Returns true if the number of failed tests, has surpassed the threshold
290
+ # to render the run unsuccessful and the build should be terminated.
291
+ def build_failed_fast?
292
+ if fail_fast.nil? || fail_fast.zero?
293
+ return false
294
+ end
295
+
296
+ @redis.multi do
297
+ @redis.hlen(key_failures)
298
+ @redis.hlen(key_errors)
299
+ end.inject(:+) >= fail_fast
300
+ end
301
+
244
302
  # redis: STRING [STATUS_INITIALIZING, STATUS_READY]
245
303
  def key_queue_status
246
304
  key("queue", "status")
247
305
  end
248
306
 
307
+ # redis: HASH<config_key => config_value>
308
+ def key_queue_config
309
+ key("queue", "config")
310
+ end
311
+
249
312
  # redis: LIST<job>
250
313
  def key_queue_unprocessed
251
314
  key("queue", "unprocessed")
@@ -9,18 +9,19 @@ module RSpecQ
9
9
  #
10
10
  # Reporters are readers of the queue.
11
11
  class Reporter
12
- def initialize(build_id:, timeout:, redis_opts:)
12
+ def initialize(build_id:, timeout:, redis_opts:, queue_wait_timeout: 30)
13
13
  @build_id = build_id
14
14
  @timeout = timeout
15
15
  @queue = Queue.new(build_id, "reporter", redis_opts)
16
+ @queue_wait_timeout = queue_wait_timeout
16
17
 
17
18
  # We want feedback to be immediattely printed to CI users, so
18
19
  # we disable buffering.
19
- STDOUT.sync = true
20
+ $stdout.sync = true
20
21
  end
21
22
 
22
23
  def report
23
- @queue.wait_until_published
24
+ @queue.wait_until_published(@queue_wait_timeout)
24
25
 
25
26
  finished = false
26
27
 
@@ -28,7 +29,7 @@ module RSpecQ
28
29
  failure_heading_printed = false
29
30
 
30
31
  tests_duration = measure_duration do
31
- @timeout.times do |i|
32
+ @timeout.times do
32
33
  @queue.example_failures.each do |job, rspec_output|
33
34
  next if reported_failures[job]
34
35
 
@@ -41,7 +42,7 @@ module RSpecQ
41
42
  puts failure_formatted(rspec_output)
42
43
  end
43
44
 
44
- if !@queue.exhausted?
45
+ unless @queue.exhausted? || @queue.build_failed_fast?
45
46
  sleep 1
46
47
  next
47
48
  end
@@ -83,6 +84,13 @@ module RSpecQ
83
84
  end
84
85
 
85
86
  summary = ""
87
+ if @queue.build_failed_fast?
88
+ summary << "\n\n"
89
+ summary << "The limit of #{@queue.fail_fast} failures has been reached\n"
90
+ summary << "Aborting..."
91
+ summary << "\n"
92
+ end
93
+
86
94
  summary << failed_examples_section if !failures.empty?
87
95
 
88
96
  errors.each { |_job, msg| summary << msg }
@@ -99,7 +107,16 @@ module RSpecQ
99
107
  if !flaky_jobs.empty?
100
108
  summary << "\n\n"
101
109
  summary << "Flaky jobs detected (count=#{flaky_jobs.count}):\n"
102
- flaky_jobs.each { |j| summary << " #{j}\n" }
110
+ flaky_jobs.each do |j|
111
+ summary << RSpec::Core::Formatters::ConsoleCodes.wrap(
112
+ "#{@queue.job_location(j)} @ #{@queue.failed_job_worker(j)}\n",
113
+ RSpec.configuration.pending_color
114
+ )
115
+
116
+ next if ENV["RSPECQ_REPORTER_RERUN_COMMAND_SKIP"]
117
+
118
+ summary << "#{@queue.job_rerun_command(j)}\n\n\n"
119
+ end
103
120
  end
104
121
 
105
122
  summary
@@ -117,16 +134,15 @@ module RSpecQ
117
134
  return if jobs.empty?
118
135
 
119
136
  jobs.each do |job|
120
- filename = job.sub(/\[.+\]/, '')
137
+ filename = job.sub(/\[.+\]/, "")[%r{spec/.+}].split(":")[0]
121
138
 
122
139
  extra = {
123
140
  build: @build_id,
124
141
  build_timeout: @timeout,
125
- queue: @queue.inspect,
126
- object: self.inspect,
127
- pid: Process.pid,
128
- job_path: job,
129
- build_duration: build_duration
142
+ build_duration: build_duration,
143
+ location: @queue.job_location(job),
144
+ rerun_command: @queue.job_rerun_command(job),
145
+ worker: @queue.failed_job_worker(job)
130
146
  }
131
147
 
132
148
  tags = {
@@ -136,7 +152,7 @@ module RSpecQ
136
152
 
137
153
  Raven.capture_message(
138
154
  "Flaky test in #{filename}",
139
- level: 'warning',
155
+ level: "warning",
140
156
  extra: extra,
141
157
  tags: tags
142
158
  )
@@ -1,3 +1,3 @@
1
1
  module RSpecQ
2
- VERSION = "0.3.0".freeze
2
+ VERSION = "0.7.1".freeze
3
3
  end
data/lib/rspecq/worker.rb CHANGED
@@ -40,17 +40,39 @@ module RSpecQ
40
40
  # Defaults to 3
41
41
  attr_accessor :max_requeues
42
42
 
43
+ # Stop the execution after N failed tests. Do not stop at any point when
44
+ # set to 0.
45
+ #
46
+ # Defaults to 0
47
+ attr_accessor :fail_fast
48
+
49
+ # Time to wait for a queue to be published.
50
+ #
51
+ # Defaults to 30
52
+ attr_accessor :queue_wait_timeout
53
+
54
+ # The RSpec seed
55
+ attr_accessor :seed
56
+
57
+ # Reproduction flag. If true, worker will publish files in the exact order
58
+ # given in the command.
59
+ attr_accessor :reproduction
60
+
43
61
  attr_reader :queue
44
62
 
45
63
  def initialize(build_id:, worker_id:, redis_opts:)
46
64
  @build_id = build_id
47
65
  @worker_id = worker_id
48
66
  @queue = Queue.new(build_id, worker_id, redis_opts)
67
+ @fail_fast = 0
49
68
  @files_or_dirs_to_run = "spec"
50
69
  @populate_timings = false
51
- @file_split_threshold = 999999
70
+ @file_split_threshold = 999_999
52
71
  @heartbeat_updated_at = nil
53
72
  @max_requeues = 3
73
+ @queue_wait_timeout = 30
74
+ @seed = srand && srand % 0xFFFF
75
+ @reproduction = false
54
76
 
55
77
  RSpec::Core::Formatters.register(Formatters::JobTimingRecorder, :dump_summary)
56
78
  RSpec::Core::Formatters.register(Formatters::ExampleCountRecorder, :dump_summary)
@@ -62,13 +84,16 @@ module RSpecQ
62
84
  puts "Working for build #{@build_id} (worker=#{@worker_id})"
63
85
 
64
86
  try_publish_queue!(queue)
65
- queue.wait_until_published
87
+ queue.wait_until_published(queue_wait_timeout)
88
+ queue.save_worker_seed(@worker_id, seed)
66
89
 
67
90
  loop do
68
91
  # we have to bootstrap this so that it can be used in the first call
69
92
  # to `requeue_lost_job` inside the work loop
70
93
  update_heartbeat
71
94
 
95
+ return if queue.build_failed_fast?
96
+
72
97
  lost = queue.requeue_lost_job
73
98
  puts "Requeued lost job: #{lost}" if lost
74
99
 
@@ -88,9 +113,9 @@ module RSpecQ
88
113
 
89
114
  # reconfigure rspec
90
115
  RSpec.configuration.detail_color = :magenta
91
- RSpec.configuration.seed = srand && srand % 0xFFFF
92
- RSpec.configuration.backtrace_formatter.filter_gem('rspecq')
93
- RSpec.configuration.add_formatter(Formatters::FailureRecorder.new(queue, job, max_requeues))
116
+ RSpec.configuration.seed = seed
117
+ RSpec.configuration.backtrace_formatter.filter_gem("rspecq")
118
+ RSpec.configuration.add_formatter(Formatters::FailureRecorder.new(queue, job, max_requeues, @worker_id))
94
119
  RSpec.configuration.add_formatter(Formatters::ExampleCountRecorder.new(queue))
95
120
  RSpec.configuration.add_formatter(Formatters::WorkerHeartbeatRecorder.new(self))
96
121
 
@@ -116,12 +141,21 @@ module RSpecQ
116
141
  def try_publish_queue!(queue)
117
142
  return if !queue.become_master
118
143
 
144
+ if reproduction
145
+ q_size = queue.publish(files_or_dirs_to_run, fail_fast)
146
+ log_event(
147
+ "Reproduction mode. Published queue as given (size=#{q_size})",
148
+ "info"
149
+ )
150
+ return
151
+ end
152
+
119
153
  RSpec.configuration.files_or_directories_to_run = files_or_dirs_to_run
120
154
  files_to_run = RSpec.configuration.files_to_run.map { |j| relative_path(j) }
121
155
 
122
156
  timings = queue.timings
123
157
  if timings.empty?
124
- q_size = queue.publish(files_to_run.shuffle)
158
+ q_size = queue.publish(files_to_run.shuffle, fail_fast)
125
159
  log_event(
126
160
  "No timings found! Published queue in random order (size=#{q_size})",
127
161
  "warning"
@@ -146,7 +180,7 @@ module RSpecQ
146
180
  jobs.concat(files_to_run)
147
181
  end
148
182
 
149
- default_timing = timings.values[timings.values.size/2]
183
+ default_timing = timings.values[timings.values.size / 2]
150
184
 
151
185
  # assign timings (based on previous runs) to all jobs
152
186
  jobs = jobs.each_with_object({}) do |j, h|
@@ -160,7 +194,7 @@ module RSpecQ
160
194
  # sort jobs based on their timings (slowest to be processed first)
161
195
  jobs = jobs.sort_by { |_j, t| -t }.map(&:first)
162
196
 
163
- puts "Published queue (size=#{queue.publish(jobs)})"
197
+ puts "Published queue (size=#{queue.publish(jobs, fail_fast)})"
164
198
  end
165
199
 
166
200
  private
@@ -171,7 +205,8 @@ module RSpecQ
171
205
  # see https://github.com/rspec/rspec-core/pull/2723
172
206
  if Gem::Version.new(RSpec::Core::Version::STRING) <= Gem::Version.new("3.9.1")
173
207
  RSpec.world.instance_variable_set(
174
- :@example_group_counts_by_spec_file, Hash.new(0))
208
+ :@example_group_counts_by_spec_file, Hash.new(0)
209
+ )
175
210
  end
176
211
 
177
212
  # RSpec.clear_examples does not reset those, which causes issues when
@@ -195,17 +230,17 @@ module RSpecQ
195
230
 
196
231
  if !cmd_result.success?
197
232
  rspec_output = begin
198
- JSON.parse(out)
199
- rescue JSON::ParserError
200
- out
201
- end
233
+ JSON.parse(out)
234
+ rescue JSON::ParserError
235
+ out
236
+ end
202
237
 
203
238
  log_event(
204
239
  "Failed to split slow files, falling back to regular scheduling.\n #{err}",
205
240
  "error",
206
241
  rspec_stdout: rspec_output,
207
242
  rspec_stderr: err,
208
- cmd_result: cmd_result.inspect,
243
+ cmd_result: cmd_result.inspect
209
244
  )
210
245
 
211
246
  pp rspec_output
@@ -227,7 +262,7 @@ module RSpecQ
227
262
 
228
263
  # Prints msg to standard output and emits an event to Sentry, if the
229
264
  # SENTRY_DSN environment variable is set.
230
- def log_event(msg, level, additional={})
265
+ def log_event(msg, level, additional = {})
231
266
  puts msg
232
267
 
233
268
  Raven.capture_message(msg, level: level, extra: {
@@ -238,8 +273,8 @@ module RSpecQ
238
273
  populate_timings: populate_timings,
239
274
  file_split_threshold: file_split_threshold,
240
275
  heartbeat_updated_at: @heartbeat_updated_at,
241
- object: self.inspect,
242
- pid: Process.pid,
276
+ object: inspect,
277
+ pid: Process.pid
243
278
  }.merge(additional))
244
279
  end
245
280
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspecq
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Agis Anastasopoulos
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-05 00:00:00.000000000 Z
11
+ date: 2021-04-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec-core
@@ -53,7 +53,7 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: rake
56
+ name: minitest
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
@@ -81,7 +81,7 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: minitest
84
+ name: rake
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - ">="
@@ -108,6 +108,20 @@ dependencies:
108
108
  - - ">="
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 0.93.0
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 0.93.0
111
125
  description:
112
126
  email: agis.anast@gmail.com
113
127
  executables:
@@ -149,7 +163,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
149
163
  - !ruby/object:Gem::Version
150
164
  version: '0'
151
165
  requirements: []
152
- rubygems_version: 3.1.4
166
+ rubyforge_project:
167
+ rubygems_version: 2.7.6.2
153
168
  signing_key:
154
169
  specification_version: 4
155
170
  summary: Optimally distribute and run RSpec suites among parallel workers; for faster