rspecq 0.5.0 → 0.7.2

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: 2abc0b960b2d28528c0d8a4f9fff7dc0708fd582c7377af021b6c01777350124
4
- data.tar.gz: 23d432f09a68ace0932d3c82a112fd0e7058357fa4029c94401a75b84240d5b7
3
+ metadata.gz: d62ec9f80b28ee9acd347b82b484eeeb0e7a6b21f76e201a19d14477840f91a3
4
+ data.tar.gz: 8b20f8f635cc3bec96707e29f6547a060baf7131c6e53527e3e1ce8583e5e27b
5
5
  SHA512:
6
- metadata.gz: 9c499c1ded556a66e9547e71ebfc3f4cdefd6ec984269986b7ae2e1078a86e157ed9805d57afd615f7e9044869124b8f2359095c65febf2d39b3065191e450b7
7
- data.tar.gz: 050a8b4eb4983234cbd2d42a24590c9d0914833231c16692f273020c297757c8afca453f09f9ff72a6ec10564809c81356f3ab23c9220644fbcba89f7f0a90a3
6
+ metadata.gz: 327351debae5f53a3a0be189fc7ff59877030d38d640e3310bb6cd24ec8944e7b38f7761f88e05fa5fa5804d37bbc5fd4889839829f88d96f1d84e1545de6625
7
+ data.tar.gz: 4ba95049a709afb17feebd22617c7018528fe9f2379170f93913e66e10ddb2723eb01411b228087efe8cfb27a255a7dacce7cddd283246da89c34dbbfe2d5fc0
data/CHANGELOG.md CHANGED
@@ -1,8 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ Breaking changes are prefixed with a "[BREAKING]" label.
4
+
3
5
  ## master (unreleased)
4
6
 
5
- Breaking changes are prefixed with a "[BREAKING]" label.
7
+ ## 0.7.2 (2021-11-15)
8
+
9
+ - Add tag option to filter specs (@jorge-wonolo)
10
+
11
+ ## 0.7.1 (2021-04-08)
12
+
13
+ - New env variable RSPECQ_REPORTER_RERUN_COMMAND_SKIP. When set, the reporter
14
+ does not include the flaky test's rerun command.
15
+
16
+ ## 0.7.0 (2021-04-01)
17
+
18
+ - New cli parameter `reproduction`.
19
+ When passed, primary worker publishes the queue in the same order as passed
20
+ in the command.
21
+ - Reporter now includes a reproduction command for flaky tests.
22
+
23
+ ## 0.6.0 (2021-03-23)
24
+
25
+ - New cli parameter `seed`.
26
+ The seed is passed to the RSpec command.
6
27
 
7
28
  ## 0.5.0 (2021-02-05)
8
29
 
data/README.md CHANGED
@@ -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).
@@ -75,10 +76,36 @@ OPTIONS:
75
76
  --max-requeues N Retry failed examples up to N times before considering them legit failures (default: 3).
76
77
  --queue-wait-timeout N Time to wait for a queue to be ready before considering it failed (default: 30).
77
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.
80
+ --tag TAG Run examples with the specified tag, or exclude examples by adding ~ before the tag. - e.g. ~slow - TAG is always converted to a symbol.
78
81
  -h, --help Show this message.
79
82
  -v, --version Print the version and exit.
80
83
  ```
81
84
 
85
+ You can set most options using ENV variables:
86
+
87
+ ```shell
88
+ $ RSPECQ_BUILD=123 RSPECQ_WORKER=foo1 rspecq spec/
89
+ ```
90
+
91
+ ### Supported ENV variables
92
+
93
+ | Name | Desc |
94
+ | --- | --- |
95
+ | `RSPECQ_BUILD` | Build ID |
96
+ | `RSPECQ_WORKER` | Worker ID |
97
+ | `RSPECQ_SEED` | RSpec seed |
98
+ | `RSPECQ_REDIS` | Redis HOST |
99
+ | `RSPECQ_UPDATE_TIMINGS` | Timings |
100
+ | `RSPECQ_FILE_SPLIT_THRESHOLD` | File split threshold |
101
+ | `RSPECQ_REPORT` | Report |
102
+ | `RSPECQ_REPORT_TIMEOUT` | Report Timeout |
103
+ | `RSPECQ_MAX_REQUEUES` | Max requests |
104
+ | `RSPECQ_QUEUE_WAIT_TIMEOUT` | Queue wait timeout |
105
+ | `RSPECQ_REDIS_URL` | Redis URL |
106
+ | `RSPECQ_FAIL_FAST` | Fail fast |
107
+ | `RSPECQ_REPORTER_RERUN_COMMAND_SKIP` | Do not report flaky test's rerun command |
108
+
82
109
  ### Sentry integration
83
110
 
84
111
  RSpecQ can optionally emit build events to a
@@ -90,7 +117,6 @@ This is convenient for monitoring important warnings/errors that may impact
90
117
  build times, such as the fact that no previous timings were found and
91
118
  therefore job scheduling was effectively random for a particular build.
92
119
 
93
-
94
120
  ## How it works
95
121
 
96
122
  The core design is almost identical to ci-queue so please refer to its
@@ -117,7 +143,7 @@ For example, a single file may need 10 minutes to run while all other
117
143
  files finish after 8 minutes. This would cause all but one workers to be
118
144
  sitting idle for 2 minutes.
119
145
 
120
- To overcome this issue, RSpecQ can splits files which their execution time is
146
+ To overcome this issue, RSpecQ can split files which their execution time is
121
147
  above a certain threshold (set with the `--file-split-threshold` option)
122
148
  and instead schedule them as individual examples.
123
149
 
@@ -216,6 +242,13 @@ To enable verbose output in the tests:
216
242
  $ RSPECQ_DEBUG=1 bundle exec rake
217
243
  ```
