rspecq 0.0.1.pre2 → 0.3.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 +54 -0
- data/README.md +170 -63
- data/Rakefile +9 -0
- data/bin/rspecq +99 -28
- data/lib/rspecq.rb +5 -7
- data/lib/rspecq/formatters/README.md +4 -0
- data/lib/rspecq/formatters/failure_recorder.rb +3 -2
- data/lib/rspecq/queue.rb +47 -6
- data/lib/rspecq/reporter.rb +57 -6
- data/lib/rspecq/version.rb +1 -1
- data/lib/rspecq/worker.rb +128 -67
- metadata +56 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 89dbfa98d1eaceb06c39d41ab85e7fa6923d0c87e9a15b9cbfaf7399ff2aaff3
|
4
|
+
data.tar.gz: b7cd028440e6eb03401dc623c7ee0fc0fe74f6ffa12a25ecc23d0cf54e6acd1e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a43f0630e8a02a001132f45c9f68cacf7edae8e90487112e640eb611e7d1345f68ad0ab163ae01c91cb38ebc879e98cda9b54044c0ee676293c7aa3bf7c17942
|
7
|
+
data.tar.gz: bf98027dc02ac56d02cc258700f5efa766c40d4d69c55c529d311083965d1fe4f76423b05fddb35a37496bb6eb3c11a8460a8e76f94897c4bb38a744b2fb40df
|
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,58 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
Breaking changes are prefixed with a "[BREAKING]" label.
|
4
|
+
|
3
5
|
## master (unreleased)
|
4
6
|
|
7
|
+
## 0.3.0 (2020-10-05)
|
8
|
+
|
9
|
+
### Added
|
10
|
+
|
11
|
+
- Providing a Redis URL is now possible using the `--redis-url` option
|
12
|
+
[[#40](https://github.com/skroutz/rspecq/pull/40)]
|
13
|
+
|
14
|
+
### Changed
|
15
|
+
|
16
|
+
- [DEPRECATION] The `--redis` option is now deprecated. Use `--redis-host`
|
17
|
+
instead [[#40](https://github.com/skroutz/rspecq/pull/40)]
|
18
|
+
|
19
|
+
## 0.2.2 (2020-09-10)
|
20
|
+
|
21
|
+
### Fixed
|
22
|
+
- Worker would fail if application code was writing to stderr
|
23
|
+
[[#35](https://github.com/skroutz/rspecq/pull/35)]
|
24
|
+
|
25
|
+
## 0.2.1 (2020-09-09)
|
26
|
+
|
27
|
+
### Changed
|
28
|
+
|
29
|
+
- Sentry Integration: Changed the way events for flaky jobs are emitted to a
|
30
|
+
per-flaky-job fashion. This ultimately improves grouping and filtering of the
|
31
|
+
flaky events in Sentry [[#33](https://github.com/skroutz/rspecq/pull/33)]
|
32
|
+
|
33
|
+
|
34
|
+
## 0.2.0 (2020-08-31)
|
35
|
+
|
36
|
+
This is a feature release with no breaking changes.
|
37
|
+
|
38
|
+
### Added
|
39
|
+
|
40
|
+
- Flaky jobs are now printed by the reporter in the final build output and also
|
41
|
+
emitted to Sentry (if the integration is enabled) [[#26](https://github.com/skroutz/rspecq/pull/26)]
|
42
|
+
|
43
|
+
## 0.1.0 (2020-08-27)
|
44
|
+
|
45
|
+
### Added
|
46
|
+
|
47
|
+
- Sentry integration for various RSpecQ-level events [[#16](https://github.com/skroutz/rspecq/pull/16)]
|
48
|
+
- CLI: Flags can now be also set environment variables [[c519230](https://github.com/skroutz/rspecq/commit/c5192303e229f361e8ac86ae449b4ea84d42e022)]
|
49
|
+
- CLI: Added shorthand specifiers versions for some flags [[df9faa8](https://github.com/skroutz/rspecq/commit/df9faa8ec6721af8357cfee4de6a2fe7b32070fc)]
|
50
|
+
- CLI: Added `--help` and `--version` flags [[df9faa8](https://github.com/skroutz/rspecq/commit/df9faa8ec6721af8357cfee4de6a2fe7b32070fc)]
|
51
|
+
- CLI: Max number of retries for failed examples is now configurable via the `--max-requeues` option [[#14](https://github.com/skroutz/rspecq/pull/14)]
|
52
|
+
|
53
|
+
### Changed
|
54
|
+
|
55
|
+
- [BREAKING] CLI: Renamed `--timings` to `--update-timings` [[c519230](https://github.com/skroutz/rspecq/commit/c5192303e229f361e8ac86ae449b4ea84d42e022)]
|
56
|
+
- [BREAKING] CLI: Renamed `--build-id` to `--build` and `--worker-id` to `--worker` [[df9faa8](https://github.com/skroutz/rspecq/commit/df9faa8ec6721af8357cfee4de6a2fe7b32070fc)]
|
57
|
+
- CLI: `--worker` is not required when `--reporter` is used [[4323a75](https://github.com/skroutz/rspecq/commit/4323a75ca357274069d02ba9fb51cdebb04e0be4)]
|
58
|
+
- CLI: Improved help output [[df9faa8](https://github.com/skroutz/rspecq/commit/df9faa8ec6721af8357cfee4de6a2fe7b32070fc)]
|
data/README.md
CHANGED
@@ -1,102 +1,209 @@
|
|
1
|
-
|
1
|
+
RSpec Queue
|
2
|
+
=========================================================================
|
3
|
+
[](https://travis-ci.com/github/skroutz/rspecq)
|
4
|
+
[](https://badge.fury.io/rb/rspecq)
|
2
5
|
|
3
|
-
|
4
|
-
|
6
|
+
RSpec Queue (RSpecQ) distributes and executes RSpec suites among parallel
|
7
|
+
workers. It uses a centralized queue that workers connect to and pop off
|
8
|
+
tests from. It ensures optimal scheduling of tests based on their run time,
|
9
|
+
facilitating faster CI builds.
|
5
10
|
|
6
|
-
RSpecQ is
|
11
|
+
RSpecQ is inspired by [test-queue](https://github.com/tmm1/test-queue)
|
7
12
|
and [ci-queue](https://github.com/Shopify/ci-queue).
|
8
13
|
|
9
|
-
##
|
14
|
+
## Features
|
15
|
+
|
16
|
+
- Run an RSpec suite among many workers
|
17
|
+
(potentially located in different hosts) in a distributed fashion,
|
18
|
+
facilitating faster CI builds.
|
19
|
+
- Consolidated, real-time reporting of a build's progress.
|
20
|
+
- Optimal scheduling of test execution by using timings statistics from previous runs and
|
21
|
+
automatically scheduling slow spec files as individual examples. See
|
22
|
+
[*Spec file splitting*](#spec-file-splitting).
|
23
|
+
- Automatic retry of test failures before being considered legit, in order to
|
24
|
+
rule out flakiness. Additionally, flaky tests are detected and provided to
|
25
|
+
the user. See [*Requeues*](#requeues).
|
26
|
+
- Handles intermittent worker failures (e.g. network hiccups, faulty hardware etc.)
|
27
|
+
by detecting non-responsive workers and requeing their jobs. See [*Worker failures*](#worker-failures)
|
28
|
+
- Sentry integration for monitoring build-level events. See [*Sentry integration*](#sentry-integration).
|
29
|
+
- [PLANNED] StatsD integration for various build-level metrics and insights.
|
30
|
+
See [#2](https://github.com/skroutz/rspecq/issues/2).
|
10
31
|
|
11
|
-
|
12
|
-
in the workers (up to 3 minutes), increased memory consumption and too much
|
13
|
-
disk I/O on boot. This is due to the fact that a worker in ci-queue has to
|
14
|
-
load every spec file on boot. This can be problematic for applications with
|
15
|
-
a large number of spec files.
|
16
|
-
|
17
|
-
RSpecQ works with spec files as its unit of work (as opposed to ci-queue which
|
18
|
-
works with individual examples). This means that an RSpecQ worker does not
|
19
|
-
have to load all spec files at once and so it doesn't have the aforementioned
|
20
|
-
problems. It also allows suites to keep using `before(:all)` hooks
|
21
|
-
(which ci-queue explicitly rejects). (Note: RSpecQ also schedules individual
|
22
|
-
examples, but only when this is deemed necessary, see section
|
23
|
-
"Spec file splitting").
|
32
|
+
## Usage
|
24
33
|
|
25
|
-
|
26
|
-
|
34
|
+
A worker needs to be given a name and the build it will participate in.
|
35
|
+
Assuming there's a Redis instance listening at `localhost`, starting a worker
|
36
|
+
is as simple as:
|
27
37
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
on every run (if the `--timings` option was used). Also, RSpecQ has a "slow
|
32
|
-
file threshold" which, currently has to be set manually (but this can be
|
33
|
-
improved).
|
38
|
+
```shell
|
39
|
+
$ rspecq --build=123 --worker=foo1 spec/
|
40
|
+
```
|
34
41
|
|
35
|
-
|
42
|
+
To start more workers for the same build, use distinct worker IDs but the same
|
43
|
+
build ID:
|
36
44
|
|
37
|
-
|
45
|
+
```shell
|
46
|
+
$ rspecq --build=123 --worker=foo2
|
47
|
+
```
|
38
48
|
|
39
|
-
|
40
|
-
Redis is located. To start a worker:
|
49
|
+
To view the progress of the build use `--report`:
|
41
50
|
|
42
51
|
```shell
|
43
|
-
$ rspecq --build
|
52
|
+
$ rspecq --build=123 --report
|
44
53
|
```
|
45
54
|
|
46
|
-
|
55
|
+
For detailed info use `--help`:
|
47
56
|
|
48
|
-
```shell
|
49
|
-
$ rspecq --build-id=foo --worker-id=reporter --redis=redis://localhost --report
|
50
57
|
```
|
58
|
+
NAME:
|
59
|
+
rspecq - Optimally distribute and run RSpec suites among parallel workers
|
60
|
+
|
61
|
+
USAGE:
|
62
|
+
rspecq [<options>] [spec files or directories]
|
63
|
+
|
64
|
+
OPTIONS:
|
65
|
+
-b, --build ID A unique identifier for the build. Should be common among workers participating in the same build.
|
66
|
+
-w, --worker ID An identifier for the worker. Workers participating in the same build should have distinct IDs.
|
67
|
+
-r, --redis HOST --redis is deprecated. Use --redis-host or --redis-url instead. Redis host to connect to (default: 127.0.0.1).
|
68
|
+
--redis-host HOST Redis host to connect to (default: 127.0.0.1).
|
69
|
+
--redis-url URL Redis URL to connect to (e.g.: redis://127.0.0.1:6379/0).
|
70
|
+
--update-timings Update the global job timings key with the timings of this build. Note: This key is used as the basis for job scheduling.
|
71
|
+
--file-split-threshold N Split spec files slower than N seconds and schedule them as individual examples.
|
72
|
+
--report Enable reporter mode: do not pull tests off the queue; instead print build progress and exit when it's finished.
|
73
|
+
Exits with a non-zero status code if there were any failures.
|
74
|
+
--report-timeout N Fail if build is not finished after N seconds. Only applicable if --report is enabled (default: 3600).
|
75
|
+
--max-requeues N Retry failed examples up to N times before considering them legit failures (default: 3).
|
76
|
+
-h, --help Show this message.
|
77
|
+
-v, --version Print the version and exit.
|
78
|
+
```
|
79
|
+
|
80
|
+
### Sentry integration
|
81
|
+
|
82
|
+
RSpecQ can optionally emit build events to a
|
83
|
+
[Sentry](https://sentry.io) project by setting the
|
84
|
+
[`SENTRY_DSN`](https://github.com/getsentry/raven-ruby#raven-only-runs-when-sentry_dsn-is-set)
|
85
|
+
environment variable.
|
51
86
|
|
52
|
-
|
87
|
+
This is convenient for monitoring important warnings/errors that may impact
|
88
|
+
build times, such as the fact that no previous timings were found and
|
89
|
+
therefore job scheduling was effectively random for a particular build.
|
53
90
|
|
54
91
|
|
55
92
|
## How it works
|
56
93
|
|
57
|
-
The
|
94
|
+
The core design is almost identical to ci-queue so please refer to its
|
95
|
+
[README](https://github.com/Shopify/ci-queue/blob/master/README.md) instead.
|
58
96
|
|
59
97
|
### Terminology
|
60
98
|
|
61
|
-
- Job
|
99
|
+
- **Job**: the smallest unit of work, which is usually a spec file
|
62
100
|
(e.g. `./spec/models/foo_spec.rb`) but can also be an individual example
|
63
|
-
(e.g. `./spec/models/foo_spec.rb[1:2:1]`) if the file is too slow
|
64
|
-
- Queue
|
65
|
-
information for RSpecQ to
|
66
|
-
be executed, the failure reports
|
67
|
-
-
|
68
|
-
|
69
|
-
|
70
|
-
|
101
|
+
(e.g. `./spec/models/foo_spec.rb[1:2:1]`) if the file is too slow.
|
102
|
+
- **Queue**: a collection of Redis-backed structures that hold all the necessary
|
103
|
+
information for an RSpecQ build to run. This includes timing statistics,
|
104
|
+
jobs to be executed, the failure reports and more.
|
105
|
+
- **Build**: a particular test suite run. Each build has its own **Queue**.
|
106
|
+
- **Worker**: an `rspecq` process that, given a build id, consumes jobs off the
|
107
|
+
build's queue and executes them using RSpec
|
108
|
+
- **Reporter**: an `rspecq` process that, given a build id, waits for the build's
|
109
|
+
queue to be drained and prints the build summary report
|
71
110
|
|
72
111
|
### Spec file splitting
|
73
112
|
|
74
|
-
|
75
|
-
a
|
76
|
-
|
77
|
-
|
78
|
-
(set with the `--file-split-threshold` option) and will instead schedule them as
|
79
|
-
individual examples.
|
113
|
+
Particularly slow spec files may set a limit to how fast a build can be.
|
114
|
+
For example, a single file may need 10 minutes to run while all other
|
115
|
+
files finish after 8 minutes. This would cause all but one workers to be
|
116
|
+
sitting idle for 2 minutes.
|
80
117
|
|
81
|
-
|
82
|
-
|
118
|
+
To overcome this issue, RSpecQ can splits files which their execution time is
|
119
|
+
above a certain threshold (set with the `--file-split-threshold` option)
|
120
|
+
and instead schedule them as individual examples.
|
121
|
+
|
122
|
+
Note: In the future, we'd like for the slow threshold to be calculated and set
|
123
|
+
dynamically (see #3).
|
83
124
|
|
84
125
|
### Requeues
|
85
126
|
|
86
|
-
As a mitigation
|
87
|
-
back to the queue to be picked up by
|
88
|
-
|
89
|
-
|
90
|
-
|
127
|
+
As a mitigation technique against flaky tests, if an example fails it will be
|
128
|
+
put back to the queue to be picked up by another worker. This will be repeated
|
129
|
+
up to a certain number of times (set with the `--max-requeues` option), after
|
130
|
+
which the example will be considered a legit failure and printed as such in the
|
131
|
+
final report.
|
132
|
+
|
133
|
+
Flaky tests are also detected and printed as such in the final report. They are
|
134
|
+
also emitted to Sentry (see [Sentry integration](#sentry-integration)).
|
91
135
|
|
92
136
|
### Worker failures
|
93
137
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
138
|
+
It's not uncommon for CI processes to encounter unrecoverable failures for
|
139
|
+
various reasons: faulty hardware, network hiccups, segmentation faults in
|
140
|
+
MRI etc.
|
141
|
+
|
142
|
+
For resiliency against such issues, workers emit a heartbeat after each
|
143
|
+
example they execute, to signal
|
144
|
+
that they're healthy and performing jobs as expected. If a worker hasn't
|
145
|
+
emitted a heartbeat for a given amount of time (set by `WORKER_LIVENESS_SEC`)
|
146
|
+
it is considered dead and its reserved job will be put back to the queue, to
|
147
|
+
be picked up by another healthy worker.
|
148
|
+
|
149
|
+
|
150
|
+
## Rationale
|
151
|
+
|
152
|
+
### Why didn't you use ci-queue?
|
153
|
+
|
154
|
+
**Update**: ci-queue [deprecated support for RSpec](https://github.com/Shopify/ci-queue/pull/149).
|
155
|
+
|
156
|
+
While evaluating ci-queue we experienced slow worker boot
|
157
|
+
times (up to 3 minutes in some cases) combined with disk IO saturation and
|
158
|
+
increased memory consumption. This is due to the fact that a worker in
|
159
|
+
ci-queue has to load every spec file on boot. In applications with a large
|
160
|
+
number of spec files this may result in a significant performance hit and
|
161
|
+
in case of cloud environments, increased costs.
|
162
|
+
|
163
|
+
We also observed slower build times compared to our previous solution which
|
164
|
+
scheduled whole spec files (as opposed to individual examples), due to
|
165
|
+
big differences in runtimes of individual examples, something common in big
|
166
|
+
RSpec suites.
|
167
|
+
|
168
|
+
We decided for RSpecQ to use whole spec files as its main unit of work (as
|
169
|
+
opposed to ci-queue which uses individual examples). This means that an RSpecQ
|
170
|
+
worker only loads the files needed and ends up with a subset of all the suite's
|
171
|
+
files. (Note: RSpecQ also schedules individual examples, but only when this is
|
172
|
+
deemed necessary, see [Spec file splitting](#spec-file-splitting)).
|
173
|
+
|
174
|
+
This kept boot and test run times considerably fast. As a side benefit, this
|
175
|
+
allows suites to keep using `before(:all)` hooks (which ci-queue explicitly
|
176
|
+
rejects).
|
177
|
+
|
178
|
+
The downside of this design is that it's more complicated, since the scheduling
|
179
|
+
of spec files happens based on timings calculated from previous runs. This
|
180
|
+
means that RSpecQ maintains a key with the timing of each job and updates it
|
181
|
+
on every run (if the `--update-timings` option was used). Also, RSpecQ has a
|
182
|
+
"slow file threshold" which, currently has to be set manually (but this can be
|
183
|
+
improved in the future).
|
184
|
+
|
185
|
+
|
186
|
+
## Development
|
187
|
+
|
188
|
+
Install the required dependencies:
|
189
|
+
|
190
|
+
```
|
191
|
+
$ bundle install
|
192
|
+
```
|
193
|
+
|
194
|
+
Then you can execute the tests after spinning up a Redis instance at
|
195
|
+
`127.0.0.1:6379`:
|
196
|
+
|
197
|
+
```
|
198
|
+
$ bundle exec rake
|
199
|
+
```
|
200
|
+
|
201
|
+
To enable verbose output in the tests:
|
202
|
+
|
203
|
+
```
|
204
|
+
$ RSPECQ_DEBUG=1 bundle exec rake
|
205
|
+
```
|
98
206
|
|
99
|
-
This protects us against unrecoverable worker failures (e.g. segfault).
|
100
207
|
|
101
208
|
## License
|
102
209
|
|
data/Rakefile
ADDED
data/bin/rspecq
CHANGED
@@ -1,67 +1,138 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
require "
|
2
|
+
require "optparse"
|
3
3
|
require "rspecq"
|
4
4
|
|
5
|
+
DEFAULT_REDIS_HOST = "127.0.0.1"
|
6
|
+
DEFAULT_REPORT_TIMEOUT = 3600 # 1 hour
|
7
|
+
DEFAULT_MAX_REQUEUES = 3
|
8
|
+
|
9
|
+
def env_set?(var)
|
10
|
+
["1", "true"].include?(ENV[var])
|
11
|
+
end
|
12
|
+
|
5
13
|
opts = {}
|
14
|
+
|
6
15
|
OptionParser.new do |o|
|
7
|
-
|
16
|
+
name = File.basename($PROGRAM_NAME)
|
17
|
+
|
18
|
+
o.banner = <<~BANNER
|
19
|
+
NAME:
|
20
|
+
#{name} - Optimally distribute and run RSpec suites among parallel workers
|
21
|
+
|
22
|
+
USAGE:
|
23
|
+
#{name} [<options>] [spec files or directories]
|
24
|
+
BANNER
|
25
|
+
|
26
|
+
o.separator ""
|
27
|
+
o.separator "OPTIONS:"
|
28
|
+
|
29
|
+
o.on("-b", "--build ID", "A unique identifier for the build. Should be " \
|
30
|
+
"common among workers participating in the same build.") do |v|
|
31
|
+
opts[:build] = v
|
32
|
+
end
|
33
|
+
|
34
|
+
o.on("-w", "--worker ID", "An identifier for the worker. Workers " \
|
35
|
+
"participating in the same build should have distinct IDs.") do |v|
|
36
|
+
opts[:worker] = v
|
37
|
+
end
|
8
38
|
|
9
|
-
o.on("
|
10
|
-
|
39
|
+
o.on("-r", "--redis HOST", "Redis host to connect to " \
|
40
|
+
"(default: #{DEFAULT_REDIS_HOST}).") do |v|
|
41
|
+
puts "--redis is deprecated. Use --redis-host or --redis-url instead"
|
42
|
+
opts[:redis_host] = v
|
11
43
|
end
|
12
44
|
|
13
|
-
o.on("--
|
14
|
-
|
45
|
+
o.on("--redis-host HOST", "Redis host to connect to " \
|
46
|
+
"(default: #{DEFAULT_REDIS_HOST}).") do |v|
|
47
|
+
opts[:redis_host] = v
|
15
48
|
end
|
16
49
|
|
17
|
-
o.on("--redis
|
18
|
-
|
50
|
+
o.on("--redis-url URL", "The URL of the Redis host to connect to " \
|
51
|
+
"(e.g.: redis://127.0.0.1:6379/0).") do |v|
|
52
|
+
opts[:redis_url] = v
|
19
53
|
end
|
20
54
|
|
21
|
-
o.on("--timings", "
|
55
|
+
o.on("--update-timings", "Update the global job timings key with the " \
|
56
|
+
"timings of this build. Note: This key is used as the basis for job " \
|
57
|
+
"scheduling.") do |v|
|
22
58
|
opts[:timings] = v
|
23
59
|
end
|
24
60
|
|
25
|
-
o.on("--file-split-threshold N", "Split spec files slower than N
|
26
|
-
"schedule them
|
27
|
-
opts[:file_split_threshold] =
|
61
|
+
o.on("--file-split-threshold N", Integer, "Split spec files slower than N " \
|
62
|
+
"seconds and schedule them as individual examples.") do |v|
|
63
|
+
opts[:file_split_threshold] = v
|
28
64
|
end
|
29
65
|
|
30
|
-
o.on("--report", "
|
31
|
-
|
66
|
+
o.on("--report", "Enable reporter mode: do not pull tests off the queue; " \
|
67
|
+
"instead print build progress and exit when it's " \
|
68
|
+
"finished.\n#{o.summary_indent*9} " \
|
69
|
+
"Exits with a non-zero status code if there were any " \
|
70
|
+
"failures.") do |v|
|
32
71
|
opts[:report] = v
|
33
72
|
end
|
34
73
|
|
35
|
-
o.on("--report-timeout N", Integer, "Fail if
|
36
|
-
"N seconds. Only applicable if --report is enabled "
|
37
|
-
"(default:
|
74
|
+
o.on("--report-timeout N", Integer, "Fail if build is not finished after " \
|
75
|
+
"N seconds. Only applicable if --report is enabled " \
|
76
|
+
"(default: #{DEFAULT_REPORT_TIMEOUT}).") do |v|
|
38
77
|
opts[:report_timeout] = v
|
39
78
|
end
|
40
79
|
|
80
|
+
o.on("--max-requeues N", Integer, "Retry failed examples up to N times " \
|
81
|
+
"before considering them legit failures " \
|
82
|
+
"(default: #{DEFAULT_MAX_REQUEUES}).") do |v|
|
83
|
+
opts[:max_requeues] = v
|
84
|
+
end
|
85
|
+
|
86
|
+
o.on_tail("-h", "--help", "Show this message.") do
|
87
|
+
puts o
|
88
|
+
exit
|
89
|
+
end
|
90
|
+
|
91
|
+
o.on_tail("-v", "--version", "Print the version and exit.") do
|
92
|
+
puts "#{name} #{RSpecQ::VERSION}"
|
93
|
+
exit
|
94
|
+
end
|
41
95
|
end.parse!
|
42
96
|
|
43
|
-
[:
|
44
|
-
|
97
|
+
opts[:build] ||= ENV["RSPECQ_BUILD"]
|
98
|
+
opts[:worker] ||= ENV["RSPECQ_WORKER"]
|
99
|
+
opts[:redis_host] ||= ENV["RSPECQ_REDIS"] || DEFAULT_REDIS_HOST
|
100
|
+
opts[:timings] ||= env_set?("RSPECQ_UPDATE_TIMINGS")
|
101
|
+
opts[:file_split_threshold] ||= Integer(ENV["RSPECQ_FILE_SPLIT_THRESHOLD"] || 9999999)
|
102
|
+
opts[:report] ||= env_set?("RSPECQ_REPORT")
|
103
|
+
opts[:report_timeout] ||= Integer(ENV["RSPECQ_REPORT_TIMEOUT"] || DEFAULT_REPORT_TIMEOUT)
|
104
|
+
opts[:max_requeues] ||= Integer(ENV["RSPECQ_MAX_REQUEUES"] || DEFAULT_MAX_REQUEUES)
|
105
|
+
opts[:redis_url] ||= ENV["RSPECQ_REDIS_URL"]
|
106
|
+
|
107
|
+
raise OptionParser::MissingArgument.new(:build) if opts[:build].nil?
|
108
|
+
raise OptionParser::MissingArgument.new(:worker) if !opts[:report] && opts[:worker].nil?
|
109
|
+
|
110
|
+
redis_opts = {}
|
111
|
+
|
112
|
+
if opts[:redis_url]
|
113
|
+
redis_opts[:url] = opts[:redis_url]
|
114
|
+
else
|
115
|
+
redis_opts[:host] = opts[:redis_host]
|
45
116
|
end
|
46
117
|
|
47
118
|
if opts[:report]
|
48
119
|
reporter = RSpecQ::Reporter.new(
|
49
|
-
build_id: opts[:
|
50
|
-
|
51
|
-
|
52
|
-
redis_host: opts[:redis_host],
|
120
|
+
build_id: opts[:build],
|
121
|
+
timeout: opts[:report_timeout],
|
122
|
+
redis_opts: redis_opts,
|
53
123
|
)
|
54
124
|
|
55
125
|
reporter.report
|
56
126
|
else
|
57
127
|
worker = RSpecQ::Worker.new(
|
58
|
-
build_id: opts[:
|
59
|
-
worker_id: opts[:
|
60
|
-
|
61
|
-
files_or_dirs_to_run: ARGV[0] || "spec",
|
128
|
+
build_id: opts[:build],
|
129
|
+
worker_id: opts[:worker],
|
130
|
+
redis_opts: redis_opts
|
62
131
|
)
|
63
132
|
|
133
|
+
worker.files_or_dirs_to_run = ARGV[0] if ARGV[0]
|
64
134
|
worker.populate_timings = opts[:timings]
|
65
|
-
worker.file_split_threshold = opts[:file_split_threshold]
|
135
|
+
worker.file_split_threshold = opts[:file_split_threshold]
|
136
|
+
worker.max_requeues = opts[:max_requeues]
|
66
137
|
worker.work
|
67
138
|
end
|
data/lib/rspecq.rb
CHANGED
@@ -1,11 +1,10 @@
|
|
1
1
|
require "rspec/core"
|
2
|
+
require "sentry-raven"
|
2
3
|
|
3
4
|
module RSpecQ
|
4
|
-
|
5
|
-
|
6
|
-
#
|
7
|
-
# (in seconds), it is considered dead and its reserved work will be put back
|
8
|
-
# to the queue, to be picked up by another worker.
|
5
|
+
# If a worker haven't executed an example for more than WORKER_LIVENESS_SEC
|
6
|
+
# seconds, it is considered dead and its reserved work will be put back
|
7
|
+
# to the queue to be picked up by another worker.
|
9
8
|
WORKER_LIVENESS_SEC = 60.0
|
10
9
|
end
|
11
10
|
|
@@ -16,6 +15,5 @@ require_relative "rspecq/formatters/worker_heartbeat_recorder"
|
|
16
15
|
|
17
16
|
require_relative "rspecq/queue"
|
18
17
|
require_relative "rspecq/reporter"
|
19
|
-
require_relative "rspecq/worker"
|
20
|
-
|
21
18
|
require_relative "rspecq/version"
|
19
|
+
require_relative "rspecq/worker"
|
@@ -1,11 +1,12 @@
|
|
1
1
|
module RSpecQ
|
2
2
|
module Formatters
|
3
3
|
class FailureRecorder
|
4
|
-
def initialize(queue, job)
|
4
|
+
def initialize(queue, job, max_requeues)
|
5
5
|
@queue = queue
|
6
6
|
@job = job
|
7
7
|
@colorizer = RSpec::Core::Formatters::ConsoleCodes
|
8
8
|
@non_example_error_recorded = false
|
9
|
+
@max_requeues = max_requeues
|
9
10
|
end
|
10
11
|
|
11
12
|
# Here we're notified about errors occuring outside of examples.
|
@@ -24,7 +25,7 @@ module RSpecQ
|
|
24
25
|
def example_failed(notification)
|
25
26
|
example = notification.example
|
26
27
|
|
27
|
-
if @queue.requeue_job(example.id,
|
28
|
+
if @queue.requeue_job(example.id, @max_requeues)
|
28
29
|
# HACK: try to avoid picking the job we just requeued; we want it
|
29
30
|
# to be picked up by a different worker
|
30
31
|
sleep 0.5
|
data/lib/rspecq/queue.rb
CHANGED
@@ -1,6 +1,17 @@
|
|
1
1
|
require "redis"
|
2
2
|
|
3
3
|
module RSpecQ
|
4
|
+
# Queue is the data store interface (Redis) and is used to manage the work
|
5
|
+
# queue for a particular build. All Redis operations happen via Queue.
|
6
|
+
#
|
7
|
+
# A queue typically contains all the data needed for a particular build to
|
8
|
+
# happen. These include (but are not limited to) the following:
|
9
|
+
#
|
10
|
+
# - the list of jobs (spec files and/or examples) to be executed
|
11
|
+
# - the failed examples along with their backtrace
|
12
|
+
# - the set of running jobs
|
13
|
+
# - previous job timing statistics used to optimally schedule the jobs
|
14
|
+
# - the set of executed jobs
|
4
15
|
class Queue
|
5
16
|
RESERVE_JOB = <<~LUA.freeze
|
6
17
|
local queue = KEYS[1]
|
@@ -57,10 +68,12 @@ module RSpecQ
|
|
57
68
|
STATUS_INITIALIZING = "initializing".freeze
|
58
69
|
STATUS_READY = "ready".freeze
|
59
70
|
|
60
|
-
|
71
|
+
attr_reader :redis
|
72
|
+
|
73
|
+
def initialize(build_id, worker_id, redis_opts)
|
61
74
|
@build_id = build_id
|
62
75
|
@worker_id = worker_id
|
63
|
-
@redis = Redis.new(
|
76
|
+
@redis = Redis.new(redis_opts.merge(id: worker_id))
|
64
77
|
end
|
65
78
|
|
66
79
|
# NOTE: jobs will be processed from head to tail (lpop)
|
@@ -150,13 +163,21 @@ module RSpecQ
|
|
150
163
|
end
|
151
164
|
|
152
165
|
def example_count
|
153
|
-
@redis.get(key_example_count)
|
166
|
+
@redis.get(key_example_count).to_i
|
154
167
|
end
|
155
168
|
|
156
169
|
def processed_jobs_count
|
157
170
|
@redis.scard(key_queue_processed)
|
158
171
|
end
|
159
172
|
|
173
|
+
def processed_jobs
|
174
|
+
@redis.smembers(key_queue_processed)
|
175
|
+
end
|
176
|
+
|
177
|
+
def requeued_jobs
|
178
|
+
@redis.hgetall(key_requeues)
|
179
|
+
end
|
180
|
+
|
160
181
|
def become_master
|
161
182
|
@redis.setnx(key_queue_status, STATUS_INITIALIZING)
|
162
183
|
end
|
@@ -174,6 +195,7 @@ module RSpecQ
|
|
174
195
|
@redis.hgetall(key_errors)
|
175
196
|
end
|
176
197
|
|
198
|
+
# True if the build is complete, false otherwise
|
177
199
|
def exhausted?
|
178
200
|
return false if !published?
|
179
201
|
|
@@ -200,10 +222,23 @@ module RSpecQ
|
|
200
222
|
exhausted? && example_failures.empty? && non_example_errors.empty?
|
201
223
|
end
|
202
224
|
|
203
|
-
|
225
|
+
# The remaining jobs to be processed. Jobs at the head of the list will
|
226
|
+
# be procesed first.
|
227
|
+
def unprocessed_jobs
|
228
|
+
@redis.lrange(key_queue_unprocessed, 0, -1)
|
229
|
+
end
|
204
230
|
|
205
|
-
|
206
|
-
|
231
|
+
# Returns the jobs considered flaky (i.e. initially failed but passed
|
232
|
+
# after being retried). Must be called after the build is complete,
|
233
|
+
# otherwise an exception will be raised.
|
234
|
+
def flaky_jobs
|
235
|
+
raise "Queue is not yet exhausted" if !exhausted?
|
236
|
+
|
237
|
+
requeued = @redis.hkeys(key_requeues)
|
238
|
+
|
239
|
+
return [] if requeued.empty?
|
240
|
+
|
241
|
+
requeued - @redis.hkeys(key_failures)
|
207
242
|
end
|
208
243
|
|
209
244
|
# redis: STRING [STATUS_INITIALIZING, STATUS_READY]
|
@@ -279,6 +314,12 @@ module RSpecQ
|
|
279
314
|
"build_times"
|
280
315
|
end
|
281
316
|
|
317
|
+
private
|
318
|
+
|
319
|
+
def key(*keys)
|
320
|
+
[@build_id, keys].join(":")
|
321
|
+
end
|
322
|
+
|
282
323
|
# We don't use any Ruby `Time` methods because specs that use timecop in
|
283
324
|
# before(:all) hooks will mess up our times.
|
284
325
|
def current_time
|
data/lib/rspecq/reporter.rb
CHANGED
@@ -1,10 +1,18 @@
|
|
1
1
|
module RSpecQ
|
2
|
+
# A Reporter, given a build ID, is responsible for consolidating the results
|
3
|
+
# from different workers and printing a complete build summary to the user,
|
4
|
+
# along with any failures that might have occured.
|
5
|
+
#
|
6
|
+
# The failures are printed in real-time as they occur, while the final
|
7
|
+
# summary is printed after the queue is empty and no tests are being
|
8
|
+
# executed. If the build failed, the status code of the reporter is non-zero.
|
9
|
+
#
|
10
|
+
# Reporters are readers of the queue.
|
2
11
|
class Reporter
|
3
|
-
def initialize(build_id:,
|
12
|
+
def initialize(build_id:, timeout:, redis_opts:)
|
4
13
|
@build_id = build_id
|
5
|
-
@worker_id = worker_id
|
6
14
|
@timeout = timeout
|
7
|
-
@queue = Queue.new(build_id,
|
15
|
+
@queue = Queue.new(build_id, "reporter", redis_opts)
|
8
16
|
|
9
17
|
# We want feedback to be immediattely printed to CI users, so
|
10
18
|
# we disable buffering.
|
@@ -12,7 +20,7 @@ module RSpecQ
|
|
12
20
|
end
|
13
21
|
|
14
22
|
def report
|
15
|
-
|
23
|
+
@queue.wait_until_published
|
16
24
|
|
17
25
|
finished = false
|
18
26
|
|
@@ -46,8 +54,13 @@ module RSpecQ
|
|
46
54
|
raise "Build not finished after #{@timeout} seconds" if !finished
|
47
55
|
|
48
56
|
@queue.record_build_time(tests_duration)
|
57
|
+
|
58
|
+
flaky_jobs = @queue.flaky_jobs
|
59
|
+
|
49
60
|
puts summary(@queue.example_failures, @queue.non_example_errors,
|
50
|
-
|
61
|
+
flaky_jobs, humanize_duration(tests_duration))
|
62
|
+
|
63
|
+
flaky_jobs_to_sentry(flaky_jobs, tests_duration)
|
51
64
|
|
52
65
|
exit 1 if !@queue.build_successful?
|
53
66
|
end
|
@@ -61,7 +74,7 @@ module RSpecQ
|
|
61
74
|
end
|
62
75
|
|
63
76
|
# We try to keep this output consistent with RSpec's original output
|
64
|
-
def summary(failures, errors, duration)
|
77
|
+
def summary(failures, errors, flaky_jobs, duration)
|
65
78
|
failed_examples_section = "\nFailed examples:\n\n"
|
66
79
|
|
67
80
|
failures.each do |_job, msg|
|
@@ -82,6 +95,14 @@ module RSpecQ
|
|
82
95
|
"#{errors.count} errors"
|
83
96
|
summary << "\n\n"
|
84
97
|
summary << "Spec execution time: #{duration}"
|
98
|
+
|
99
|
+
if !flaky_jobs.empty?
|
100
|
+
summary << "\n\n"
|
101
|
+
summary << "Flaky jobs detected (count=#{flaky_jobs.count}):\n"
|
102
|
+
flaky_jobs.each { |j| summary << " #{j}\n" }
|
103
|
+
end
|
104
|
+
|
105
|
+
summary
|
85
106
|
end
|
86
107
|
|
87
108
|
def failure_formatted(rspec_output)
|
@@ -91,5 +112,35 @@ module RSpecQ
|
|
91
112
|
def humanize_duration(seconds)
|
92
113
|
Time.at(seconds).utc.strftime("%H:%M:%S")
|
93
114
|
end
|
115
|
+
|
116
|
+
def flaky_jobs_to_sentry(jobs, build_duration)
|
117
|
+
return if jobs.empty?
|
118
|
+
|
119
|
+
jobs.each do |job|
|
120
|
+
filename = job.sub(/\[.+\]/, '')
|
121
|
+
|
122
|
+
extra = {
|
123
|
+
build: @build_id,
|
124
|
+
build_timeout: @timeout,
|
125
|
+
queue: @queue.inspect,
|
126
|
+
object: self.inspect,
|
127
|
+
pid: Process.pid,
|
128
|
+
job_path: job,
|
129
|
+
build_duration: build_duration
|
130
|
+
}
|
131
|
+
|
132
|
+
tags = {
|
133
|
+
flaky: true,
|
134
|
+
spec_file: filename
|
135
|
+
}
|
136
|
+
|
137
|
+
Raven.capture_message(
|
138
|
+
"Flaky test in #{filename}",
|
139
|
+
level: 'warning',
|
140
|
+
extra: extra,
|
141
|
+
tags: tags
|
142
|
+
)
|
143
|
+
end
|
144
|
+
end
|
94
145
|
end
|
95
146
|
end
|
data/lib/rspecq/version.rb
CHANGED
data/lib/rspecq/worker.rb
CHANGED
@@ -1,10 +1,28 @@
|
|
1
1
|
require "json"
|
2
|
+
require "pathname"
|
2
3
|
require "pp"
|
4
|
+
require "open3"
|
3
5
|
|
4
6
|
module RSpecQ
|
7
|
+
# A Worker, given a build ID, continuously consumes tests off the
|
8
|
+
# corresponding and executes them, until the queue is empty.
|
9
|
+
# It is also responsible for populating the initial queue.
|
10
|
+
#
|
11
|
+
# Essentially, a worker is an RSpec runner that prints the results of the
|
12
|
+
# tests it executes to standard output.
|
13
|
+
#
|
14
|
+
# The typical use case is to spawn many workers for a given build, thereby
|
15
|
+
# parallelizing the work and achieving faster build times.
|
16
|
+
#
|
17
|
+
# Workers are readers+writers of the queue.
|
5
18
|
class Worker
|
6
19
|
HEARTBEAT_FREQUENCY = WORKER_LIVENESS_SEC / 6
|
7
20
|
|
21
|
+
# The root path or individual spec files to execute.
|
22
|
+
#
|
23
|
+
# Defaults to "spec" (similar to RSpec)
|
24
|
+
attr_accessor :files_or_dirs_to_run
|
25
|
+
|
8
26
|
# If true, job timings will be populated in the global Redis timings key
|
9
27
|
#
|
10
28
|
# Defaults to false
|
@@ -12,15 +30,27 @@ module RSpecQ
|
|
12
30
|
|
13
31
|
# If set, spec files that are known to take more than this value to finish,
|
14
32
|
# will be split and scheduled on a per-example basis.
|
33
|
+
#
|
34
|
+
# Defaults to 999999
|
15
35
|
attr_accessor :file_split_threshold
|
16
36
|
|
17
|
-
|
37
|
+
# Retry failed examples up to N times (with N being the supplied value)
|
38
|
+
# before considering them legit failures
|
39
|
+
#
|
40
|
+
# Defaults to 3
|
41
|
+
attr_accessor :max_requeues
|
42
|
+
|
43
|
+
attr_reader :queue
|
44
|
+
|
45
|
+
def initialize(build_id:, worker_id:, redis_opts:)
|
18
46
|
@build_id = build_id
|
19
47
|
@worker_id = worker_id
|
20
|
-
@queue = Queue.new(build_id, worker_id,
|
21
|
-
@files_or_dirs_to_run =
|
48
|
+
@queue = Queue.new(build_id, worker_id, redis_opts)
|
49
|
+
@files_or_dirs_to_run = "spec"
|
22
50
|
@populate_timings = false
|
23
51
|
@file_split_threshold = 999999
|
52
|
+
@heartbeat_updated_at = nil
|
53
|
+
@max_requeues = 3
|
24
54
|
|
25
55
|
RSpec::Core::Formatters.register(Formatters::JobTimingRecorder, :dump_summary)
|
26
56
|
RSpec::Core::Formatters.register(Formatters::ExampleCountRecorder, :dump_summary)
|
@@ -31,23 +61,23 @@ module RSpecQ
|
|
31
61
|
def work
|
32
62
|
puts "Working for build #{@build_id} (worker=#{@worker_id})"
|
33
63
|
|
34
|
-
try_publish_queue!(
|
35
|
-
|
64
|
+
try_publish_queue!(queue)
|
65
|
+
queue.wait_until_published
|
36
66
|
|
37
67
|
loop do
|
38
68
|
# we have to bootstrap this so that it can be used in the first call
|
39
69
|
# to `requeue_lost_job` inside the work loop
|
40
70
|
update_heartbeat
|
41
71
|
|
42
|
-
lost =
|
72
|
+
lost = queue.requeue_lost_job
|
43
73
|
puts "Requeued lost job: #{lost}" if lost
|
44
74
|
|
45
75
|
# TODO: can we make `reserve_job` also act like exhausted? and get
|
46
76
|
# rid of `exhausted?` (i.e. return false if no jobs remain)
|
47
|
-
job =
|
77
|
+
job = queue.reserve_job
|
48
78
|
|
49
79
|
# build is finished
|
50
|
-
return if job.nil? &&
|
80
|
+
return if job.nil? && queue.exhausted?
|
51
81
|
|
52
82
|
next if job.nil?
|
53
83
|
|
@@ -60,112 +90,125 @@ module RSpecQ
|
|
60
90
|
RSpec.configuration.detail_color = :magenta
|
61
91
|
RSpec.configuration.seed = srand && srand % 0xFFFF
|
62
92
|
RSpec.configuration.backtrace_formatter.filter_gem('rspecq')
|
63
|
-
RSpec.configuration.add_formatter(Formatters::FailureRecorder.new(
|
64
|
-
RSpec.configuration.add_formatter(Formatters::ExampleCountRecorder.new(
|
93
|
+
RSpec.configuration.add_formatter(Formatters::FailureRecorder.new(queue, job, max_requeues))
|
94
|
+
RSpec.configuration.add_formatter(Formatters::ExampleCountRecorder.new(queue))
|
65
95
|
RSpec.configuration.add_formatter(Formatters::WorkerHeartbeatRecorder.new(self))
|
66
96
|
|
67
97
|
if populate_timings
|
68
|
-
RSpec.configuration.add_formatter(Formatters::JobTimingRecorder.new(
|
98
|
+
RSpec.configuration.add_formatter(Formatters::JobTimingRecorder.new(queue, job))
|
69
99
|
end
|
70
100
|
|
71
101
|
opts = RSpec::Core::ConfigurationOptions.new(["--format", "progress", job])
|
72
102
|
_result = RSpec::Core::Runner.new(opts).run($stderr, $stdout)
|
73
103
|
|
74
|
-
|
104
|
+
queue.acknowledge_job(job)
|
75
105
|
end
|
76
106
|
end
|
77
107
|
|
78
108
|
# Update the worker heartbeat if necessary
|
79
109
|
def update_heartbeat
|
80
110
|
if @heartbeat_updated_at.nil? || elapsed(@heartbeat_updated_at) >= HEARTBEAT_FREQUENCY
|
81
|
-
|
111
|
+
queue.record_worker_heartbeat
|
82
112
|
@heartbeat_updated_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
83
113
|
end
|
84
114
|
end
|
85
115
|
|
86
|
-
private
|
87
|
-
|
88
|
-
def reset_rspec_state!
|
89
|
-
RSpec.clear_examples
|
90
|
-
|
91
|
-
# TODO: remove after https://github.com/rspec/rspec-core/pull/2723
|
92
|
-
RSpec.world.instance_variable_set(:@example_group_counts_by_spec_file, Hash.new(0))
|
93
|
-
|
94
|
-
# RSpec.clear_examples does not reset those, which causes issues when
|
95
|
-
# a non-example error occurs (subsequent jobs are not executed)
|
96
|
-
# TODO: upstream
|
97
|
-
RSpec.world.non_example_failure = false
|
98
|
-
|
99
|
-
# we don't want an error that occured outside of the examples (which
|
100
|
-
# would set this to `true`) to stop the worker
|
101
|
-
RSpec.world.wants_to_quit = false
|
102
|
-
end
|
103
|
-
|
104
116
|
def try_publish_queue!(queue)
|
105
117
|
return if !queue.become_master
|
106
118
|
|
107
|
-
RSpec.configuration.files_or_directories_to_run =
|
119
|
+
RSpec.configuration.files_or_directories_to_run = files_or_dirs_to_run
|
108
120
|
files_to_run = RSpec.configuration.files_to_run.map { |j| relative_path(j) }
|
109
121
|
|
110
122
|
timings = queue.timings
|
111
123
|
if timings.empty?
|
112
|
-
# TODO: should be a warning reported somewhere (Sentry?)
|
113
124
|
q_size = queue.publish(files_to_run.shuffle)
|
114
|
-
|
115
|
-
|
125
|
+
log_event(
|
126
|
+
"No timings found! Published queue in random order (size=#{q_size})",
|
127
|
+
"warning"
|
128
|
+
)
|
116
129
|
return
|
117
130
|
end
|
118
131
|
|
119
|
-
|
120
|
-
|
121
|
-
|
132
|
+
# prepare jobs to run
|
133
|
+
jobs = []
|
134
|
+
slow_files = []
|
122
135
|
|
123
|
-
if
|
124
|
-
|
136
|
+
if file_split_threshold
|
137
|
+
slow_files = timings.take_while do |_job, duration|
|
138
|
+
duration >= file_split_threshold
|
139
|
+
end.map(&:first) & files_to_run
|
125
140
|
end
|
126
141
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
142
|
+
if slow_files.any?
|
143
|
+
jobs.concat(files_to_run - slow_files)
|
144
|
+
jobs.concat(files_to_example_ids(slow_files))
|
145
|
+
else
|
146
|
+
jobs.concat(files_to_run)
|
147
|
+
end
|
131
148
|
|
132
|
-
# assign timings to all of them
|
133
149
|
default_timing = timings.values[timings.values.size/2]
|
134
150
|
|
151
|
+
# assign timings (based on previous runs) to all jobs
|
135
152
|
jobs = jobs.each_with_object({}) do |j, h|
|
136
|
-
|
137
|
-
|
153
|
+
puts "Untimed job: #{j}" if timings[j].nil?
|
154
|
+
|
155
|
+
# HEURISTIC: put jobs without previous timings (e.g. a newly added
|
156
|
+
# spec file) in the middle of the queue
|
138
157
|
h[j] = timings[j] || default_timing
|
139
158
|
end
|
140
159
|
|
141
|
-
#
|
160
|
+
# sort jobs based on their timings (slowest to be processed first)
|
142
161
|
jobs = jobs.sort_by { |_j, t| -t }.map(&:first)
|
143
162
|
|
144
163
|
puts "Published queue (size=#{queue.publish(jobs)})"
|
145
164
|
end
|
146
165
|
|
166
|
+
private
|
167
|
+
|
168
|
+
def reset_rspec_state!
|
169
|
+
RSpec.clear_examples
|
170
|
+
|
171
|
+
# see https://github.com/rspec/rspec-core/pull/2723
|
172
|
+
if Gem::Version.new(RSpec::Core::Version::STRING) <= Gem::Version.new("3.9.1")
|
173
|
+
RSpec.world.instance_variable_set(
|
174
|
+
:@example_group_counts_by_spec_file, Hash.new(0))
|
175
|
+
end
|
176
|
+
|
177
|
+
# RSpec.clear_examples does not reset those, which causes issues when
|
178
|
+
# a non-example error occurs (subsequent jobs are not executed)
|
179
|
+
# TODO: upstream
|
180
|
+
RSpec.world.non_example_failure = false
|
181
|
+
|
182
|
+
# we don't want an error that occured outside of the examples (which
|
183
|
+
# would set this to `true`) to stop the worker
|
184
|
+
RSpec.world.wants_to_quit = false
|
185
|
+
end
|
186
|
+
|
147
187
|
# NOTE: RSpec has to load the files before we can split them as individual
|
148
188
|
# examples. In case a file to be splitted fails to be loaded
|
149
|
-
# (e.g. contains a syntax error), we return the
|
150
|
-
#
|
151
|
-
#
|
152
|
-
# Their errors will be reported in the normal flow, when they're picked up
|
153
|
-
# as jobs by a worker.
|
189
|
+
# (e.g. contains a syntax error), we return the files unchanged, thereby
|
190
|
+
# falling back to scheduling them as whole files. Their errors will be
|
191
|
+
# reported in the normal flow when they're eventually picked up by a worker.
|
154
192
|
def files_to_example_ids(files)
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
193
|
+
cmd = "DISABLE_SPRING=1 bundle exec rspec --dry-run --format json #{files.join(' ')}"
|
194
|
+
out, err, cmd_result = Open3.capture3(cmd)
|
195
|
+
|
196
|
+
if !cmd_result.success?
|
197
|
+
rspec_output = begin
|
198
|
+
JSON.parse(out)
|
199
|
+
rescue JSON::ParserError
|
200
|
+
out
|
201
|
+
end
|
202
|
+
|
203
|
+
log_event(
|
204
|
+
"Failed to split slow files, falling back to regular scheduling.\n #{err}",
|
205
|
+
"error",
|
206
|
+
rspec_stdout: rspec_output,
|
207
|
+
rspec_stderr: err,
|
208
|
+
cmd_result: cmd_result.inspect,
|
209
|
+
)
|
210
|
+
|
211
|
+
pp rspec_output
|
169
212
|
|
170
213
|
return files
|
171
214
|
end
|
@@ -181,5 +224,23 @@ module RSpecQ
|
|
181
224
|
def elapsed(since)
|
182
225
|
Process.clock_gettime(Process::CLOCK_MONOTONIC) - since
|
183
226
|
end
|
227
|
+
|
228
|
+
# Prints msg to standard output and emits an event to Sentry, if the
|
229
|
+
# SENTRY_DSN environment variable is set.
|
230
|
+
def log_event(msg, level, additional={})
|
231
|
+
puts msg
|
232
|
+
|
233
|
+
Raven.capture_message(msg, level: level, extra: {
|
234
|
+
build: @build_id,
|
235
|
+
worker: @worker_id,
|
236
|
+
queue: queue.inspect,
|
237
|
+
files_or_dirs_to_run: files_or_dirs_to_run,
|
238
|
+
populate_timings: populate_timings,
|
239
|
+
file_split_threshold: file_split_threshold,
|
240
|
+
heartbeat_updated_at: @heartbeat_updated_at,
|
241
|
+
object: self.inspect,
|
242
|
+
pid: Process.pid,
|
243
|
+
}.merge(additional))
|
244
|
+
end
|
184
245
|
end
|
185
246
|
end
|
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.0
|
4
|
+
version: 0.3.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: 2020-
|
11
|
+
date: 2020-10-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec-core
|
@@ -38,22 +38,64 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sentry-raven
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pry-byebug
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
41
83
|
- !ruby/object:Gem::Dependency
|
42
84
|
name: minitest
|
43
85
|
requirement: !ruby/object:Gem::Requirement
|
44
86
|
requirements:
|
45
|
-
- - "
|
87
|
+
- - ">="
|
46
88
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
89
|
+
version: '0'
|
48
90
|
type: :development
|
49
91
|
prerelease: false
|
50
92
|
version_requirements: !ruby/object:Gem::Requirement
|
51
93
|
requirements:
|
52
|
-
- - "
|
94
|
+
- - ">="
|
53
95
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
96
|
+
version: '0'
|
55
97
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
98
|
+
name: rspec
|
57
99
|
requirement: !ruby/object:Gem::Requirement
|
58
100
|
requirements:
|
59
101
|
- - ">="
|
@@ -76,8 +118,10 @@ files:
|
|
76
118
|
- CHANGELOG.md
|
77
119
|
- LICENSE
|
78
120
|
- README.md
|
121
|
+
- Rakefile
|
79
122
|
- bin/rspecq
|
80
123
|
- lib/rspecq.rb
|
124
|
+
- lib/rspecq/formatters/README.md
|
81
125
|
- lib/rspecq/formatters/example_count_recorder.rb
|
82
126
|
- lib/rspecq/formatters/failure_recorder.rb
|
83
127
|
- lib/rspecq/formatters/job_timing_recorder.rb
|
@@ -101,12 +145,13 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
101
145
|
version: '0'
|
102
146
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
147
|
requirements:
|
104
|
-
- - "
|
148
|
+
- - ">="
|
105
149
|
- !ruby/object:Gem::Version
|
106
|
-
version:
|
150
|
+
version: '0'
|
107
151
|
requirements: []
|
108
|
-
rubygems_version: 3.1.
|
152
|
+
rubygems_version: 3.1.4
|
109
153
|
signing_key:
|
110
154
|
specification_version: 4
|
111
|
-
summary:
|
155
|
+
summary: Optimally distribute and run RSpec suites among parallel workers; for faster
|
156
|
+
CI builds
|
112
157
|
test_files: []
|