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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +2 -0
- data/bin/rspecq +9 -1
- data/lib/rspecq/formatters/failure_recorder.rb +4 -10
- data/lib/rspecq/queue.rb +31 -7
- data/lib/rspecq/reporter.rb +9 -8
- data/lib/rspecq/version.rb +1 -1
- data/lib/rspecq/worker.rb +16 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4337137edc0f3ee2c22d4f9cd2547ab31d4ddc0b043b49848c7b513ae98094a9
|
|
4
|
+
data.tar.gz: 6de02e1ce31cb7dff6af9d4862f1dceebfbfbbd10063329c9d3d058d32bf8f06
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
139
|
-
@redis.hset(key("
|
|
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
|
|
143
|
-
|
|
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)
|
data/lib/rspecq/reporter.rb
CHANGED
|
@@ -56,7 +56,7 @@ module RSpecQ
|
|
|
56
56
|
|
|
57
57
|
@queue.record_build_time(tests_duration)
|
|
58
58
|
|
|
59
|
-
flaky_jobs = @queue.flaky_jobs
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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 = {
|
data/lib/rspecq/version.rb
CHANGED
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.
|
|
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-
|
|
11
|
+
date: 2021-04-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rspec-core
|