218
244
 
245
+ ## Redis
246
+
247
+ RSpecQ by design doesn't expire its keys from Redis. It is left to the user
248
+ to configure the Redis server to do so; see
249
+ [Using Redis as an LRU cache](https://redis.io/topics/lru-cache) for more info.
250
+
251
+ You can do this from a configuration file or with `redis-cli`.
219
252
 
220
253
  ## License
221
254
 
data/bin/rspecq CHANGED
@@ -12,7 +12,9 @@ def env_set?(var)
12
12
  ["1", "true"].include?(ENV[var])
13
13
  end
14
14
 
15
- opts = {}
15
+ opts = {
16
+ tags: []
17
+ }
16
18
 
17
19
  OptionParser.new do |o|
18
20
  name = File.basename($PROGRAM_NAME)
@@ -38,9 +40,14 @@ OptionParser.new do |o|
38
40
  opts[:worker] = v
39
41
  end
40
42
 
43
+ o.on("--seed SEED", "The RSpec seed. Passing the seed can be helpful in " \
44
+ "many ways i.e reproduction and testing.") do |v|
45
+ opts[:seed] = v
46
+ end
47
+
41
48
  o.on("-r", "--redis HOST", "Redis host to connect to " \
42
49
  "(default: #{DEFAULT_REDIS_HOST}).") do |v|
43
- puts "--redis is deprecated. Use --redis-host or --redis-url instead"
50
+ puts "DEPRECATION: --redis is deprecated. Use --redis-host or --redis-url instead"
44
51
  opts[:redis_host] = v
45
52
  end
46
53
 
@@ -96,11 +103,24 @@ OptionParser.new do |o|
96
103
  opts[:fail_fast] = v
97
104
  end
98
105
 
106
+ o.on("--reproduction", "Enable reproduction mode: run rspec on the given files " \
107
+ "and examples in the exact order they are given. Incompatible with " \
108
+ "--timings.") do |v|
109
+ opts[:reproduction] = v
110
+ end
111
+
99
112
  o.on_tail("-h", "--help", "Show this message.") do
100
113
  puts o
101
114
  exit
102
115
  end
103
116
 
117
+ o.on("--tag TAG", "Run examples with the specified tag, or exclude examples " \
118
+ "by adding ~ before the tag." \
119
+ " - e.g. ~slow" \
120
+ " - TAG is always converted to a symbol.") do |tag|
121
+ opts[:tags] << tag
122
+ end
123
+
104
124
  o.on_tail("-v", "--version", "Print the version and exit.") do
105
125
  puts "#{name} #{RSpecQ::VERSION}"
106
126
  exit
@@ -109,6 +129,7 @@ end.parse!
109
129
 
110
130
  opts[:build] ||= ENV["RSPECQ_BUILD"]
111
131
  opts[:worker] ||= ENV["RSPECQ_WORKER"]
132
+ opts[:seed] ||= ENV["RSPECQ_SEED"]
112
133
  opts[:redis_host] ||= ENV["RSPECQ_REDIS"] || DEFAULT_REDIS_HOST
113
134
  opts[:timings] ||= env_set?("RSPECQ_UPDATE_TIMINGS")
114
135
  opts[:file_split_threshold] ||= Integer(ENV["RSPECQ_FILE_SPLIT_THRESHOLD"] || 9_999_999)
@@ -118,6 +139,7 @@ opts[:max_requeues] ||= Integer(ENV["RSPECQ_MAX_REQUEUES"] || DEFAULT_MAX_REQUEU
118
139
  opts[:queue_wait_timeout] ||= Integer(ENV["RSPECQ_QUEUE_WAIT_TIMEOUT"] || DEFAULT_QUEUE_WAIT_TIMEOUT)
119
140
  opts[:redis_url] ||= ENV["RSPECQ_REDIS_URL"]
120
141
  opts[:fail_fast] ||= Integer(ENV["RSPECQ_FAIL_FAST"] || DEFAULT_FAIL_FAST)
142
+ opts[:reproduction] ||= env_set?("RSPECQ_REPRODUCTION")
121
143
 
122
144
  # rubocop:disable Style/RaiseArgs, Layout/EmptyLineAfterGuardClause
123
145
  raise OptionParser::MissingArgument.new(:build) if opts[:build].nil?
@@ -148,11 +170,14 @@ else
148
170
  redis_opts: redis_opts
149
171
  )
150
172
 
151
- worker.files_or_dirs_to_run = ARGV[0] if ARGV[0]
173
+ worker.files_or_dirs_to_run = ARGV if ARGV.any?
152
174
  worker.populate_timings = opts[:timings]
153
175
  worker.file_split_threshold = opts[:file_split_threshold]
154
176
  worker.max_requeues = opts[:max_requeues]
155
177
  worker.queue_wait_timeout = opts[:queue_wait_timeout]
156
178
  worker.fail_fast = opts[:fail_fast]
179
+ worker.seed = Integer(opts[:seed]) if opts[:seed]
180
+ worker.reproduction = opts[:reproduction]
181
+ worker.tags = opts[:tags]
157
182
  worker.work
158
183
  end
@@ -6,12 +6,13 @@ module RSpecQ
6
6
  # Also persists non-example error information (e.g. a syntax error that
7
7
  # in a spec file).
8
8
  class FailureRecorder
9
- def initialize(queue, job, max_requeues)
9
+ def initialize(queue, job, max_requeues, worker_id)
10
10
  @queue = queue
11
11
  @job = job
12
12
  @colorizer = RSpec::Core::Formatters::ConsoleCodes
13
13
  @non_example_error_recorded = false
14
14
  @max_requeues = max_requeues
15
+ @worker_id = worker_id
15
16
  end
16
17
 
17
18
  # Here we're notified about errors occuring outside of examples.
@@ -30,7 +31,7 @@ module RSpecQ
30
31
  def example_failed(notification)
31
32
  example = notification.example
32
33
 
33
- if @queue.requeue_job(example.id, @max_requeues)
34
+ if @queue.requeue_job(example, @max_requeues, @worker_id)
34
35
  # HACK: try to avoid picking the job we just requeued; we want it
35
36
  # to be picked up by a different worker
36
37
  sleep 0.5
@@ -44,7 +45,7 @@ module RSpecQ
44
45
  msg = presenter.fully_formatted(nil, @colorizer)
45
46
  msg << "\n"
46
47
  msg << @colorizer.wrap(
47
- "bin/rspec #{example.location_rerun_argument}",
48
+ "bin/rspec --seed #{RSpec.configuration.seed} #{example.location_rerun_argument}",
48
49
  RSpec.configuration.failure_color
49
50
  )
50
51
 
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
@@ -117,6 +123,7 @@ module RSpecQ
117
123
  @redis.multi do
118
124
  @redis.hdel(key_queue_running, @worker_id)
119
125
  @redis.sadd(key_queue_processed, job)
126
+ @redis.rpush(key("queue", "jobs_per_worker", @worker_id), job)
120
127
  end
121
128
  end
122
129
 
@@ -125,16 +132,41 @@ module RSpecQ
125
132
  #
126
133
  # Returns nil if the job hit the requeue limit and therefore was not
127
134
  # requeued and should be considered a failure.
128
- def requeue_job(job, max_requeues)
135
+ def requeue_job(example, max_requeues, original_worker_id)
129
136
  return false if max_requeues.zero?
130
137
 
138
+ job = example.id
139
+ location = example.location_rerun_argument
140
+
131
141
  @redis.eval(
132
142
  REQUEUE_JOB,
133
- keys: [key_queue_unprocessed, key_requeues],
134
- 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]
135
145
  )
