rspecq 0.6.0 → 0.7.0

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: 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