rspecq 0.0.1.pre2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4fcc5311329946efb2a7801087f4cdb5f0be8becc01bd1bb8f367b6d130a02ea
4
- data.tar.gz: d6f64c6d0c1dae8a53af8bf2d7724e2fb988b03fa795c59d0f5ecd18a92b072a
3
+ metadata.gz: 89dbfa98d1eaceb06c39d41ab85e7fa6923d0c87e9a15b9cbfaf7399ff2aaff3
4
+ data.tar.gz: b7cd028440e6eb03401dc623c7ee0fc0fe74f6ffa12a25ecc23d0cf54e6acd1e
5
5
  SHA512:
6
- metadata.gz: 7611cf0944ea7751eaf93a7aae5686f6c03563b01e71978d9e1d61f30f14f89b12de0ce9ac590f9351eff22a4d4811f9e2f6c241232754ede3162142225f2c27
7
- data.tar.gz: bdbd6da559607026b8e6fead442d4b15b1bb73e957a63b022c4459a83aba0c2a8e297204d9c29ae3bbc700d39ea2434c7b99e55dfb3752a56af5376d0511fea0
6
+ metadata.gz: a43f0630e8a02a001132f45c9f68cacf7edae8e90487112e640eb611e7d1345f68ad0ab163ae01c91cb38ebc879e98cda9b54044c0ee676293c7aa3bf7c17942
7
+ data.tar.gz: bf98027dc02ac56d02cc258700f5efa766c40d4d69c55c529d311083965d1fe4f76423b05fddb35a37496bb6eb3c11a8460a8e76f94897c4bb38a744b2fb40df
@@ -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
- # RSpecQ
1
+ RSpec Queue
2
+ =========================================================================
3
+ [![Build Status](https://travis-ci.com/skroutz/rspecq.svg?branch=master)](https://travis-ci.com/github/skroutz/rspecq)
4
+ [![Gem Version](https://badge.fury.io/rb/rspecq.svg)](https://badge.fury.io/rb/rspecq)
2
5
 
3
- RSpecQ (`rspecq`) distributes and executes an RSpec suite over many workers,
4
- using a centralized queue backed by Redis.
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 heavily inspired by [test-queue](https://github.com/tmm1/test-queue)
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
- ## Why don't you just use ci-queue?
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
- While evaluating ci-queue for our RSpec suite, we observed slow boot times
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
- We also observed faster build times by scheduling spec files instead of
26
- individual examples, due to way less Redis operations.
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
- The downside of this design is that it's more complicated, since the scheduling
29
- of spec files happens based on timings calculated from previous runs. This
30
- means that RSpecQ maintains a key with the timing of each job and updates it
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
- *Update*: ci-queue deprecated support for RSpec, so there's that.
42
+ To start more workers for the same build, use distinct worker IDs but the same
43
+ build ID:
36
44
 
37
- ## Usage
45
+ ```shell
46
+ $ rspecq --build=123 --worker=foo2
47
+ ```
38
48
 
39
- Each worker needs to know the build it will participate in, its name and where
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-id=foo --worker-id=worker1 --redis=redis://localhost
52
+ $ rspecq --build=123 --report
44
53
  ```
45
54
 
46
- To view the progress of the build print use `--report`:
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
- For detailed info use `--help`.
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 basic idea is identical to ci-queue so please refer to its README
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: the smallest unit of work, which is usually a spec file
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: a collection of Redis-backed structures that hold all the necessary
65
- information for RSpecQ to function. This includes timing statistics, jobs to
66
- be executed, the failure reports, requeueing statistics and more.
67
- - Worker: a process that, given a build id, pops up jobs of that build and
68
- executes them using RSpec
69
- - Reporter: a process that, given a build id, waits for the build to finish
70
- and prints the summary report (examples executed, build result, failures etc.)
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
- Very slow files may put a limit to how fast the suite can execute. For example,
75
- a worker may spend 10 minutes running a single slow file, while all the other
76
- workers finish after 8 minutes. To overcome this issue, rspecq splits
77
- files that their execution time is above a certain threshold
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
- In the future, we'd like for the slow threshold to be calculated and set
82
- dynamically.
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 measure for flaky tests, if an example fails it will be put
87
- back to the queue to be picked up by
88
- another worker. This will be repeated up to a certain number of times before,
89
- after which the example will be considered a legit failure and will be printed
90
- in the final report (`--report`).
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
- Workers emit a timestamp after each example, as a heartbeat, to denote
95
- that they're fine and performing jobs. If a worker hasn't reported for
96
- a given amount of time (see `WORKER_LIVENESS_SEC`) it is considered dead
97
- and the job it reserved will be requeued, so that it is picked up by another worker.
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
 
@@ -0,0 +1,9 @@
1
+ require "rake/testtask"
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << "test"
5
+ t.test_files = FileList['test/test_*.rb']
6
+ t.verbose = true
7
+ end
8
+
9
+ task default: :test
data/bin/rspecq CHANGED
@@ -1,67 +1,138 @@
1
1
  #!/usr/bin/env ruby
2
- require "optionparser"
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
- o.banner = "Usage: #{$PROGRAM_NAME} [opts] [files_or_directories_to_run]"
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("--build-id ID", "A unique identifier denoting the build") do |v|
10
- opts[:build_id] = v
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("--worker-id ID", "A unique identifier denoting the worker") do |v|
14
- opts[:worker_id] = v
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 HOST", "Redis HOST to connect to (default: 127.0.0.1)") do |v|
18
- opts[:redis_host] = v || "127.0.0.1"
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", "Populate global job timings in Redis") do |v|
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 sec. and " \
26
- "schedule them by example (default: 999999)") do |v|
27
- opts[:file_split_threshold] = Float(v)
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", "Do not execute tests but wait until queue is empty and " \
31
- "print a report") do |v|
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 queue is not empty after " \
36
- "N seconds. Only applicable if --report is enabled " \
37
- "(default: 3600)") do |v|
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
- [:build_id, :worker_id].each do |o|
44
- raise OptionParser::MissingArgument.new(o) if opts[o].nil?
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[:build_id],
50
- worker_id: opts[:worker_id],
51
- timeout: opts[:report_timeout] || 3600,
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[:build_id],
59
- worker_id: opts[:worker_id],
60
- redis_host: opts[:redis_host],
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] || 999999
135
+ worker.file_split_threshold = opts[:file_split_threshold]
136
+ worker.max_requeues = opts[:max_requeues]
66
137
  worker.work
67
138
  end
@@ -1,11 +1,10 @@
1
1
  require "rspec/core"
2
+ require "sentry-raven"
2
3
 
3
4
  module RSpecQ
4
- MAX_REQUEUES = 3
5
-
6
- # If a worker haven't executed an RSpec example for more than this time
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"
@@ -0,0 +1,4 @@
1
+ RSpec Formatters are used by RSpecQ as hooks for various execution events.
2
+
3
+ For more info on formatters in general, see
4
+ https://rubydoc.info/gems/rspec-core/RSpec/Core/Formatters.
@@ -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, MAX_REQUEUES)
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
@@ -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
- def initialize(build_id, worker_id, redis_host)
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(host: redis_host, id: worker_id)
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) || 0
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
- private
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
- def key(*keys)
206
- [@build_id, keys].join(":")
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
@@ -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:, worker_id:, timeout:, redis_host:)
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, worker_id, redis_host)
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
- t = measure_duration { @queue.wait_until_published }
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
- humanize_duration(tests_duration))
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
@@ -1,3 +1,3 @@
1
1
  module RSpecQ
2
- VERSION = "0.0.1.pre2".freeze
2
+ VERSION = "0.3.0".freeze
3
3
  end
@@ -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
- def initialize(build_id:, worker_id:, redis_host:, files_or_dirs_to_run:)
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, redis_host)
21
- @files_or_dirs_to_run = 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!(@queue)
35
- @queue.wait_until_published
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 = @queue.requeue_lost_job
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 = @queue.reserve_job
77
+ job = queue.reserve_job
48
78
 
49
79
  # build is finished
50
- return if job.nil? && @queue.exhausted?
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(@queue, job))
64
- RSpec.configuration.add_formatter(Formatters::ExampleCountRecorder.new(@queue))
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(@queue, job))
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
- @queue.acknowledge_job(job)
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
- @queue.record_worker_heartbeat
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 = @files_or_dirs_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
- puts "WARNING: No timings found! Published queue in " \
115
- "random order (size=#{q_size})"
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
- slow_files = timings.take_while do |_job, duration|
120
- duration >= file_split_threshold
121
- end.map(&:first) & files_to_run
132
+ # prepare jobs to run
133
+ jobs = []
134
+ slow_files = []
122
135
 
123
- if slow_files.any?
124
- puts "Slow files (threshold=#{file_split_threshold}): #{slow_files}"
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
- # prepare jobs to run
128
- jobs = []
129
- jobs.concat(files_to_run - slow_files)
130
- jobs.concat(files_to_example_ids(slow_files)) if slow_files.any?
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
- # heuristic: put untimed jobs in the middle of the queue
137
- puts "New/untimed job: #{j}" if timings[j].nil?
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
- # finally, sort them based on their timing (slowest first)
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 slow files unchanged,
150
- # thereby falling back to scheduling them normally.
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
- # TODO: do this programatically
156
- cmd = "DISABLE_SPRING=1 bin/rspec --dry-run --format json #{files.join(' ')}"
157
- out = `#{cmd}`
158
-
159
- if !$?.success?
160
- # TODO: emit warning to Sentry
161
- puts "WARNING: Error splitting slow files; falling back to regular scheduling:"
162
-
163
- begin
164
- pp JSON.parse(out)
165
- rescue JSON::ParserError
166
- puts out
167
- end
168
- puts
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.1.pre2
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-06-26 00:00:00.000000000 Z
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: '5.14'
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: '5.14'
96
+ version: '0'
55
97
  - !ruby/object:Gem::Dependency
56
- name: rake
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: 1.3.1
150
+ version: '0'
107
151
  requirements: []
108
- rubygems_version: 3.1.2
152
+ rubygems_version: 3.1.4
109
153
  signing_key:
110
154
  specification_version: 4
111
- summary: Distribute an RSpec suite among many workers
155
+ summary: Optimally distribute and run RSpec suites among parallel workers; for faster
156
+ CI builds
112
157
  test_files: []