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