rspecq 0.6.0 → 0.7.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: 86b8792f2ff107cbfb1e67b881724c535df7730804feec7e4813f4690795fac1
4
- data.tar.gz: bbd5b78cf7919b653446b00c0748c0ea0d9fca32f69e40e7dd439b3396042dec
3
+ metadata.gz: 4337137edc0f3ee2c22d4f9cd2547ab31d4ddc0b043b49848c7b513ae98094a9
4
+ data.tar.gz: 6de02e1ce31cb7dff6af9d4862f1dceebfbfbbd10063329c9d3d058d32bf8f06
5
5
  SHA512:
6
- metadata.gz: 29ee44880ef2d654cd8faeeeb7bcdee2f7ff2b468a22a51a3831d6a175a31246e0cd2206ce012eaa3c1a2ad3aad319360baa10f10d3b84745e7ee9e58fdf3b7b
7
- data.tar.gz: d8cd7a9515a25a9b8679acffa99f3b8f19764ff7e23079921677aa40b1941cb6fbce05dbad813f308d68d460f7c06b97a453de2622ee8060161033f65c889189
6
+ metadata.gz: beb1b013a1d78e8c18b473ef33d286224a92ef46ae3944f99296cc3d2d13193309b0efb020307fa3e9139b7fd88eb9a9111ad526026762ccf0b17ba892b3ddb4
7
+ data.tar.gz: 178481fe5b707cefa25426ec6b2ead691dd69494a6b9869eaf8e438b78e100c5dd8c0c9caab2dd4b792c18e5c931e4e7231cc971b77fa048c14420f33978fe2a
data/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@ Breaking changes are prefixed with a "[BREAKING]" label.
4
4
 
5
5
  ## master (unreleased)
6
6
 
7
+ ## 0.7.0 (2021-04-1)
8
+
9
+ - New cli parameter `reproduction`.
10
+ When passed, primary worker publishes the queue in the same order as passed
11
+ in the command.
12
+ - Reporter now includes a reproduction command for flaky tests.
13
+
7
14
  ## 0.6.0 (2021-03-23)
8
15
 
9
16
  - New cli parameter `seed`.
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,6 +76,7 @@ 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.
78
80
  -h, --help Show this message.
79
81
  -v, --version Print the version and exit.
