rspecq 0.3.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 89dbfa98d1eaceb06c39d41ab85e7fa6923d0c87e9a15b9cbfaf7399ff2aaff3
4
- data.tar.gz: b7cd028440e6eb03401dc623c7ee0fc0fe74f6ffa12a25ecc23d0cf54e6acd1e
3
+ metadata.gz: ae9ce385f55d300aa1c6d50b75ffe5bb91b28783e834009dc308d3e52856f786
4
+ data.tar.gz: 13cf6b430cb88a14040442a94f0df5d2831f263ae1205a6b0c6e228c5bc3aefe
5
5
  SHA512:
6
- metadata.gz: a43f0630e8a02a001132f45c9f68cacf7edae8e90487112e640eb611e7d1345f68ad0ab163ae01c91cb38ebc879e98cda9b54044c0ee676293c7aa3bf7c17942
7
- data.tar.gz: bf98027dc02ac56d02cc258700f5efa766c40d4d69c55c529d311083965d1fe4f76423b05fddb35a37496bb6eb3c11a8460a8e76f94897c4bb38a744b2fb40df
6
+ metadata.gz: a98eadc353fbc02963228503b92d88cdc04414c037d5728ebad471764b202967a5b535e34a9533abb8879ccfe20b88ff2561f8e122433b0f6deb1333ee9c22cf
7
+ data.tar.gz: 58af317b915a83bb8f0458dd25ab7a2b1aa31ee3835e3ad05082a6478f68d74681c0d040d9455b11135e8952bee2aadde70a344d1f25301af0a604a4e996b746
@@ -4,6 +4,14 @@ Breaking changes are prefixed with a "[BREAKING]" label.
4
4
 
5
5
  ## master (unreleased)
6
6
 
7
+ ## 0.4.0 (2020-10-07)
8
+
9
+ ### Added
10
+
11
+ - Builds can be configured to terminate after a specified number of failures,
12
+ using the `--fail-fast` option.
13
+
14
+
7
15
  ## 0.3.0 (2020-10-05)
8
16
 
9
17
  ### Added
