rspecq 0.5.0 → 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
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