rspecq-instructure 0.8.1
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 +7 -0
- data/CHANGELOG.md +106 -0
- data/LICENSE +20 -0
- data/README.md +268 -0
- data/Rakefile +10 -0
- data/bin/rspecq +49 -0
- data/lib/rspecq/configuration.rb +103 -0
- data/lib/rspecq/formatters/README.md +4 -0
- data/lib/rspecq/formatters/example_count_recorder.rb +15 -0
- data/lib/rspecq/formatters/failure_recorder.rb +62 -0
- data/lib/rspecq/formatters/job_timing_recorder.rb +23 -0
- data/lib/rspecq/formatters/junit_formatter.rb +53 -0
- data/lib/rspecq/formatters/worker_heartbeat_recorder.rb +16 -0
- data/lib/rspecq/parser.rb +252 -0
- data/lib/rspecq/queue.rb +634 -0
- data/lib/rspecq/reporter.rb +195 -0
- data/lib/rspecq/version.rb +3 -0
- data/lib/rspecq/worker.rb +418 -0
- data/lib/rspecq.rb +15 -0
- metadata +206 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1df575298a210ef26b7f057dcfd500287519e2ec6f8d32b2296f6eea26bc05fc
|
|
4
|
+
data.tar.gz: fe66df1684e18af2501fb67cf26ce475a9db89e65717f83e0ff1f3bb9455041c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 52b16ee95c80f6a7da5246c6335910dde1a897d7ce37ac7d5f99b508aa306d3cdce350d47fc7d641d1c4947f894345eea3d49844df04aab5aa0ae7de97c51bc1
|
|
7
|
+
data.tar.gz: 31ec697c4f6b54c39a8dad7527d6151e177e45a81ad55e74e6a07ad459ae3c430bf02d35a8674817488b9e56008941875e371e73a293d69e8646ac446aefc7b2
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Breaking changes are prefixed with a "[BREAKING]" label.
|
|
4
|
+
|
|
5
|
+
## 0.8.1 (unreleased)
|
|
6
|
+
|
|
7
|
+
- [BREAKING] The gem is published to rubygems.org as `rspecq-instructure`,
|
|
8
|
+
not to Artifactory as `rspecq`. Change the dependency to
|
|
9
|
+
`gem "rspecq-instructure"` and drop the Artifactory source; credentials are
|
|
10
|
+
no longer needed to `bundle install`.
|
|
11
|
+
|
|
12
|
+
Only the package name changed. Everything the gem provides keeps its old
|
|
13
|
+
name, so no code has to change: `require "rspecq"`, the `RSpecQ` namespace,
|
|
14
|
+
and the `rspecq` executable all still work as before.
|
|
15
|
+
|
|
16
|
+
## 0.8.0.instructure2 (unreleased)
|
|
17
|
+
|
|
18
|
+
- [BREAKING] Require Ruby >= 3.2 and raise the redis-rb floor to `>= 5.0`
|
|
19
|
+
(cap `< 7.0`). Verified compatible with redis-rb 6.0 / RESP3.
|
|
20
|
+
- `REDIS_GEM` env var pins the redis-rb version in CI (mirrors `RSPEC_CORE`).
|
|
21
|
+
|
|
22
|
+
## 0.7.1 (2021-04-08)
|
|
23
|
+
|
|
24
|
+
- New env variable RSPECQ_REPORTER_RERUN_COMMAND_SKIP. When set, the reporter
|
|
25
|
+
does not include the flaky test's rerun command.
|
|
26
|
+
|
|
27
|
+
## 0.7.0 (2021-04-01)
|
|
28
|
+
|
|
29
|
+
- New cli parameter `reproduction`.
|
|
30
|
+
When passed, primary worker publishes the queue in the same order as passed
|
|
31
|
+
in the command.
|
|
32
|
+
- Reporter now includes a reproduction command for flaky tests.
|
|
33
|
+
|
|
34
|
+
## 0.6.0 (2021-03-23)
|
|
35
|
+
|
|
36
|
+
- New cli parameter `seed`.
|
|
37
|
+
The seed is passed to the RSpec command.
|
|
38
|
+
|
|
39
|
+
## 0.5.0 (2021-02-05)
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- New cli parameter `queue_wait_timeout`.
|
|
44
|
+
It configured the time a queue can wait to be ready. The env equivalent
|
|
45
|
+
is `RSPECQ_QUEUE_WAIT_TIMEOUT`. [#51](https://github.com/skroutz/rspecq/pull/51)
|
|
46
|
+
|
|
47
|
+
## 0.4.0 (2020-10-07)
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- Builds can be configured to terminate after a specified number of failures,
|
|
52
|
+
using the `--fail-fast` option.
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
## 0.3.0 (2020-10-05)
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- Providing a Redis URL is now possible using the `--redis-url` option
|
|
60
|
+
[[#40](https://github.com/skroutz/rspecq/pull/40)]
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
|
|
64
|
+
- [DEPRECATION] The `--redis` option is now deprecated. Use `--redis-host`
|
|
65
|
+
instead [[#40](https://github.com/skroutz/rspecq/pull/40)]
|
|
66
|
+
|
|
67
|
+
## 0.2.2 (2020-09-10)
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
- Worker would fail if application code was writing to stderr
|
|
71
|
+
[[#35](https://github.com/skroutz/rspecq/pull/35)]
|
|
72
|
+
|
|
73
|
+
## 0.2.1 (2020-09-09)
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
|
|
77
|
+
- Sentry Integration: Changed the way events for flaky jobs are emitted to a
|
|
78
|
+
per-flaky-job fashion. This ultimately improves grouping and filtering of the
|
|
79
|
+
flaky events in Sentry [[#33](https://github.com/skroutz/rspecq/pull/33)]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
## 0.2.0 (2020-08-31)
|
|
83
|
+
|
|
84
|
+
This is a feature release with no breaking changes.
|
|
85
|
+
|
|
86
|
+
### Added
|
|
87
|
+
|
|
88
|
+
- Flaky jobs are now printed by the reporter in the final build output and also
|
|
89
|
+
emitted to Sentry (if the integration is enabled) [[#26](https://github.com/skroutz/rspecq/pull/26)]
|
|
90
|
+
|
|
91
|
+
## 0.1.0 (2020-08-27)
|
|
92
|
+
|
|
93
|
+
### Added
|
|
94
|
+
|
|
95
|
+
- Sentry integration for various RSpecQ-level events [[#16](https://github.com/skroutz/rspecq/pull/16)]
|
|
96
|
+
- CLI: Flags can now be also set environment variables [[c519230](https://github.com/skroutz/rspecq/commit/c5192303e229f361e8ac86ae449b4ea84d42e022)]
|
|
97
|
+
- CLI: Added shorthand specifiers versions for some flags [[df9faa8](https://github.com/skroutz/rspecq/commit/df9faa8ec6721af8357cfee4de6a2fe7b32070fc)]
|
|
98
|
+
- CLI: Added `--help` and `--version` flags [[df9faa8](https://github.com/skroutz/rspecq/commit/df9faa8ec6721af8357cfee4de6a2fe7b32070fc)]
|
|
99
|
+
- CLI: Max number of retries for failed examples is now configurable via the `--max-requeues` option [[#14](https://github.com/skroutz/rspecq/pull/14)]
|
|
100
|
+
|
|
101
|
+
### Changed
|
|
102
|
+
|
|
103
|
+
- [BREAKING] CLI: Renamed `--timings` to `--update-timings` [[c519230](https://github.com/skroutz/rspecq/commit/c5192303e229f361e8ac86ae449b4ea84d42e022)]
|
|
104
|
+
- [BREAKING] CLI: Renamed `--build-id` to `--build` and `--worker-id` to `--worker` [[df9faa8](https://github.com/skroutz/rspecq/commit/df9faa8ec6721af8357cfee4de6a2fe7b32070fc)]
|
|
105
|
+
- CLI: `--worker` is not required when `--reporter` is used [[4323a75](https://github.com/skroutz/rspecq/commit/4323a75ca357274069d02ba9fb51cdebb04e0be4)]
|
|
106
|
+
- CLI: Improved help output [[df9faa8](https://github.com/skroutz/rspecq/commit/df9faa8ec6721af8357cfee4de6a2fe7b32070fc)]
|
data/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
The MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Skroutz S.A.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
RSpec Queue
|
|
2
|
+
=========================================================================
|
|
3
|
+
[](https://travis-ci.com/github/skroutz/rspecq)
|
|
4
|
+
[](https://badge.fury.io/rb/rspecq)
|
|
5
|
+
|
|
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.
|
|
10
|
+
|
|
11
|
+
RSpecQ is inspired by [test-queue](https://github.com/tmm1/test-queue)
|
|
12
|
+
and [ci-queue](https://github.com/Shopify/ci-queue).
|
|
13
|
+
|
|
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
|
+
See [#2](https://github.com/skroutz/rspecq/issues/2).
|
|
30
|
+
- Automatic termination of builds after a certain amount of failures. See [*Fail-fast*](#fail-fast).
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- Ruby >= 3.2
|
|
35
|
+
- redis-rb 5.x or 6.x (runs on RESP3, the redis-rb 6.0 default), backed by a Redis server >= 6.2 (the `COPY` command is used to promote build timings).
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
A worker needs to be given a name and the build it will participate in.
|
|
40
|
+
Assuming there's a Redis instance listening at `localhost`, starting a worker
|
|
41
|
+
is as simple as:
|
|
42
|
+
|
|
43
|
+
```shell
|
|
44
|
+
$ rspecq --build=123 --worker=foo1 spec/
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
To start more workers for the same build, use distinct worker IDs but the same
|
|
48
|
+
build ID:
|
|
49
|
+
|
|
50
|
+
```shell
|
|
51
|
+
$ rspecq --build=123 --worker=foo2
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
To view the progress of the build use `--report`:
|
|
55
|
+
|
|
56
|
+
```shell
|
|
57
|
+
$ rspecq --build=123 --report
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
For detailed info use `--help`:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
NAME:
|
|
64
|
+
rspecq - Optimally distribute and run RSpec suites among parallel workers
|
|
65
|
+
|
|
66
|
+
USAGE:
|
|
67
|
+
rspecq [<options>] [spec files or directories]
|
|
68
|
+
|
|
69
|
+
OPTIONS:
|
|
70
|
+
-b, --build ID A unique identifier for the build. Should be common among workers participating in the same build.
|
|
71
|
+
-w, --worker ID An identifier for the worker. Workers participating in the same build should have distinct IDs.
|
|
72
|
+
--seed SEED The RSpec seed. Passing the seed can be helpful in many ways i.e reproduction and testing.
|
|
73
|
+
-r, --redis HOST --redis is deprecated. Use --redis-host or --redis-url instead. Redis host to connect to (default: 127.0.0.1).
|
|
74
|
+
--redis-host HOST Redis host to connect to (default: 127.0.0.1).
|
|
75
|
+
--redis-url URL Redis URL to connect to (e.g.: redis://127.0.0.1:6379/0).
|
|
76
|
+
--redis-connect-timeout N Seconds to wait when establishing a Redis connection (default: 1.0).
|
|
77
|
+
--redis-read-timeout N Seconds to wait for a Redis read (default: 5.0).
|
|
78
|
+
--redis-write-timeout N Seconds to wait for a Redis write (default: 5.0).
|
|
79
|
+
--redis-reconnect-attempts LIST Comma-separated backoff (seconds) before each Redis reconnect attempt, e.g. "0.05,0.1,0.25,0.5,1.0". Empty disables reconnects.
|
|
80
|
+
--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.
|
|
81
|
+
--file-split-threshold N Split spec files slower than N seconds and schedule them as individual examples.
|
|
82
|
+
--report Enable reporter mode: do not pull tests off the queue; instead print build progress and exit when it's finished.
|
|
83
|
+
Exits with a non-zero status code if there were any failures.
|
|
84
|
+
--report-timeout N Fail if build is not finished after N seconds. Only applicable if --report is enabled (default: 3600).
|
|
85
|
+
--max-requeues N Retry failed examples up to N times before considering them legit failures (default: 3).
|
|
86
|
+
--queue-wait-timeout N Time to wait for a queue to be ready before considering it failed (default: 30).
|
|
87
|
+
--worker-liveness-sec N Worker liveness timeout in seconds, node will be considered dead if it hasn't emitted a heartbeat for N seconds (default: 60).
|
|
88
|
+
--fail-fast N Abort build with a non-zero status code after N failed examples.
|
|
89
|
+
--reproduction Enable reproduction mode: Publish files and examples in the exact order given in the command. Incompatible with --timings.
|
|
90
|
+
-h, --help Show this message.
|
|
91
|
+
-v, --version Print the version and exit.
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
You can set most options using ENV variables:
|
|
95
|
+
|
|
96
|
+
```shell
|
|
97
|
+
$ RSPECQ_BUILD=123 RSPECQ_WORKER=foo1 rspecq spec/
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Supported ENV variables
|
|
101
|
+
|
|
102
|
+
| Name | Desc |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| `RSPECQ_BUILD` | Build ID |
|
|
105
|
+
| `RSPECQ_WORKER` | Worker ID |
|
|
106
|
+
| `RSPECQ_SEED` | RSpec seed |
|
|
107
|
+
| `RSPECQ_REDIS` | Redis HOST |
|
|
108
|
+
| `RSPECQ_UPDATE_TIMINGS` | Timings |
|
|
109
|
+
| `RSPECQ_FILE_SPLIT_THRESHOLD` | File split threshold |
|
|
110
|
+
| `RSPECQ_REPORT` | Report |
|
|
111
|
+
| `RSPECQ_REPORT_TIMEOUT` | Report Timeout |
|
|
112
|
+
| `RSPECQ_MAX_REQUEUES` | Max requests |
|
|
113
|
+
| `RSPECQ_QUEUE_WAIT_TIMEOUT` | Queue wait timeout |
|
|
114
|
+
| `RSPECQ_REDIS_URL` | Redis URL |
|
|
115
|
+
| `RSPECQ_REDIS_CONNECT_TIMEOUT` | Seconds to wait when establishing a Redis connection (default: 1.0) |
|
|
116
|
+
| `RSPECQ_REDIS_READ_TIMEOUT` | Seconds to wait for a Redis read (default: 5.0) |
|
|
117
|
+
| `RSPECQ_REDIS_WRITE_TIMEOUT` | Seconds to wait for a Redis write (default: 5.0) |
|
|
118
|
+
| `RSPECQ_REDIS_RECONNECT_ATTEMPTS` | Comma-separated backoff (seconds) before each Redis reconnect attempt (default: `0.05,0.1,0.25,0.5,1.0`); empty disables reconnects |
|
|
119
|
+
| `RSPECQ_WORKER_LIVENESS_SEC` | Worker liveness timeout in seconds |
|
|
120
|
+
| `RSPECQ_FAIL_FAST` | Fail fast |
|
|
121
|
+
| `RSPECQ_REPORTER_RERUN_COMMAND_SKIP` | Do not report flaky test's rerun command |
|
|
122
|
+
|
|
123
|
+
### Sentry integration
|
|
124
|
+
|
|
125
|
+
RSpecQ can optionally emit build events to a
|
|
126
|
+
[Sentry](https://sentry.io) project by setting the
|
|
127
|
+
`SENTRY_DSN` environment variable.
|
|
128
|
+
|
|
129
|
+
This is convenient for monitoring important warnings/errors that may impact
|
|
130
|
+
build times, such as the fact that no previous timings were found and
|
|
131
|
+
therefore job scheduling was effectively random for a particular build.
|
|
132
|
+
|
|
133
|
+
## How it works
|
|
134
|
+
|
|
135
|
+
The core design is almost identical to ci-queue so please refer to its
|
|
136
|
+
[README](https://github.com/Shopify/ci-queue/blob/master/README.md) instead.
|
|
137
|
+
|
|
138
|
+
### Terminology
|
|
139
|
+
|
|
140
|
+
- **Job**: the smallest unit of work, which is usually a spec file
|
|
141
|
+
(e.g. `./spec/models/foo_spec.rb`) but can also be an individual example
|
|
142
|
+
(e.g. `./spec/models/foo_spec.rb[1:2:1]`) if the file is too slow.
|
|
143
|
+
- **Queue**: a collection of Redis-backed structures that hold all the necessary
|
|
144
|
+
information for an RSpecQ build to run. This includes timing statistics,
|
|
145
|
+
jobs to be executed, the failure reports and more.
|
|
146
|
+
- **Build**: a particular test suite run. Each build has its own **Queue**.
|
|
147
|
+
- **Worker**: an `rspecq` process that, given a build id, consumes jobs off the
|
|
148
|
+
build's queue and executes them using RSpec
|
|
149
|
+
- **Reporter**: an `rspecq` process that, given a build id, waits for the build's
|
|
150
|
+
queue to be drained and prints the build summary report
|
|
151
|
+
|
|
152
|
+
### Spec file splitting
|
|
153
|
+
|
|
154
|
+
Particularly slow spec files may set a limit to how fast a build can be.
|
|
155
|
+
For example, a single file may need 10 minutes to run while all other
|
|
156
|
+
files finish after 8 minutes. This would cause all but one workers to be
|
|
157
|
+
sitting idle for 2 minutes.
|
|
158
|
+
|
|
159
|
+
To overcome this issue, RSpecQ can split files which their execution time is
|
|
160
|
+
above a certain threshold (set with the `--file-split-threshold` option)
|
|
161
|
+
and instead schedule them as individual examples.
|
|
162
|
+
|
|
163
|
+
Note: In the future, we'd like for the slow threshold to be calculated and set
|
|
164
|
+
dynamically (see #3).
|
|
165
|
+
|
|
166
|
+
### Requeues
|
|
167
|
+
|
|
168
|
+
As a mitigation technique against flaky tests, if an example fails it will be
|
|
169
|
+
put back to the queue to be picked up by another worker. This will be repeated
|
|
170
|
+
up to a certain number of times (set with the `--max-requeues` option), after
|
|
171
|
+
which the example will be considered a legit failure and printed as such in the
|
|
172
|
+
final report.
|
|
173
|
+
|
|
174
|
+
Flaky tests are also detected and printed as such in the final report. They are
|
|
175
|
+
also emitted to Sentry (see [Sentry integration](#sentry-integration)).
|
|
176
|
+
|
|
177
|
+
### Fail-fast
|
|
178
|
+
|
|
179
|
+
In order to prevent large suites running for a long time with a lot of
|
|
180
|
+
failures, a threshold can be set to control the number of failed examples that
|
|
181
|
+
will render the build unsuccessful. This is in par with RSpec's
|
|
182
|
+
[--fail-fast](https://relishapp.com/rspec/rspec-core/docs/command-line/fail-fast-option).
|
|
183
|
+
|
|
184
|
+
This feature is disabled by default, and can be controlled via the
|
|
185
|
+
`--fail-fast` command line option.
|
|
186
|
+
|
|
187
|
+
### Worker failures
|
|
188
|
+
|
|
189
|
+
It's not uncommon for CI processes to encounter unrecoverable failures for
|
|
190
|
+
various reasons: faulty hardware, network hiccups, segmentation faults in
|
|
191
|
+
MRI etc.
|
|
192
|
+
|
|
193
|
+
For resiliency against such issues, workers emit a heartbeat after each
|
|
194
|
+
example they execute, to signal
|
|
195
|
+
that they're healthy and performing jobs as expected. If a worker hasn't
|
|
196
|
+
emitted a heartbeat for a given amount of time (set by the `worker_liveness_sec` option)
|
|
197
|
+
it is considered dead and its reserved job will be put back to the queue, to
|
|
198
|
+
be picked up by another healthy worker.
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
## Rationale
|
|
202
|
+
|
|
203
|
+
### Why didn't you use ci-queue?
|
|
204
|
+
|
|
205
|
+
**Update**: ci-queue [deprecated support for RSpec](https://github.com/Shopify/ci-queue/pull/149).
|
|
206
|
+
|
|
207
|
+
While evaluating ci-queue we experienced slow worker boot
|
|
208
|
+
times (up to 3 minutes in some cases) combined with disk IO saturation and
|
|
209
|
+
increased memory consumption. This is due to the fact that a worker in
|
|
210
|
+
ci-queue has to load every spec file on boot. In applications with a large
|
|
211
|
+
number of spec files this may result in a significant performance hit and
|
|
212
|
+
in case of cloud environments, increased costs.
|
|
213
|
+
|
|
214
|
+
We also observed slower build times compared to our previous solution which
|
|
215
|
+
scheduled whole spec files (as opposed to individual examples), due to
|
|
216
|
+
big differences in runtimes of individual examples, something common in big
|
|
217
|
+
RSpec suites.
|
|
218
|
+
|
|
219
|
+
We decided for RSpecQ to use whole spec files as its main unit of work (as
|
|
220
|
+
opposed to ci-queue which uses individual examples). This means that an RSpecQ
|
|
221
|
+
worker only loads the files needed and ends up with a subset of all the suite's
|
|
222
|
+
files. (Note: RSpecQ also schedules individual examples, but only when this is
|
|
223
|
+
deemed necessary, see [Spec file splitting](#spec-file-splitting)).
|
|
224
|
+
|
|
225
|
+
This kept boot and test run times considerably fast. As a side benefit, this
|
|
226
|
+
allows suites to keep using `before(:all)` hooks (which ci-queue explicitly
|
|
227
|
+
rejects).
|
|
228
|
+
|
|
229
|
+
The downside of this design is that it's more complicated, since the scheduling
|
|
230
|
+
of spec files happens based on timings calculated from previous runs. This
|
|
231
|
+
means that RSpecQ maintains a key with the timing of each job and updates it
|
|
232
|
+
on every run (if the `--update-timings` option was used). Also, RSpecQ has a
|
|
233
|
+
"slow file threshold" which, currently has to be set manually (but this can be
|
|
234
|
+
improved in the future).
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
## Development
|
|
238
|
+
|
|
239
|
+
Install the required dependencies:
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
$ bundle install
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Then you can execute the tests after spinning up a Redis instance at
|
|
246
|
+
`127.0.0.1:6379`:
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
$ bundle exec rake
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
To enable verbose output in the tests:
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
$ RSPECQ_DEBUG=1 bundle exec rake
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Redis
|
|
259
|
+
|
|
260
|
+
RSpecQ by design doesn't expire its keys from Redis. It is left to the user
|
|
261
|
+
to configure the Redis server to do so; see
|
|
262
|
+
[Using Redis as an LRU cache](https://redis.io/topics/lru-cache) for more info.
|
|
263
|
+
|
|
264
|
+
You can do this from a configuration file or with `redis-cli`.
|
|
265
|
+
|
|
266
|
+
## License
|
|
267
|
+
|
|
268
|
+
RSpecQ is licensed under MIT. See [LICENSE](LICENSE).
|
data/Rakefile
ADDED
data/bin/rspecq
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
require "rspecq"
|
|
3
|
+
|
|
4
|
+
config = RSpecQ::Configuration.new(ARGV)
|
|
5
|
+
|
|
6
|
+
# Log the Redis connection resilience settings so they can be
|
|
7
|
+
# verified from the build console
|
|
8
|
+
redis_conn = config.redis_opts
|
|
9
|
+
puts "[rspecq] redis connection: " \
|
|
10
|
+
"connect_timeout=#{redis_conn[:connect_timeout]} " \
|
|
11
|
+
"read_timeout=#{redis_conn[:read_timeout]} " \
|
|
12
|
+
"write_timeout=#{redis_conn[:write_timeout]} " \
|
|
13
|
+
"reconnect_attempts=#{redis_conn[:reconnect_attempts].inspect}"
|
|
14
|
+
|
|
15
|
+
Sentry.init if ENV["SENTRY_DSN"]
|
|
16
|
+
|
|
17
|
+
if config.report?
|
|
18
|
+
reporter = RSpecQ::Reporter.new(
|
|
19
|
+
build_id: config.build,
|
|
20
|
+
timeout: config.report_timeout,
|
|
21
|
+
redis_opts: config.redis_opts,
|
|
22
|
+
queue_wait_timeout: config.queue_wait_timeout,
|
|
23
|
+
worker_liveness_sec: config.worker_liveness_sec,
|
|
24
|
+
update_timings: config.timings,
|
|
25
|
+
timings_key: config.timings_key
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
reporter.report
|
|
29
|
+
else
|
|
30
|
+
worker = RSpecQ::Worker.new(
|
|
31
|
+
build_id: config.build,
|
|
32
|
+
worker_id: config.worker,
|
|
33
|
+
redis_opts: config.redis_opts,
|
|
34
|
+
worker_liveness_sec: config.worker_liveness_sec
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
worker.files_or_dirs_to_run = config.files_or_dirs_to_run if config.files_or_dirs_to_run
|
|
38
|
+
worker.rspec_args = config.rspec_args
|
|
39
|
+
worker.file_split_threshold = config.file_split_threshold
|
|
40
|
+
worker.chunk_target_duration = config.chunk_target_duration
|
|
41
|
+
worker.max_requeues = config.max_requeues
|
|
42
|
+
worker.queue_wait_timeout = config.queue_wait_timeout
|
|
43
|
+
worker.fail_fast = config.fail_fast
|
|
44
|
+
worker.junit_output = config.junit_output
|
|
45
|
+
worker.seed = Integer(config.seed, 10) if config.seed
|
|
46
|
+
worker.reproduction = config.reproduction
|
|
47
|
+
worker.tags = config.tags
|
|
48
|
+
worker.work
|
|
49
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
module RSpecQ
|
|
2
|
+
# Use the RSpec parser to parse any command line args intended
|
|
3
|
+
# for rspec such as `-- --format JUnit -o foo.xml` so that we can
|
|
4
|
+
# pass these args to rspec while removing the
|
|
5
|
+
# files_or_dirs_to_run since we want to pull those from the
|
|
6
|
+
# queue. The RSpecQ::Parser will mutate args, removing any rspecq
|
|
7
|
+
# args so that RSpec::Core::Parser only sees the args intended
|
|
8
|
+
# for rspec.
|
|
9
|
+
Configuration = Struct.new(
|
|
10
|
+
:build,
|
|
11
|
+
:chunk_target_duration,
|
|
12
|
+
:exclude_pattern,
|
|
13
|
+
:fail_fast,
|
|
14
|
+
:files_or_dirs_to_run,
|
|
15
|
+
:file_split_threshold,
|
|
16
|
+
:include_pattern,
|
|
17
|
+
:junit_output,
|
|
18
|
+
:max_requeues,
|
|
19
|
+
:queue_wait_timeout,
|
|
20
|
+
:redis_host,
|
|
21
|
+
:redis_url,
|
|
22
|
+
:redis_opts,
|
|
23
|
+
:redis_connect_timeout,
|
|
24
|
+
:redis_read_timeout,
|
|
25
|
+
:redis_write_timeout,
|
|
26
|
+
:redis_reconnect_attempts,
|
|
27
|
+
:report,
|
|
28
|
+
:report_timeout,
|
|
29
|
+
:reproduction,
|
|
30
|
+
:rspec_args,
|
|
31
|
+
:seed,
|
|
32
|
+
:tags,
|
|
33
|
+
:timings,
|
|
34
|
+
:timings_key,
|
|
35
|
+
:worker,
|
|
36
|
+
:worker_liveness_sec,
|
|
37
|
+
keyword_init: true
|
|
38
|
+
) do
|
|
39
|
+
def initialize(args)
|
|
40
|
+
super(**RSpecQ::Parser.parse!(args))
|
|
41
|
+
|
|
42
|
+
self.files_or_dirs_to_run = RSpec::Core::Parser.new(args).parse[:files_or_directories_to_run]
|
|
43
|
+
l = files_or_dirs_to_run.length
|
|
44
|
+
if l.zero?
|
|
45
|
+
self.files_or_dirs_to_run = nil
|
|
46
|
+
self.rspec_args = args
|
|
47
|
+
else
|
|
48
|
+
self.rspec_args = args[0...-l]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if include_pattern || exclude_pattern
|
|
52
|
+
self.files_or_dirs_to_run = filter_tests(files_or_dirs_to_run, self)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
self.redis_opts = if redis_url
|
|
56
|
+
{ url: redis_url }
|
|
57
|
+
else
|
|
58
|
+
{ host: redis_host }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
self.redis_opts = redis_opts.merge(
|
|
62
|
+
connect_timeout: redis_connect_timeout,
|
|
63
|
+
read_timeout: redis_read_timeout,
|
|
64
|
+
write_timeout: redis_write_timeout,
|
|
65
|
+
reconnect_attempts: parse_reconnect_attempts(redis_reconnect_attempts)
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def report?
|
|
70
|
+
!!report
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def filter_tests(tests, options = {})
|
|
74
|
+
suffix_pattern = /_spec\.rb$/
|
|
75
|
+
include_pattern = options[:include_pattern] || //
|
|
76
|
+
exclude_pattern = options[:exclude_pattern]
|
|
77
|
+
pattern = "**{,/*/**}/*"
|
|
78
|
+
|
|
79
|
+
(tests || []).flat_map do |file_or_folder|
|
|
80
|
+
if File.directory?(file_or_folder)
|
|
81
|
+
files = Dir[File.join(file_or_folder, pattern)].uniq.sort
|
|
82
|
+
files = files.grep(suffix_pattern).grep(include_pattern)
|
|
83
|
+
files -= files.grep(exclude_pattern) if exclude_pattern
|
|
84
|
+
files
|
|
85
|
+
else
|
|
86
|
+
file_or_folder
|
|
87
|
+
end
|
|
88
|
+
end.uniq
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
# Coerces the reconnect_attempts option into the form redis-client expects.
|
|
94
|
+
# The option is a comma-separated list of backoff durations in seconds
|
|
95
|
+
# (e.g. "0.05,0.1,0.25,0.5,1.0"); each entry is how long to sleep before
|
|
96
|
+
# that retry. An empty/blank value (including whitespace) disables
|
|
97
|
+
# reconnects (false).
|
|
98
|
+
def parse_reconnect_attempts(value)
|
|
99
|
+
attempts = value.to_s.split(",").map(&:strip).reject(&:empty?).map { |v| Float(v) }
|
|
100
|
+
attempts.empty? ? false : attempts
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module RSpecQ
|
|
2
|
+
module Formatters
|
|
3
|
+
# Increments the example counter after each job.
|
|
4
|
+
class ExampleCountRecorder
|
|
5
|
+
def initialize(queue)
|
|
6
|
+
@queue = queue
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def dump_summary(summary)
|
|
10
|
+
n = summary.examples.count
|
|
11
|
+
@queue.increment_example_count(n) if n > 0
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module RSpecQ
|
|
2
|
+
module Formatters
|
|
3
|
+
# Persists failed examples information (i.e. message and backtrace), so
|
|
4
|
+
# that they can be reported to the end user by the Reporter.
|
|
5
|
+
#
|
|
6
|
+
# Also persists non-example error information (e.g. a syntax error that
|
|
7
|
+
# in a spec file).
|
|
8
|
+
class FailureRecorder
|
|
9
|
+
def initialize(queue, job, max_requeues, worker_id)
|
|
10
|
+
@queue = queue
|
|
11
|
+
@job = job
|
|
12
|
+
@colorizer = RSpec::Core::Formatters::ConsoleCodes
|
|
13
|
+
@non_example_error_recorded = false
|
|
14
|
+
@max_requeues = max_requeues
|
|
15
|
+
@worker_id = worker_id
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Here we're notified about errors occuring outside of examples.
|
|
19
|
+
#
|
|
20
|
+
# NOTE: Upon such an error, RSpec emits multiple notifications but we only
|
|
21
|
+
# want the _first_, which is the one that contains the error backtrace.
|
|
22
|
+
# That's why have to keep track of whether we've already received the
|
|
23
|
+
# needed notification and act accordingly.
|
|
24
|
+
def message(n)
|
|
25
|
+
if RSpec.world.non_example_failure && !@non_example_error_recorded
|
|
26
|
+
@queue.record_non_example_error(@job, n.message)
|
|
27
|
+
@non_example_error_recorded = true
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def example_failed(notification)
|
|
32
|
+
example = notification.example
|
|
33
|
+
|
|
34
|
+
presenter = RSpec::Core::Formatters::ExceptionPresenter.new(
|
|
35
|
+
example.exception, example
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
msg = presenter.fully_formatted(nil, @colorizer)
|
|
39
|
+
|
|
40
|
+
if @queue.requeue_job(example, @max_requeues, @worker_id)
|
|
41
|
+
# HACK: try to avoid picking the job we just requeued; we want it
|
|
42
|
+
# to be picked up by a different worker
|
|
43
|
+
@queue.record_flaky_failure(notification.example.id, msg)
|
|
44
|
+
sleep 0.5
|
|
45
|
+
return
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
msg << "\n"
|
|
49
|
+
msg << @colorizer.wrap(
|
|
50
|
+
"bin/rspec --seed #{RSpec.configuration.seed} #{example.location_rerun_argument}",
|
|
51
|
+
RSpec.configuration.failure_color
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
msg << @colorizer.wrap(
|
|
55
|
+
" # #{example.full_description}", RSpec.configuration.detail_color
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@queue.record_example_failure(notification.example.id, msg)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module RSpecQ
|
|
2
|
+
module Formatters
|
|
3
|
+
# Persists each job's timing (in seconds). Those timings are used when
|
|
4
|
+
# determining the ordering in which jobs are scheduled (slower jobs will
|
|
5
|
+
# be enqueued first).
|
|
6
|
+
class JobTimingRecorder
|
|
7
|
+
def initialize(queue, job)
|
|
8
|
+
@queue = queue
|
|
9
|
+
@job = job
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def dump_summary(summary)
|
|
13
|
+
if @job.include?("+")
|
|
14
|
+
summary.examples.each do |example|
|
|
15
|
+
@queue.record_build_timing(example.id, Float(example.execution_result.run_time))
|
|
16
|
+
end
|
|
17
|
+
else
|
|
18
|
+
@queue.record_build_timing(@job, Float(summary.duration))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|