80
82
  ```
data/bin/rspecq CHANGED
@@ -101,6 +101,12 @@ OptionParser.new do |o|
101
101
  opts[:fail_fast] = v
102
102
  end
103
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
+
104
110
  o.on_tail("-h", "--help", "Show this message.") do
105
111
  puts o
106
112
  exit
@@ -124,6 +130,7 @@ opts[:max_requeues] ||= Integer(ENV["RSPECQ_MAX_REQUEUES"] || DEFAULT_MAX_REQUEU
124
130
  opts[:queue_wait_timeout] ||= Integer(ENV["RSPECQ_QUEUE_WAIT_TIMEOUT"] || DEFAULT_QUEUE_WAIT_TIMEOUT)
125
131
  opts[:redis_url] ||= ENV["RSPECQ_REDIS_URL"]
126
132
  opts[:fail_fast] ||= Integer(ENV["RSPECQ_FAIL_FAST"] || DEFAULT_FAIL_FAST)
133
+ opts[:reproduction] ||= env_set?("RSPECQ_REPRODUCTION")
127
134
 
128
135
  # rubocop:disable Style/RaiseArgs, Layout/EmptyLineAfterGuardClause
129
136
  raise OptionParser::MissingArgument.new(:build) if opts[:build].nil?
@@ -154,12 +161,13 @@ else
154
161
  redis_opts: redis_opts
155
162
  )
156
163
 
157
- worker.files_or_dirs_to_run = ARGV[0] if ARGV[0]
164
+ worker.files_or_dirs_to_run = ARGV if ARGV.any?
158
165
  worker.populate_timings = opts[:timings]
159
166
  worker.file_split_threshold = opts[:file_split_threshold]
160
167
  worker.max_requeues = opts[:max_requeues]
161
168
  worker.queue_wait_timeout = opts[:queue_wait_timeout]
162
169
  worker.fail_fast = opts[:fail_fast]
163
170
  worker.seed = Integer(opts[:seed]) if opts[:seed]
171
+ worker.reproduction = opts[:reproduction]
164
172
  worker.work
165
173
  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,14 +31,7 @@ module RSpecQ
30
31
  def example_failed(notification)
31
32
  example = notification.example
32
33
 
33
- rerun_cmd = "bin/rspec --seed #{RSpec.configuration.seed} #{example.location_rerun_argument}"
34
-
35
- if @queue.requeue_job(example.id, @max_requeues)
36
-
37
- # Save the rerun command for later. It will be used if this is
38
- # a flaky test for more user-friendly reporting.
39
- @queue.save_rerun_command(example.id, rerun_cmd)
40
-
34
+ if @queue.requeue_job(example, @max_requeues, @worker_id)
41
35
  # HACK: try to avoid picking the job we just requeued; we want it
42
36
  # to be picked up by a different worker
43
37
  sleep 0.5
@@ -51,7 +45,7 @@ module RSpecQ
51
45
  msg = presenter.fully_formatted(nil, @colorizer)
52
46
  msg << "\n"
53
47
  msg << @colorizer.wrap(
54
- rerun_cmd,
48
+ "bin/rspec --seed #{RSpec.configuration.seed} #{example.location_rerun_argument}",
55
49
  RSpec.configuration.failure_color
56
50
  )
57
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,22 +132,39 @@ 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
 
138
- def save_rerun_command(job, cmd)
139
- @redis.hset(key("job_metadata"), job, cmd)
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)
140
154
  end
141
155
 
142
- def rerun_command(job)
143
- @redis.hget(key("job_metadata"), job)
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(' ')}"
144
168
  end
145
169
 
146
170
  def record_example_failure(example_id, message)
@@ -56,7 +56,7 @@ module RSpecQ
56
56
 
57
57
  @queue.record_build_time(tests_duration)
58
58
 
59
- flaky_jobs = @queue.flaky_jobs.map { |job| @queue.rerun_command(job) }
59
+ flaky_jobs = @queue.flaky_jobs
60
60
 
61
61
  puts summary(@queue.example_failures, @queue.non_example_errors,
62
62
  flaky_jobs, humanize_duration(tests_duration))
@@ -109,9 +109,11 @@ module RSpecQ
109
109
  summary << "Flaky jobs detected (count=#{flaky_jobs.count}):\n"
110
110
  flaky_jobs.each do |j|
111
111
  summary << RSpec::Core::Formatters::ConsoleCodes.wrap(
112
- "#{j}\n",
112
+ "#{@queue.job_location(j)} @ #{@queue.failed_job_worker(j)}\n",
113
113
  RSpec.configuration.pending_color
114
114
  )
115
+
116
+ summary << "#{@queue.job_rerun_command(j)}\n\n\n"
115
117
  end
116
118
  end
117
119
 
@@ -130,16 +132,15 @@ module RSpecQ
130
132
  return if jobs.empty?
131
133
 
132
134
  jobs.each do |job|
133
- filename = job.sub(/\[.+\]/, "")
135
+ filename = job.sub(/\[.+\]/, "")[%r{spec/.+}].split(":")[0]
134
136
 
135
137
  extra = {
136
138
  build: @build_id,
137
139
  build_timeout: @timeout,
138
- queue: @queue.inspect,
139
- object: inspect,
140
- pid: Process.pid,
141
- job_path: job,
142
- build_duration: build_duration
140
+ build_duration: build_duration,
141
+ location: @queue.job_location(job),
142
+ rerun_command: @queue.job_rerun_command(job),
143
+ worker: @queue.failed_job_worker(job)
143
144
  }
144
145
 
145
146
  tags = {
@@ -1,3 +1,3 @@
1
1
  module RSpecQ
2
- VERSION = "0.6.0".freeze
2
+ VERSION = "0.7.0".freeze
3
3
  end
data/lib/rspecq/worker.rb CHANGED
@@ -54,6 +54,10 @@ module RSpecQ
54
54
  # The RSpec seed
55
55
  attr_accessor :seed
56
56
 
57
+ # Reproduction flag. If true, worker will publish files in the exact order
58
+ # given in the command.
59
+ attr_accessor :reproduction
60
+
57
61
  attr_reader :queue
58
62
 
59
63
  def initialize(build_id:, worker_id:, redis_opts:)
@@ -68,6 +72,7 @@ module RSpecQ
68
72
  @max_requeues = 3
69
73
  @queue_wait_timeout = 30
70
74
  @seed = srand && srand % 0xFFFF
75
+ @reproduction = false
71
76
 
72
77
  RSpec::Core::Formatters.register(Formatters::JobTimingRecorder, :dump_summary)
73
78
  RSpec::Core::Formatters.register(Formatters::ExampleCountRecorder, :dump_summary)
@@ -80,6 +85,7 @@ module RSpecQ
80
85
 
81
86
  try_publish_queue!(queue)
82
87
  queue.wait_until_published(queue_wait_timeout)
88
+ queue.save_worker_seed(@worker_id, seed)
83
89
 
84
90
  loop do
85
91
  # we have to bootstrap this so that it can be used in the first call
@@ -109,7 +115,7 @@ module RSpecQ
109
115
  RSpec.configuration.detail_color = :magenta
110
116
  RSpec.configuration.seed = seed
111
117
  RSpec.configuration.backtrace_formatter.filter_gem("rspecq")
112
- RSpec.configuration.add_formatter(Formatters::FailureRecorder.new(queue, job, max_requeues))
118
+ RSpec.configuration.add_formatter(Formatters::FailureRecorder.new(queue, job, max_requeues, @worker_id))
113
119
  RSpec.configuration.add_formatter(Formatters::ExampleCountRecorder.new(queue))
114
120
  RSpec.configuration.add_formatter(Formatters::WorkerHeartbeatRecorder.new(self))
115
121
 
@@ -135,6 +141,15 @@ module RSpecQ
135
141
  def try_publish_queue!(queue)
136
142
  return if !queue.become_master
137
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
+
138
153
  RSpec.configuration.files_or_directories_to_run = files_or_dirs_to_run
139
154
  files_to_run = RSpec.configuration.files_to_run.map { |j| relative_path(j) }
140
155
 
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.6.0
4
+ version: 0.7.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: 2021-03-23 00:00:00.000000000 Z
11
+ date: 2021-04-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec-core