136
146
  end
137
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
+
138
170
  def record_example_failure(example_id, message)
139
171
  @redis.hset(key_failures, example_id, message)
140
172
  end
@@ -107,7 +107,16 @@ module RSpecQ
107
107
  if !flaky_jobs.empty?
108
108
  summary << "\n\n"
109
109
  summary << "Flaky jobs detected (count=#{flaky_jobs.count}):\n"
110
- 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
111
120
  end
112
121
 
113
122
  summary
@@ -125,16 +134,15 @@ module RSpecQ
125
134
  return if jobs.empty?
126
135
 
127
136
  jobs.each do |job|
128
- filename = job.sub(/\[.+\]/, "")
137
+ filename = job.sub(/\[.+\]/, "")[%r{spec/.+}].split(":")[0]
129
138
 
130
139
  extra = {
131
140
  build: @build_id,
132
141
  build_timeout: @timeout,
133
- queue: @queue.inspect,
134
- object: inspect,
135
- pid: Process.pid,
136
- job_path: job,
137
- 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)
138
146
  }
139
147
 
140
148
  tags = {
@@ -1,3 +1,3 @@
1
1
  module RSpecQ
2
- VERSION = "0.5.0".freeze
2
+ VERSION = "0.7.2".freeze
3
3
  end
data/lib/rspecq/worker.rb CHANGED
@@ -51,6 +51,16 @@ module RSpecQ
51
51
  # Defaults to 30
52
52
  attr_accessor :queue_wait_timeout
53
53
 
54
+ # The RSpec seed
55
+ attr_accessor :seed
56
+
57
+ # Rspec tags
58
+ attr_accessor :tags
59
+
60
+ # Reproduction flag. If true, worker will publish files in the exact order
61
+ # given in the command.
62
+ attr_accessor :reproduction
63
+
54
64
  attr_reader :queue
55
65
 
56
66
  def initialize(build_id:, worker_id:, redis_opts:)
@@ -64,6 +74,9 @@ module RSpecQ
64
74
  @heartbeat_updated_at = nil
65
75
  @max_requeues = 3
66
76
  @queue_wait_timeout = 30
77
+ @seed = srand && srand % 0xFFFF
78
+ @tags = []
79
+ @reproduction = false
67
80
 
68
81
  RSpec::Core::Formatters.register(Formatters::JobTimingRecorder, :dump_summary)
69
82
  RSpec::Core::Formatters.register(Formatters::ExampleCountRecorder, :dump_summary)
@@ -76,6 +89,7 @@ module RSpecQ
76
89
 
77
90
  try_publish_queue!(queue)
78
91
  queue.wait_until_published(queue_wait_timeout)
92
+ queue.save_worker_seed(@worker_id, seed)
79
93
 
80
94
  loop do
81
95
  # we have to bootstrap this so that it can be used in the first call
@@ -103,17 +117,19 @@ module RSpecQ
103
117
 
104
118
  # reconfigure rspec
105
119
  RSpec.configuration.detail_color = :magenta
106
- RSpec.configuration.seed = srand && srand % 0xFFFF
120
+ RSpec.configuration.seed = seed
107
121
  RSpec.configuration.backtrace_formatter.filter_gem("rspecq")
108
- RSpec.configuration.add_formatter(Formatters::FailureRecorder.new(queue, job, max_requeues))
122
+ RSpec.configuration.add_formatter(Formatters::FailureRecorder.new(queue, job, max_requeues, @worker_id))
109
123
  RSpec.configuration.add_formatter(Formatters::ExampleCountRecorder.new(queue))
110
124
  RSpec.configuration.add_formatter(Formatters::WorkerHeartbeatRecorder.new(self))
111
125
 
112
126
  if populate_timings
113
127
  RSpec.configuration.add_formatter(Formatters::JobTimingRecorder.new(queue, job))
114
128
  end
115
-
116
- opts = RSpec::Core::ConfigurationOptions.new(["--format", "progress", job])
129
+
130
+ options = ["--format", "progress", job]
131
+ tags.each { |tag| options.push(*["--tag", tag]) }
132
+ opts = RSpec::Core::ConfigurationOptions.new(options)
117
133
  _result = RSpec::Core::Runner.new(opts).run($stderr, $stdout)
118
134
 
119
135
  queue.acknowledge_job(job)
@@ -131,6 +147,14 @@ module RSpecQ
131
147
  def try_publish_queue!(queue)
132
148
  return if !queue.become_master
133
149
 
150
+ if reproduction
151
+ q_size = queue.publish(files_or_dirs_to_run, fail_fast)
152
+ log_event(
153
+ "Reproduction mode. Published queue as given (size=#{q_size})",
154
+ "info"
155
+ )
156
+ return
157
+ end
134
158
  RSpec.configuration.files_or_directories_to_run = files_or_dirs_to_run
135
159
  files_to_run = RSpec.configuration.files_to_run.map { |j| relative_path(j) }
136
160
 
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.5.0
4
+ version: 0.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Agis Anastasopoulos
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-02-05 00:00:00.000000000 Z
11
+ date: 2021-11-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec-core