quarantine 1.0.7 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -8
- data/README.md +76 -75
- data/lib/quarantine.rb +105 -101
- data/lib/quarantine/cli.rb +23 -30
- data/lib/quarantine/databases/base.rb +24 -7
- data/lib/quarantine/databases/dynamo_db.rb +40 -30
- data/lib/quarantine/error.rb +2 -0
- data/lib/quarantine/rspec_adapter.rb +64 -45
- data/lib/quarantine/test.rb +14 -21
- data/lib/quarantine/version.rb +1 -1
- data/quarantine.gemspec +2 -2
- metadata +15 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 47aa8e8df1db802259f0107a7a06c2d05162899b803ffd224da8a53246c1e105
|
4
|
+
data.tar.gz: 8cb6c6c9c5b79666c10feaf38e0077eab27b57d494dc08e0c385925de1c89583
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e3f9ba5314c425861f18b7451a7f28f8ced4af50479ad880f0be263a9104ba3c6dd85bcada8a0c89d194e9487d02702c9fe4930caceebf835413891fc31d6a37
|
7
|
+
data.tar.gz: 3177827c0ff075302fa8e62c0b3480728769a3cb6199cb8eb0e510237df9839d0efc10db0180d32016d1ced933d9d50f87336f5f2d6ff3f741622a5aed3364b2
|
data/CHANGELOG.md
CHANGED
@@ -1,21 +1,20 @@
|
|
1
1
|
### 1.0.7
|
2
|
-
|
2
|
+
Support `it_behaves_like` behavior
|
3
3
|
|
4
4
|
### 1.0.6
|
5
|
-
|
5
|
+
Update DynamoDB batch_write_item implementation to check for duplicates based on different keys before uploading
|
6
6
|
|
7
7
|
### 1.0.5
|
8
|
-
|
8
|
+
Add aws_credentials argument during dynamodb initialization to override the AWS SDK credential chain
|
9
9
|
|
10
10
|
### 1.0.4
|
11
|
-
|
11
|
+
Enable upstream callers to mark an example as flaky through the example's metadata
|
12
12
|
|
13
13
|
### 1.0.3
|
14
|
-
|
14
|
+
Only require dynamodb instead of full aws-sdk
|
15
15
|
|
16
16
|
### 1.0.2
|
17
|
-
|
18
|
-
compatible with v2
|
17
|
+
Relax Aws gem version constraint as aws-sdk v3 is 100% backwards compatible with v2
|
19
18
|
|
20
19
|
### 1.0.1
|
21
|
-
|
20
|
+
Initial Release
|
data/README.md
CHANGED
@@ -1,27 +1,18 @@
|
|
1
1
|
# Quarantine
|
2
|
+
|
2
3
|
[![Build Status](https://travis-ci.com/flexport/quarantine.svg?branch=master)](https://travis-ci.com/flexport/quarantine)
|
3
4
|
|
4
|
-
Quarantine
|
5
|
+
Quarantine automatically detects flaky tests (i.e. those which fail non-deterministically) and disables them until they're proven reliable.
|
5
6
|
|
6
|
-
|
7
|
+
Quarantine current supports the following testing frameworks. If you need an additional one, please file an issue or open a pull request.
|
7
8
|
- [RSpec](http://rspec.info/)
|
8
9
|
|
9
|
-
|
10
|
-
- [Buildkite](https://buildkite.com/docs/tutorials/getting-started)
|
11
|
-
|
12
|
-
If you are interested in using quarantine but it does not support your CI or testing framework, feel free to reach out or create an issue and we can try to make it happen.
|
10
|
+
Quarantine should provide the necessary hooks for compatibility with any CI solution. If it's insufficient for yours, please file an issue or open a pull request.
|
13
11
|
|
14
|
-
##
|
15
|
-
Flaky tests impact engineering velocity, reduce faith in test reliablity and give a false representation of code coverage. Managing flaky tests is a clunky process that involves constant build monitorization, difficult diagnosis and manual ticket creation. As a result, here at Flexport, we have created a Gem to automate the entire process to help improve the workflow and keep our massive test suites in pristine condition.
|
12
|
+
## Getting started
|
16
13
|
|
17
|
-
|
14
|
+
Quarantine works in tandem with [RSpec::Retry](https://github.com/NoRedInk/rspec-retry). Add this to your `Gemfile` and run `bundle install`:
|
18
15
|
|
19
|
-
![ideal workflow](misc/flexport_workflow.png)
|
20
|
-
|
21
|
-
---
|
22
|
-
## Installation and Setup
|
23
|
-
|
24
|
-
Add these lines to your application's Gemfile:
|
25
16
|
```rb
|
26
17
|
group :test do
|
27
18
|
gem 'quarantine'
|
@@ -29,123 +20,133 @@ group :test do
|
|
29
20
|
end
|
30
21
|
```
|
31
22
|
|
32
|
-
|
33
|
-
```sh
|
34
|
-
bundle install
|
35
|
-
```
|
23
|
+
In your `spec_helper.rb`, set up Quarantine and RSpec::Retry. See [RSpec::Retry](https://github.com/NoRedInk/rspec-retry)'s documentation for details of its configuration.
|
36
24
|
|
37
|
-
In your `spec_helper.rb` setup quarantine and rspec-retry gem. Click [rspec-retry](https://github.com/NoRedInk/rspec-retry) to get a more detailed explaination on rspec-retry configurations and how to setup.
|
38
25
|
```rb
|
39
26
|
require 'quarantine'
|
40
|
-
require 'rspec
|
27
|
+
require 'rspec/retry'
|
41
28
|
|
42
|
-
Quarantine.bind
|
29
|
+
Quarantine::RSpecAdapter.bind
|
43
30
|
|
44
31
|
RSpec.configure do |config|
|
32
|
+
# Also accepts `:credentials` to override the standard AWS credential chain
|
33
|
+
config.quarantine_database = {type: :dynamodb, region: 'us-west-1'}
|
34
|
+
# Prevent the list of flaky tests from being polluted by local development and PRs
|
35
|
+
config.quarantine_record_tests = ENV["CI"] && ENV["BRANCH"] == "master"
|
45
36
|
|
46
|
-
config.around(:each) do |
|
47
|
-
|
37
|
+
config.around(:each) do |example|
|
38
|
+
example.run_with_retry(retry: 3)
|
48
39
|
end
|
49
|
-
|
50
40
|
end
|
51
41
|
```
|
52
42
|
|
53
|
-
|
43
|
+
Quarantine comes with a CLI tool for setting up the necessary table in DynamoDB, if you use that database.
|
54
44
|
|
55
|
-
```rb
|
56
|
-
if ENV[CI] && ENV[BRANCH] == "master"
|
57
|
-
Quarantine.bind({database: :dynamodb, aws_region: 'us-west-1'})
|
58
|
-
end
|
59
|
-
```
|
60
|
-
|
61
|
-
Setup tables in AWS DynamoDB to support pulling and uploading quarantined tests
|
62
45
|
```sh
|
63
|
-
bundle exec quarantine_dynamodb -h #
|
46
|
+
bundle exec quarantine_dynamodb -h # See all options
|
64
47
|
|
65
|
-
bundle exec quarantine_dynamodb \ #
|
66
|
-
--
|
67
|
-
# table names
|
48
|
+
bundle exec quarantine_dynamodb \ # Create the "test_statuses" table in us-west-1 in AWS DynamoDB
|
49
|
+
--region us-west-1
|
68
50
|
```
|
69
51
|
|
70
|
-
|
52
|
+
## How It Works
|
53
|
+
|
54
|
+
A flaky test fails on the first run, but passes after being retried via RSpec::Retry.
|
71
55
|
|
72
|
-
## Try Quarantining Tests Locally
|
73
|
-
Add a test that will flake
|
74
56
|
```rb
|
75
57
|
require "spec_helper"
|
76
58
|
|
77
59
|
describe Quarantine do
|
78
|
-
it "
|
79
|
-
|
60
|
+
it "fails on the first run" do
|
61
|
+
raise "error" if RSpec.current_example.attempts == 0
|
62
|
+
# otherwise, pass
|
80
63
|
end
|
81
64
|
end
|
82
65
|
```
|
83
66
|
|
84
|
-
Run `rspec` on the test
|
85
67
|
```sh
|
86
|
-
CI=1 BRANCH=master rspec <filename>
|
68
|
+
$ CI=1 BRANCH=master bundle exec rspec <filename>
|
69
|
+
[quarantine] Quarantined tests:
|
70
|
+
./bar_spec.rb[1:1] Quarantine fails on the first run
|
87
71
|
```
|
88
72
|
|
89
|
-
|
73
|
+
When the build completes, all test statuses are written to the database. Flaky tests are marked `quarantined`, and will be executed in future builds, but any failures will be ignored.
|
74
|
+
|
75
|
+
A test can be removed from quarantine by updating the database manually (outside this gem), or by configuring `quarantine_release_at_consecutive_passes` to remove it after it passes on a certain number of builds in a row.
|
90
76
|
|
91
77
|
## Configuration
|
92
78
|
|
93
|
-
|
79
|
+
In `spec_helper.rb`, you can set configuration variables by doing:
|
80
|
+
|
94
81
|
```rb
|
95
82
|
RSpec.configure do |config|
|
96
|
-
|
83
|
+
config.VAR_NAME = VALUE
|
97
84
|
end
|
98
85
|
```
|
99
|
-
- Table name for quarantined tests `:quarantine_list_table, default: "quarantine_list"`
|
100
86
|
|
101
|
-
-
|
87
|
+
- `quarantine_database`: Database configuration (see below), default: `{ type: :dynamodb, region: 'us-west-1' }`
|
88
|
+
- `test_statusus_table`: Table name for test statuses, default: `"test_statuses"`
|
89
|
+
- `skip_quarantined_tests`: Skipping quarantined tests during test runs default: `true`
|
90
|
+
- `quarantine_record_tests`: Recording test statuses, default: `true`
|
91
|
+
- `quarantine_logging`: Outputting quarantined gem info, default: `true`
|
92
|
+
- `quarantine_extra_attributes`: Storing custom per-example attributes in the table, default: `nil`
|
93
|
+
- `quarantine_failsafe_limit`: A failsafe limit of quarantined tests in a single run, default: `10`
|
94
|
+
- `quarantine_release_at_consecutive_passes`: Releasing a test from quarantine after it records enough consecutive passes (`nil` to disable this feature), default: `nil`
|
102
95
|
|
103
|
-
|
96
|
+
### Databases
|
104
97
|
|
105
|
-
-
|
98
|
+
Quarantine comes with built-in support for the following database types:
|
99
|
+
- `:dynamodb`
|
106
100
|
|
107
|
-
|
101
|
+
To use `:dynamodb`, be sure to add `gem 'aws-sdk-dynamodb', '~> 1', group: :test` to your `Gemfile`.
|
108
102
|
|
109
|
-
|
103
|
+
To use a custom database that's not provided, subclass `Quarantine::Databases::Base` and pass an instance of your class as the `quarantine_database` setting:
|
110
104
|
|
111
|
-
|
112
|
-
|
105
|
+
```rb
|
106
|
+
class MyDatabase < Quarantine::Databases::Base
|
107
|
+
...
|
108
|
+
end
|
113
109
|
|
114
|
-
|
110
|
+
RSpec.configure do |config|
|
111
|
+
config.quarantine_database = MyDatabase.new(...)
|
112
|
+
end
|
113
|
+
```
|
115
114
|
|
116
|
-
|
115
|
+
### Extra attributes
|
116
|
+
|
117
|
+
Use `quarantine_extra_attributes` to store custom data with each test in the database, e.g. variables useful for your CI setup.
|
118
|
+
|
119
|
+
```rb
|
120
|
+
config.quarantine_extra_attributes = Proc.new do |example|
|
121
|
+
{
|
122
|
+
build_url: ENV['BUILDKITE_BUILD_URL'],
|
123
|
+
job_id: ENV['BUILDKITE_JOB_ID'],
|
124
|
+
}
|
125
|
+
end
|
126
|
+
```
|
117
127
|
|
118
|
-
---
|
119
|
-
## Contributing
|
120
|
-
1. Create an issue regarding a bug fix or feature request
|
121
|
-
2. Fork the repository
|
122
|
-
3. Create your feature branch, commit changes and create a pull request
|
123
|
-
|
124
|
-
## Contributors
|
125
|
-
- [Eric Zhu](https://github.com/eric-zhu-uw)
|
126
|
-
- [Kevin Miller](https://github.com/Gasparila)
|
127
|
-
- [Nicholas Silva](https://github.com/flexportnes)
|
128
|
-
- [Ankur Dahiya](https://github.com/legalosLOTR)
|
129
128
|
---
|
130
129
|
|
131
130
|
## FAQs
|
132
131
|
|
133
132
|
#### Why are quarantined tests not being skipped locally?
|
134
133
|
|
135
|
-
|
134
|
+
Quarantine may be configured to only run in certain environments. Check your `spec_helper.rb`, and make sure you have all necessary environment variables set, e.g.:
|
136
135
|
|
137
136
|
```sh
|
138
|
-
CI=
|
137
|
+
CI=1 BRANCH=master bundle exec rspec
|
139
138
|
```
|
140
139
|
|
141
|
-
#### Why is
|
140
|
+
#### Why is Quarantine failing to connect to DynamoDB?
|
142
141
|
|
143
|
-
The AWS client loads credentials from the following locations
|
144
|
-
- The optional `
|
142
|
+
The AWS client attempts to loads credentials from the following locations, in order:
|
143
|
+
- The optional `credentials` field in `RSpec.configuration.quarantine_database`
|
145
144
|
- `ENV['AWS_ACCESS_KEY_ID']` and `ENV['AWS_SECRET_ACCESS_KEY']`
|
146
145
|
- `Aws.config[:credentials]`
|
147
146
|
- The shared credentials ini file at `~/.aws/credentials`
|
148
147
|
|
149
|
-
|
148
|
+
More detailed information can be found in the [AWS SDK documentation](https://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Client.html)
|
149
|
+
|
150
|
+
#### Why is `example.clear_exception` failing locally?
|
150
151
|
|
151
|
-
|
152
|
+
`Example#clear_exception` is an attribute added through [RSpec::Retry](https://github.com/NoRedInk/rspec-retry). Make sure it has been installed and configured.
|
data/lib/quarantine.rb
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
require 'sorbet-runtime'
|
4
|
+
|
1
5
|
require 'rspec/retry'
|
2
6
|
require 'quarantine/rspec_adapter'
|
3
7
|
require 'quarantine/test'
|
@@ -7,146 +11,146 @@ require 'quarantine/databases/dynamo_db'
|
|
7
11
|
module RSpec
|
8
12
|
module Core
|
9
13
|
class Example
|
14
|
+
extend T::Sig
|
15
|
+
|
10
16
|
# The implementation of clear_exception in rspec-retry doesn't work
|
11
17
|
# for examples that use `it_behaves_like`, so we implement our own version that
|
12
18
|
# clear the exception field recursively.
|
19
|
+
sig { void }
|
13
20
|
def clear_exception!
|
14
|
-
@exception = nil
|
15
|
-
example.clear_exception! if defined?(example)
|
21
|
+
@exception = T.let(nil, T.untyped)
|
22
|
+
T.unsafe(self).example.clear_exception! if defined?(example)
|
16
23
|
end
|
17
24
|
end
|
18
25
|
end
|
19
26
|
end
|
20
27
|
|
21
28
|
class Quarantine
|
22
|
-
extend
|
23
|
-
|
24
|
-
|
25
|
-
attr_reader :
|
26
|
-
|
27
|
-
|
28
|
-
attr_reader :
|
29
|
-
attr_reader :buildkite_build_number
|
30
|
-
attr_reader :summary
|
31
|
-
|
32
|
-
def initialize(options = {})
|
33
|
-
case options[:database]
|
34
|
-
# default database option is dynamodb
|
35
|
-
when :dynamodb, nil
|
36
|
-
@database = Quarantine::Databases::DynamoDB.new(options)
|
37
|
-
else
|
38
|
-
raise Quarantine::UnsupportedDatabaseError.new("Quarantine does not support #{options[:database]}")
|
39
|
-
end
|
29
|
+
extend T::Sig
|
30
|
+
|
31
|
+
sig { returns(T::Hash[String, Quarantine::Test]) }
|
32
|
+
attr_reader :tests
|
33
|
+
|
34
|
+
sig { returns(T::Hash[String, Quarantine::Test]) }
|
35
|
+
attr_reader :old_tests
|
40
36
|
|
41
|
-
|
42
|
-
|
43
|
-
@
|
44
|
-
@
|
45
|
-
@
|
37
|
+
sig { params(options: T::Hash[T.untyped, T.untyped]).void }
|
38
|
+
def initialize(options)
|
39
|
+
@options = options
|
40
|
+
@old_tests = T.let({}, T::Hash[String, Quarantine::Test])
|
41
|
+
@tests = T.let({}, T::Hash[String, Quarantine::Test])
|
42
|
+
@database_failures = T.let([], T::Array[String])
|
43
|
+
@database = T.let(nil, T.nilable(Quarantine::Databases::Base))
|
46
44
|
end
|
47
45
|
|
48
|
-
|
49
|
-
def
|
46
|
+
sig { returns(Quarantine::Databases::Base) }
|
47
|
+
def database
|
48
|
+
@database ||=
|
49
|
+
case @options[:database]
|
50
|
+
when Quarantine::Databases::Base
|
51
|
+
@options[:database]
|
52
|
+
else
|
53
|
+
database_options = @options[:database].dup
|
54
|
+
type = database_options.delete(:type)
|
55
|
+
case type
|
56
|
+
when :dynamodb
|
57
|
+
Quarantine::Databases::DynamoDB.new(database_options)
|
58
|
+
else
|
59
|
+
raise Quarantine::UnsupportedDatabaseError.new("Quarantine does not support database type: #{type.inspect}")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Scans the test_statuses from the database and store their IDs in quarantined_ids
|
65
|
+
sig { void }
|
66
|
+
def fetch_test_statuses
|
50
67
|
begin
|
51
|
-
|
68
|
+
test_statuses = database.fetch_items(@options[:test_statuses_table_name])
|
52
69
|
rescue Quarantine::DatabaseError => e
|
53
|
-
|
70
|
+
@database_failures << "#{e.cause&.class}: #{e.cause&.message}"
|
54
71
|
raise Quarantine::DatabaseError.new(
|
55
72
|
<<~ERROR_MSG
|
56
|
-
Failed to pull the quarantine list from #{
|
57
|
-
because of #{e
|
73
|
+
Failed to pull the quarantine list from #{@options[:test_statuses_table_name]}
|
74
|
+
because of #{e.cause&.class}: #{e.cause&.message}
|
58
75
|
ERROR_MSG
|
59
76
|
)
|
60
77
|
end
|
61
78
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
79
|
+
pairs =
|
80
|
+
test_statuses
|
81
|
+
.group_by { |t| t['id'] }
|
82
|
+
.map { |_id, tests| tests.max_by { |t| t['created_at'] } }
|
83
|
+
.compact
|
84
|
+
.filter { |t| t['last_status'] == 'quarantined' }
|
85
|
+
.map do |t|
|
86
|
+
[
|
87
|
+
t['id'],
|
88
|
+
Quarantine::Test.new(
|
89
|
+
id: t['id'],
|
90
|
+
status: t['last_status'].to_sym,
|
91
|
+
consecutive_passes: t['consecutive_passes'].to_i,
|
92
|
+
full_description: t['full_description'],
|
93
|
+
location: t['location'],
|
94
|
+
extra_attributes: t['extra_attributes']
|
95
|
+
)
|
96
|
+
]
|
97
|
+
end
|
76
98
|
|
77
|
-
|
78
|
-
|
79
|
-
if type == :failed
|
80
|
-
tests = failed_tests
|
81
|
-
table_name = RSpec.configuration.quarantine_failed_tests_table
|
82
|
-
elsif type == :flaky
|
83
|
-
tests = flaky_tests
|
84
|
-
table_name = RSpec.configuration.quarantine_list_table
|
85
|
-
else
|
86
|
-
raise Quarantine::UnknownUploadError.new(
|
87
|
-
"Quarantine gem did not know how to handle #{type} upload of tests to dynamodb"
|
88
|
-
)
|
89
|
-
end
|
99
|
+
@old_tests = Hash[pairs]
|
100
|
+
end
|
90
101
|
|
91
|
-
|
102
|
+
sig { void }
|
103
|
+
def upload_tests
|
104
|
+
return if @tests.empty? || @tests.values.count { |test| test.status == :quarantined } >= @options[:failsafe_limit]
|
92
105
|
|
93
106
|
begin
|
94
107
|
timestamp = Time.now.to_i / 1000 # Truncated millisecond from timestamp for reasons specific to Flexport
|
95
|
-
database.
|
96
|
-
|
97
|
-
tests
|
98
|
-
{
|
99
|
-
build_job_id: ENV['BUILDKITE_JOB_ID'] || '-1',
|
100
|
-
created_at: timestamp,
|
101
|
-
updated_at: timestamp
|
102
|
-
}
|
108
|
+
database.write_items(
|
109
|
+
@options[:test_statuses_table_name],
|
110
|
+
@tests.values.map { |item| item.to_hash.merge(updated_at: timestamp) }
|
103
111
|
)
|
104
112
|
rescue Quarantine::DatabaseError => e
|
105
|
-
|
113
|
+
@database_failures << "#{e.cause&.class}: #{e.cause&.message}"
|
106
114
|
end
|
107
115
|
end
|
108
116
|
|
109
117
|
# Param: RSpec::Core::Example
|
110
|
-
# Add the example to the internal
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
example.full_description,
|
126
|
-
example.location,
|
127
|
-
buildkite_build_number
|
118
|
+
# Add the example to the internal tests list
|
119
|
+
sig { params(example: T.untyped, status: Symbol, passed: T::Boolean).void }
|
120
|
+
def record_test(example, status, passed:)
|
121
|
+
extra_attributes = @options[:extra_attributes] ? @options[:extra_attributes].call(example) : {}
|
122
|
+
|
123
|
+
new_consecutive_passes = passed ? (@old_tests[example.id]&.consecutive_passes || 0) + 1 : 0
|
124
|
+
release_at = @options[:release_at_consecutive_passes]
|
125
|
+
new_status = !release_at.nil? && new_consecutive_passes >= release_at ? :passing : status
|
126
|
+
test = Quarantine::Test.new(
|
127
|
+
id: example.id,
|
128
|
+
status: new_status,
|
129
|
+
consecutive_passes: new_consecutive_passes,
|
130
|
+
full_description: example.full_description,
|
131
|
+
location: example.location,
|
132
|
+
extra_attributes: extra_attributes
|
128
133
|
)
|
129
134
|
|
130
|
-
|
131
|
-
add_to_summary(:flaky_tests, flaky_test.id)
|
132
|
-
end
|
133
|
-
|
134
|
-
# Param: RSpec::Core::Example
|
135
|
-
# Clear exceptions on a flaky tests that has been quarantined
|
136
|
-
def pass_flaky_test(example)
|
137
|
-
example.clear_exception!
|
138
|
-
add_to_summary(:quarantined_tests, example.id)
|
135
|
+
@tests[test.id] = test
|
139
136
|
end
|
140
137
|
|
141
138
|
# Param: RSpec::Core::Example
|
142
|
-
# Check the internal
|
139
|
+
# Check the internal old_tests to see if this test should be quarantined
|
140
|
+
sig { params(example: T.untyped).returns(T::Boolean) }
|
143
141
|
def test_quarantined?(example)
|
144
|
-
|
142
|
+
@old_tests[example.id]&.status == :quarantined
|
145
143
|
end
|
146
144
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
145
|
+
sig { returns(String) }
|
146
|
+
def summary
|
147
|
+
quarantined_tests = @tests.values.select { |test| test.status == :quarantined }.sort_by(&:id)
|
148
|
+
<<~MESSAGE
|
149
|
+
\n[quarantine] Quarantined tests:
|
150
|
+
#{quarantined_tests.map { |test| "#{test.id} #{test.full_description}" }.join("\n ")}
|
151
|
+
|
152
|
+
[quarantine] Database errors:
|
153
|
+
#{@database_failures.join("\n ")}
|
154
|
+
MESSAGE
|
151
155
|
end
|
152
156
|
end
|
data/lib/quarantine/cli.rb
CHANGED
@@ -1,43 +1,42 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
1
3
|
require 'optparse'
|
2
4
|
require_relative 'databases/base'
|
3
5
|
require_relative 'databases/dynamo_db'
|
4
6
|
|
5
7
|
class Quarantine
|
6
8
|
class CLI
|
7
|
-
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
sig { returns(T::Hash[T.untyped, T.untyped]) }
|
12
|
+
attr_reader :options
|
8
13
|
|
14
|
+
sig { void }
|
9
15
|
def initialize
|
10
16
|
# default options
|
11
|
-
@options =
|
12
|
-
|
13
|
-
|
14
|
-
|
17
|
+
@options = T.let(
|
18
|
+
{
|
19
|
+
test_statuses_table_name: 'test_statuses'
|
20
|
+
}, T::Hash[Symbol, T.untyped]
|
21
|
+
)
|
15
22
|
end
|
16
23
|
|
24
|
+
sig { void }
|
17
25
|
def parse
|
18
26
|
OptionParser.new do |parser|
|
19
27
|
parser.banner = 'Usage: quarantine_dynamodb [options]'
|
20
28
|
|
21
|
-
parser.on('-rREGION', '--
|
22
|
-
options[:
|
29
|
+
parser.on('-rREGION', '--region=REGION', String, 'Specify the aws region for DynamoDB') do |region|
|
30
|
+
@options[:region] = region
|
23
31
|
end
|
24
32
|
|
25
33
|
parser.on(
|
26
34
|
'-qTABLE',
|
27
35
|
'--quarantine_table=TABLE',
|
28
36
|
String,
|
29
|
-
"Specify the table name for the quarantine list | Default: #{options[:
|
30
|
-
) do |table_name|
|
31
|
-
options[:quarantine_list_table_name] = table_name
|
32
|
-
end
|
33
|
-
|
34
|
-
parser.on(
|
35
|
-
'-fTABLE',
|
36
|
-
'--failed_table=TABLE',
|
37
|
-
String,
|
38
|
-
"Specify the table name for the failed test list | Default: #{options[:failed_test_table_name]}"
|
37
|
+
"Specify the table name for the quarantine list | Default: #{@options[:test_statuses_table_name]}"
|
39
38
|
) do |table_name|
|
40
|
-
options[:
|
39
|
+
@options[:test_statuses_table_name] = table_name
|
41
40
|
end
|
42
41
|
|
43
42
|
parser.on('-h', '--help', 'Prints help page') do
|
@@ -46,7 +45,7 @@ class Quarantine
|
|
46
45
|
end
|
47
46
|
end.parse!
|
48
47
|
|
49
|
-
if options[:
|
48
|
+
if @options[:region].nil?
|
50
49
|
error_msg = 'Failed to specify the required aws region with -r option'.freeze
|
51
50
|
warn error_msg
|
52
51
|
raise ArgumentError.new(error_msg)
|
@@ -54,12 +53,12 @@ class Quarantine
|
|
54
53
|
end
|
55
54
|
|
56
55
|
# TODO: eventually move to a separate file & create_table by db type when my db adapters
|
56
|
+
sig { void }
|
57
57
|
def create_tables
|
58
|
-
dynamodb = Quarantine::Databases::DynamoDB.new(options)
|
58
|
+
dynamodb = Quarantine::Databases::DynamoDB.new(region: @options[:region])
|
59
59
|
|
60
60
|
attributes = [
|
61
|
-
{ attribute_name: 'id', attribute_type: 'S', key_type: 'HASH' }
|
62
|
-
{ attribute_name: 'build_number', attribute_type: 'S', key_type: 'RANGE' }
|
61
|
+
{ attribute_name: 'id', attribute_type: 'S', key_type: 'HASH' }
|
63
62
|
]
|
64
63
|
|
65
64
|
additional_arguments = {
|
@@ -70,15 +69,9 @@ class Quarantine
|
|
70
69
|
}
|
71
70
|
|
72
71
|
begin
|
73
|
-
dynamodb.create_table(options[:
|
74
|
-
rescue Quarantine::DatabaseError => e
|
75
|
-
warn "#{e&.cause&.class}: #{e&.cause&.message}"
|
76
|
-
end
|
77
|
-
|
78
|
-
begin
|
79
|
-
dynamodb.create_table(options[:failed_test_table_name], attributes, additional_arguments)
|
72
|
+
dynamodb.create_table(@options[:test_statuses_table_name], attributes, additional_arguments)
|
80
73
|
rescue Quarantine::DatabaseError => e
|
81
|
-
warn "#{e
|
74
|
+
warn "#{e.cause&.class}: #{e.cause&.message}"
|
82
75
|
end
|
83
76
|
end
|
84
77
|
end
|
@@ -1,17 +1,34 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
1
3
|
class Quarantine
|
2
4
|
module Databases
|
3
5
|
class Base
|
4
|
-
|
5
|
-
|
6
|
-
end
|
6
|
+
extend T::Sig
|
7
|
+
extend T::Helpers
|
7
8
|
|
8
|
-
|
9
|
-
|
9
|
+
abstract!
|
10
|
+
|
11
|
+
Item = T.type_alias do
|
12
|
+
{
|
13
|
+
'id' => String,
|
14
|
+
'last_status' => String,
|
15
|
+
'consecutive_passes' => Integer,
|
16
|
+
'full_description' => String,
|
17
|
+
'location' => String,
|
18
|
+
'extra_attributes' => T.untyped
|
19
|
+
}
|
10
20
|
end
|
11
21
|
|
12
|
-
|
13
|
-
|
22
|
+
sig { abstract.params(table_name: String).returns(T::Enumerable[Item]) }
|
23
|
+
def fetch_items(table_name); end
|
24
|
+
|
25
|
+
sig do
|
26
|
+
abstract.params(
|
27
|
+
table_name: String,
|
28
|
+
items: T::Array[Item]
|
29
|
+
).void
|
14
30
|
end
|
31
|
+
def write_items(table_name, items); end
|
15
32
|
end
|
16
33
|
end
|
17
34
|
end
|
@@ -1,51 +1,53 @@
|
|
1
|
-
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'aws-sdk-dynamodb'
|
5
|
+
rescue LoadError
|
6
|
+
end
|
2
7
|
require 'quarantine/databases/base'
|
3
8
|
require 'quarantine/error'
|
4
9
|
|
5
10
|
class Quarantine
|
6
11
|
module Databases
|
7
12
|
class DynamoDB < Base
|
13
|
+
extend T::Sig
|
14
|
+
|
15
|
+
Attribute = T.type_alias { { attribute_name: String, attribute_type: String, key_type: String } }
|
16
|
+
|
17
|
+
sig { returns(Aws::DynamoDB::Client) }
|
8
18
|
attr_accessor :dynamodb
|
9
19
|
|
10
|
-
|
11
|
-
|
12
|
-
|
20
|
+
sig { params(options: T::Hash[T.untyped, T.untyped]).void }
|
21
|
+
def initialize(options)
|
22
|
+
super()
|
13
23
|
|
14
|
-
@dynamodb = Aws::DynamoDB::Client.new(options)
|
24
|
+
@dynamodb = T.let(Aws::DynamoDB::Client.new(options), Aws::DynamoDB::Client)
|
15
25
|
end
|
16
26
|
|
17
|
-
|
27
|
+
sig { override.params(table_name: String).returns(T::Enumerable[Item]) }
|
28
|
+
def fetch_items(table_name)
|
18
29
|
begin
|
19
|
-
result = dynamodb.scan(table_name: table_name)
|
30
|
+
result = @dynamodb.scan(table_name: table_name)
|
20
31
|
rescue Aws::DynamoDB::Errors::ServiceError
|
21
32
|
raise Quarantine::DatabaseError
|
22
33
|
end
|
23
34
|
|
24
|
-
result
|
35
|
+
result.items
|
25
36
|
end
|
26
37
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
deduped_items = items.reject do |item|
|
36
|
-
scanned_items.any? do |scanned_item|
|
37
|
-
is_a_duplicate.call(item.to_string_hash, scanned_item)
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
return if deduped_items.empty?
|
42
|
-
|
43
|
-
dynamodb.batch_write_item(
|
38
|
+
sig do
|
39
|
+
override.params(
|
40
|
+
table_name: String,
|
41
|
+
items: T::Array[Item]
|
42
|
+
).void
|
43
|
+
end
|
44
|
+
def write_items(table_name, items)
|
45
|
+
@dynamodb.batch_write_item(
|
44
46
|
request_items: {
|
45
|
-
table_name =>
|
47
|
+
table_name => items.map do |item|
|
46
48
|
{
|
47
49
|
put_request: {
|
48
|
-
item:
|
50
|
+
item: item
|
49
51
|
}
|
50
52
|
}
|
51
53
|
end
|
@@ -55,8 +57,9 @@ class Quarantine
|
|
55
57
|
raise Quarantine::DatabaseError
|
56
58
|
end
|
57
59
|
|
58
|
-
|
59
|
-
|
60
|
+
sig { params(table_name: String, keys: T::Hash[T.untyped, T.untyped]).void }
|
61
|
+
def delete_items(table_name, keys)
|
62
|
+
@dynamodb.delete_item(
|
60
63
|
table_name: table_name,
|
61
64
|
key: {
|
62
65
|
**keys
|
@@ -66,8 +69,15 @@ class Quarantine
|
|
66
69
|
raise Quarantine::DatabaseError
|
67
70
|
end
|
68
71
|
|
72
|
+
sig do
|
73
|
+
params(
|
74
|
+
table_name: String,
|
75
|
+
attributes: T::Array[Attribute],
|
76
|
+
additional_arguments: T::Hash[T.untyped, T.untyped]
|
77
|
+
).void
|
78
|
+
end
|
69
79
|
def create_table(table_name, attributes, additional_arguments = {})
|
70
|
-
dynamodb.create_table(
|
80
|
+
@dynamodb.create_table(
|
71
81
|
{
|
72
82
|
table_name: table_name,
|
73
83
|
attribute_definitions: attributes.map do |attribute|
|
data/lib/quarantine/error.rb
CHANGED
@@ -1,86 +1,105 @@
|
|
1
|
-
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module Quarantine::RSpecAdapter # rubocop:disable Style/ClassAndModuleChildren
|
4
|
+
extend T::Sig
|
5
|
+
|
2
6
|
# Purpose: create an instance of Quarantine which contains information
|
3
7
|
# about the test suite (ie. quarantined tests) and binds RSpec configurations
|
4
8
|
# and hooks onto the global RSpec class
|
5
|
-
|
6
|
-
|
9
|
+
sig { void }
|
10
|
+
def self.bind
|
7
11
|
bind_rspec_configurations
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
bind_logger
|
12
|
+
bind_fetch_test_statuses
|
13
|
+
bind_record_tests
|
14
|
+
bind_upload_tests
|
15
|
+
bind_logger
|
12
16
|
end
|
13
17
|
|
14
|
-
|
18
|
+
sig { returns(Quarantine) }
|
19
|
+
def self.quarantine
|
20
|
+
@quarantine = T.let(@quarantine, T.nilable(Quarantine))
|
21
|
+
@quarantine ||= Quarantine.new(
|
22
|
+
database: RSpec.configuration.quarantine_database,
|
23
|
+
test_statuses_table_name: RSpec.configuration.quarantine_test_statuses,
|
24
|
+
extra_attributes: RSpec.configuration.quarantine_extra_attributes,
|
25
|
+
failsafe_limit: RSpec.configuration.quarantine_failsafe_limit
|
26
|
+
)
|
27
|
+
end
|
15
28
|
|
16
29
|
# Purpose: binds rspec configuration variables
|
17
|
-
|
30
|
+
sig { void }
|
31
|
+
def self.bind_rspec_configurations
|
18
32
|
::RSpec.configure do |config|
|
19
|
-
config.add_setting(:
|
20
|
-
config.add_setting(:
|
33
|
+
config.add_setting(:quarantine_database, default: { type: :dynamodb, region: 'us-west-1' })
|
34
|
+
config.add_setting(:quarantine_test_statuses, { default: 'test_statuses' })
|
21
35
|
config.add_setting(:skip_quarantined_tests, { default: true })
|
22
|
-
config.add_setting(:
|
23
|
-
config.add_setting(:quarantine_record_flaky_tests, { default: true })
|
36
|
+
config.add_setting(:quarantine_record_tests, { default: true })
|
24
37
|
config.add_setting(:quarantine_logging, { default: true })
|
38
|
+
config.add_setting(:quarantine_extra_attributes)
|
39
|
+
config.add_setting(:quarantine_failsafe_limit, default: 10)
|
40
|
+
config.add_setting(:quarantine_release_at_consecutive_passes)
|
25
41
|
end
|
26
42
|
end
|
27
43
|
|
28
|
-
# Purpose: binds quarantine to fetch the
|
29
|
-
|
44
|
+
# Purpose: binds quarantine to fetch the test_statuses from dynamodb in the before suite
|
45
|
+
sig { void }
|
46
|
+
def self.bind_fetch_test_statuses
|
30
47
|
::RSpec.configure do |config|
|
31
48
|
config.before(:suite) do
|
32
|
-
quarantine.
|
49
|
+
Quarantine::RSpecAdapter.quarantine.fetch_test_statuses
|
33
50
|
end
|
34
51
|
end
|
35
52
|
end
|
36
53
|
|
37
|
-
# Purpose: binds quarantine to
|
38
|
-
|
39
|
-
|
40
|
-
config.after(:each) do |example|
|
41
|
-
if RSpec.configuration.skip_quarantined_tests && quarantine.test_quarantined?(example)
|
42
|
-
quarantine.pass_flaky_test(example)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
# Purpose: binds quarantine to record failed and flaky tests
|
49
|
-
def bind_quarantine_record_tests(quarantine)
|
54
|
+
# Purpose: binds quarantine to record test statuses
|
55
|
+
sig { void }
|
56
|
+
def self.bind_record_tests
|
50
57
|
::RSpec.configure do |config|
|
51
58
|
config.after(:each) do |example|
|
52
59
|
metadata = example.metadata
|
53
60
|
|
54
|
-
# will record the failed test if is not quarantined and it is on it's final retry from the rspec-retry gem
|
55
|
-
quarantine.record_failed_test(example) if
|
56
|
-
RSpec.configuration.quarantine_record_failed_tests &&
|
57
|
-
!quarantine.test_quarantined?(example) &&
|
58
|
-
metadata[:retry_attempts] + 1 == metadata[:retry] && example.exception
|
59
|
-
|
60
|
-
# will record the flaky test if is not quarantined and it failed the first run but passed a subsequent run;
|
61
61
|
# optionally, the upstream RSpec configuration could define an after hook that marks an example as flaky in
|
62
62
|
# the example's metadata
|
63
|
-
quarantine.
|
64
|
-
|
65
|
-
|
66
|
-
|
63
|
+
quarantined = Quarantine::RSpecAdapter.quarantine.test_quarantined?(example) || metadata[:flaky]
|
64
|
+
if example.exception
|
65
|
+
if metadata[:retry_attempts] + 1 == metadata[:retry]
|
66
|
+
# will record the failed test if it's final retry from the rspec-retry gem
|
67
|
+
if RSpec.configuration.skip_quarantined_tests && quarantined
|
68
|
+
example.clear_exception!
|
69
|
+
Quarantine::RSpecAdapter.quarantine.record_test(example, :quarantined, passed: false)
|
70
|
+
else
|
71
|
+
Quarantine::RSpecAdapter.quarantine.record_test(example, :failing, passed: false)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
elsif metadata[:retry_attempts] > 0
|
75
|
+
# will record the flaky test if it failed the first run but passed a subsequent run
|
76
|
+
Quarantine::RSpecAdapter.quarantine.record_test(example, :quarantined, passed: false)
|
77
|
+
elsif quarantined
|
78
|
+
Quarantine::RSpecAdapter.quarantine.record_test(example, :quarantined, passed: true)
|
79
|
+
else
|
80
|
+
Quarantine::RSpecAdapter.quarantine.record_test(example, :passing, passed: true)
|
81
|
+
end
|
67
82
|
end
|
68
83
|
end
|
84
|
+
end
|
69
85
|
|
86
|
+
sig { void }
|
87
|
+
def self.bind_upload_tests
|
70
88
|
::RSpec.configure do |config|
|
71
89
|
config.after(:suite) do
|
72
|
-
quarantine.upload_tests
|
73
|
-
|
74
|
-
quarantine.upload_tests(:flaky) if RSpec.configuration.quarantine_record_flaky_tests
|
90
|
+
Quarantine::RSpecAdapter.quarantine.upload_tests if RSpec.configuration.quarantine_record_tests
|
75
91
|
end
|
76
92
|
end
|
77
93
|
end
|
78
94
|
|
79
95
|
# Purpose: binds quarantine logger to output test to RSpec formatter messages
|
80
|
-
|
96
|
+
sig { void }
|
97
|
+
def self.bind_logger
|
81
98
|
::RSpec.configure do |config|
|
82
99
|
config.after(:suite) do
|
83
|
-
|
100
|
+
if RSpec.configuration.quarantine_logging
|
101
|
+
RSpec.configuration.reporter.message(Quarantine::RSpecAdapter.quarantine.summary)
|
102
|
+
end
|
84
103
|
end
|
85
104
|
end
|
86
105
|
end
|
data/lib/quarantine/test.rb
CHANGED
@@ -1,32 +1,25 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
1
3
|
class Quarantine
|
2
|
-
class Test
|
3
|
-
|
4
|
-
attr_accessor :full_description
|
5
|
-
attr_accessor :location
|
6
|
-
attr_accessor :build_number
|
4
|
+
class Test < T::Struct
|
5
|
+
extend T::Sig
|
7
6
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
7
|
+
const :id, String
|
8
|
+
const :status, Symbol
|
9
|
+
const :consecutive_passes, Integer
|
10
|
+
const :full_description, String
|
11
|
+
const :location, String
|
12
|
+
const :extra_attributes, T::Hash[T.untyped, T.untyped]
|
14
13
|
|
14
|
+
sig { returns(Quarantine::Databases::Base::Item) }
|
15
15
|
def to_hash
|
16
|
-
{
|
17
|
-
id: id,
|
18
|
-
full_description: full_description,
|
19
|
-
location: location,
|
20
|
-
build_number: build_number
|
21
|
-
}
|
22
|
-
end
|
23
|
-
|
24
|
-
def to_string_hash
|
25
16
|
{
|
26
17
|
'id' => id,
|
18
|
+
'last_status' => status.to_s,
|
19
|
+
'consecutive_passes' => consecutive_passes,
|
27
20
|
'full_description' => full_description,
|
28
21
|
'location' => location,
|
29
|
-
'
|
22
|
+
'extra_attributes' => extra_attributes
|
30
23
|
}
|
31
24
|
end
|
32
25
|
end
|
data/lib/quarantine/version.rb
CHANGED
data/quarantine.gemspec
CHANGED
@@ -11,14 +11,14 @@ Gem::Specification.new do |s|
|
|
11
11
|
s.version = Quarantine::VERSION
|
12
12
|
s.authors = ['Flexport Engineering, Eric Zhu']
|
13
13
|
s.email = ['ericzhu77@gmail.com']
|
14
|
-
s.summary = 'Quarantine flaky
|
14
|
+
s.summary = 'Quarantine flaky RSpec tests'
|
15
15
|
s.homepage = 'https://github.com/flexport/quarantine'
|
16
16
|
s.license = 'MIT'
|
17
17
|
s.files = Dir['{lib, bin}/**/*', '*.md', '*.gemspec']
|
18
18
|
s.executables = ['quarantine_dynamodb']
|
19
19
|
s.required_ruby_version = '>= 2.0'
|
20
20
|
|
21
|
-
s.add_dependency('aws-sdk-dynamodb', '~> 1')
|
22
21
|
s.add_dependency('rspec', '~> 3.0')
|
23
22
|
s.add_dependency('rspec-retry', '~> 0.6')
|
23
|
+
s.add_dependency('sorbet-runtime', '0.5.6338')
|
24
24
|
end
|
metadata
CHANGED
@@ -1,57 +1,57 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: quarantine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Flexport Engineering, Eric Zhu
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-04-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: rspec
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '3.0'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '3.0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name: rspec
|
28
|
+
name: rspec-retry
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '0.6'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '0.6'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: sorbet-runtime
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- -
|
45
|
+
- - '='
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
47
|
+
version: 0.5.6338
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - '='
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
54
|
+
version: 0.5.6338
|
55
55
|
description:
|
56
56
|
email:
|
57
57
|
- ericzhu77@gmail.com
|
@@ -91,8 +91,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
91
91
|
- !ruby/object:Gem::Version
|
92
92
|
version: '0'
|
93
93
|
requirements: []
|
94
|
-
rubygems_version: 3.0.
|
94
|
+
rubygems_version: 3.0.8
|
95
95
|
signing_key:
|
96
96
|
specification_version: 4
|
97
|
-
summary: Quarantine flaky
|
97
|
+
summary: Quarantine flaky RSpec tests
|
98
98
|
test_files: []
|