data/README.md CHANGED
@@ -28,6 +28,7 @@ and [ci-queue](https://github.com/Shopify/ci-queue).
28
28
  - Sentry integration for monitoring build-level events. See [*Sentry integration*](#sentry-integration).
29
29
  - [PLANNED] StatsD integration for various build-level metrics and insights.
30
30
  See [#2](https://github.com/skroutz/rspecq/issues/2).
31
+ - Automatic termination of builds after a certain amount of failures. See [*Fail-fast*](#fail-fast).
31
32
 
32
33
  ## Usage
33
34
 
@@ -73,6 +74,7 @@ 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
+ --fail-fast N Abort build with a non-zero status code after N failed examples.
76
78
  -h, --help Show this message.
77
79
  -v, --version Print the version and exit.
78
80
  ```
@@ -133,6 +135,16 @@ final report.
133
135
  Flaky tests are also detected and printed as such in the final report. They are
134
136
  also emitted to Sentry (see [Sentry integration](#sentry-integration)).
135
137
 
138
+ ### Fail-fast
139
+
140
+ In order to prevent large suites running for a long time with a lot of
141
+ failures, a threshold can be set to control the number of failed examples that
142
+ will render the build unsuccessful. This is in par with RSpec's
143
+ [--fail-fast](https://relishapp.com/rspec/rspec-core/docs/command-line/fail-fast-option).
144
+
145
+ This feature is disabled by default, and can be controlled via the
146
+ `--fail-fast` command line option.
147
+
136
148
  ### Worker failures
137
149
 
138
150
  It's not uncommon for CI processes to encounter unrecoverable failures for
data/bin/rspecq CHANGED
@@ -5,6 +5,7 @@ require "rspecq"
5
5
  DEFAULT_REDIS_HOST = "127.0.0.1"
6
6
  DEFAULT_REPORT_TIMEOUT = 3600 # 1 hour
7
7
  DEFAULT_MAX_REQUEUES = 3
8
+ DEFAULT_FAIL_FAST = 0
8
9
 
9
10
  def env_set?(var)
10
11
  ["1", "true"].include?(ENV[var])
@@ -83,6 +84,11 @@ OptionParser.new do |o|
83
84
  opts[:max_requeues] = v
84
85
  end
85
86
 
87
+ o.on("--fail-fast N", Integer, "Abort build with a non-zero status code " \
88
+ "after N failed examples." ) do |v|
89
+ opts[:fail_fast] = v
90
+ end
91
+
86
92
  o.on_tail("-h", "--help", "Show this message.") do
87
93
  puts o
88
94
  exit
@@ -103,6 +109,7 @@ opts[:report] ||= env_set?("RSPECQ_REPORT")
103
109
  opts[:report_timeout] ||= Integer(ENV["RSPECQ_REPORT_TIMEOUT"] || DEFAULT_REPORT_TIMEOUT)
104
110
  opts[:max_requeues] ||= Integer(ENV["RSPECQ_MAX_REQUEUES"] || DEFAULT_MAX_REQUEUES)
105
111
  opts[:redis_url] ||= ENV["RSPECQ_REDIS_URL"]
112
+ opts[:fail_fast] ||= Integer(ENV["RSPECQ_FAIL_FAST"] || DEFAULT_FAIL_FAST)
106
113
 
107
114
  raise OptionParser::MissingArgument.new(:build) if opts[:build].nil?
108
115
  raise OptionParser::MissingArgument.new(:worker) if !opts[:report] && opts[:worker].nil?
@@ -134,5 +141,6 @@ else
134
141
  worker.populate_timings = opts[:timings]
135
142
  worker.file_split_threshold = opts[:file_split_threshold]
136
143
  worker.max_requeues = opts[:max_requeues]
144
+ worker.fail_fast = opts[:fail_fast]
137
145
  worker.work
138
146
  end
@@ -77,8 +77,9 @@ module RSpecQ
77
77
  end
78
78
 
79
79
  # NOTE: jobs will be processed from head to tail (lpop)
80
- def publish(jobs)
80
+ def publish(jobs, fail_fast = 0)
81
81
  @redis.multi do
82
+ @redis.hset(key_queue_config, 'fail_fast', fail_fast)
82
83
  @redis.rpush(key_queue_unprocessed, jobs)
83
84
  @redis.set(key_queue_status, STATUS_READY)
84
85
  end.first
@@ -232,7 +233,9 @@ module RSpecQ
232
233
  # after being retried). Must be called after the build is complete,
233
234
  # otherwise an exception will be raised.
234
235
  def flaky_jobs
235
- raise "Queue is not yet exhausted" if !exhausted?
236
+ if !exhausted? && !build_failed_fast?
237
+ raise "Queue is not yet exhausted"
238
+ end
236
239
 
237
240
  requeued = @redis.hkeys(key_requeues)
238
241
 
@@ -241,11 +244,38 @@ module RSpecQ
241
244
  requeued - @redis.hkeys(key_failures)
242
245
  end
243
246
 
247
+ # Returns the number of failures that will trigger the build to fail-fast.
248
+ # Returns 0 if this feature is disabled and nil if the Queue is not yet
249
+ # published
250
+ def fail_fast
251
+ return nil unless published?
252
+
253
+ @fail_fast ||= Integer(@redis.hget(key_queue_config, 'fail_fast'))
254
+ end
255
+
256
+ # Returns true if the number of failed tests, has surpassed the threshold
257
+ # to render the run unsuccessful and the build should be terminated.
258
+ def build_failed_fast?
259
+ if fail_fast.nil? || fail_fast.zero?
260
+ return false
261
+ end
262
+
263
+ @redis.multi do
264
+ @redis.hlen(key_failures)
265
+ @redis.hlen(key_errors)
266
+ end.inject(:+) >= fail_fast
267
+ end
268
+
244
269
  # redis: STRING [STATUS_INITIALIZING, STATUS_READY]
245
270
  def key_queue_status
246
271
  key("queue", "status")
247
272
  end
248
273
 
274
+ # redis: HASH<config_key => config_value>
275
+ def key_queue_config
276
+ key("queue", "config")
277
+ end
278
+
249
279
  # redis: LIST<job>
250
280
  def key_queue_unprocessed
251
281
  key("queue", "unprocessed")
@@ -41,7 +41,7 @@ module RSpecQ
41
41
  puts failure_formatted(rspec_output)
42
42
  end
43
43
 
44
- if !@queue.exhausted?
44
+ unless @queue.exhausted? || @queue.build_failed_fast?
45
45
  sleep 1
46
46
  next
47
47
  end
@@ -83,6 +83,13 @@ module RSpecQ
83
83
  end
84
84
 
85
85
  summary = ""
86
+ if @queue.build_failed_fast?
87
+ summary << "\n\n"
88
+ summary << "The limit of #{@queue.fail_fast} failures has been reached\n"
89
+ summary << "Aborting..."
90
+ summary << "\n"
91
+ end
92
+
86
93
  summary << failed_examples_section if !failures.empty?
87
94
 
88
95
  errors.each { |_job, msg| summary << msg }
@@ -1,3 +1,3 @@
1
1
  module RSpecQ
2
- VERSION = "0.3.0".freeze
2
+ VERSION = "0.4.0".freeze
3
3
  end
@@ -40,12 +40,19 @@ 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
+
43
49
  attr_reader :queue
44
50
 
45
51
  def initialize(build_id:, worker_id:, redis_opts:)
46
52
  @build_id = build_id
47
53
  @worker_id = worker_id
48
54
  @queue = Queue.new(build_id, worker_id, redis_opts)
55
+ @fail_fast = 0
49
56
  @files_or_dirs_to_run = "spec"
50
57
  @populate_timings = false
51
58
  @file_split_threshold = 999999
@@ -69,6 +76,8 @@ module RSpecQ
69
76
  # to `requeue_lost_job` inside the work loop
70
77
  update_heartbeat
71
78
 
79
+ return if queue.build_failed_fast?
80
+
72
81
  lost = queue.requeue_lost_job
73
82
  puts "Requeued lost job: #{lost}" if lost
74
83
 
@@ -121,7 +130,7 @@ module RSpecQ
121
130
 
122
131
  timings = queue.timings
123
132
  if timings.empty?
124
- q_size = queue.publish(files_to_run.shuffle)
133
+ q_size = queue.publish(files_to_run.shuffle, fail_fast)
125
134
  log_event(
126
135
  "No timings found! Published queue in random order (size=#{q_size})",
127
136
  "warning"
@@ -160,7 +169,7 @@ module RSpecQ
160
169
  # sort jobs based on their timings (slowest to be processed first)
161
170
  jobs = jobs.sort_by { |_j, t| -t }.map(&:first)
162
171
 
163
- puts "Published queue (size=#{queue.publish(jobs)})"
172
+ puts "Published queue (size=#{queue.publish(jobs, fail_fast)})"
164
173
  end
165
174
 
166
175
  private
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.4.0
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: 2020-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec-core