quarantine 1.0.4 → 2.1.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 +13 -5
- data/README.md +87 -78
- data/lib/quarantine.rb +120 -102
- data/lib/quarantine/cli.rb +23 -30
- data/lib/quarantine/databases/base.rb +16 -8
- data/lib/quarantine/databases/dynamo_db.rb +38 -24
- data/lib/quarantine/databases/google_sheets.rb +107 -0
- data/lib/quarantine/error.rb +2 -0
- data/lib/quarantine/rspec_adapter.rb +86 -65
- data/lib/quarantine/test.rb +17 -15
- data/lib/quarantine/version.rb +1 -1
- data/quarantine.gemspec +2 -2
- metadata +20 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ffb721a4514eda413ea4c096913b5d0c3a672b23fe6aefee44393062f202e453
|
4
|
+
data.tar.gz: eec03b0ec1533643ae86245e76a69b51fdbf871e6e621293fa542b1b252fc123
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 67f36af412a0f1b23aaa018402e81b087f9595d3257726aac3b4e6472f232a498be52d683fd6b0216f652ad3f2b702d6a5f582de70a62a8fb25fc29ec5a21df1
|
7
|
+
data.tar.gz: 3aabf31b49b00cded2af25bac1b8d9f3c1c1bb89a202689a87ded5e23369fb6e4d32b82f664d3c4d3b1f314cae02f34dc80ca0a0380e6c034466e7ae8ec15300
|
data/CHANGELOG.md
CHANGED
@@ -1,12 +1,20 @@
|
|
1
|
+
### 1.0.7
|
2
|
+
Support `it_behaves_like` behavior
|
3
|
+
|
4
|
+
### 1.0.6
|
5
|
+
Update DynamoDB batch_write_item implementation to check for duplicates based on different keys before uploading
|
6
|
+
|
7
|
+
### 1.0.5
|
8
|
+
Add aws_credentials argument during dynamodb initialization to override the AWS SDK credential chain
|
9
|
+
|
1
10
|
### 1.0.4
|
2
|
-
|
11
|
+
Enable upstream callers to mark an example as flaky through the example's metadata
|
3
12
|
|
4
13
|
### 1.0.3
|
5
|
-
|
14
|
+
Only require dynamodb instead of full aws-sdk
|
6
15
|
|
7
16
|
### 1.0.2
|
8
|
-
|
9
|
-
compatible with v2
|
17
|
+
Relax Aws gem version constraint as aws-sdk v3 is 100% backwards compatible with v2
|
10
18
|
|
11
19
|
### 1.0.1
|
12
|
-
|
20
|
+
Initial Release
|
data/README.md
CHANGED
@@ -1,154 +1,163 @@
|
|
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.
|
13
|
-
|
14
|
-
## Purpose
|
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.
|
16
|
-
|
17
|
-
The workflow at Flexport involves:
|
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.
|
18
11
|
|
19
|
-
|
12
|
+
## Getting started
|
20
13
|
|
21
|
-
|
22
|
-
## Installation and Setup
|
14
|
+
Quarantine works in tandem with [RSpec::Retry](https://github.com/NoRedInk/rspec-retry). Add this to your `Gemfile` and run `bundle install`:
|
23
15
|
|
24
|
-
Add these lines to your application's Gemfile:
|
25
16
|
```rb
|
26
17
|
group :test do
|
27
18
|
gem 'quarantine'
|
28
|
-
gem 'rspec-retry
|
19
|
+
gem 'rspec-retry'
|
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`
|
100
|
+
- `:google_sheets`
|
106
101
|
|
107
|
-
|
102
|
+
To use `:dynamodb`, be sure to add `gem 'aws-sdk-dynamodb', '~> 1', group: :test` to your `Gemfile`.
|
108
103
|
|
109
|
-
|
104
|
+
To use `:google_sheets`, be sure to add `gem 'google_drive', '~> 2', group: :test` to your `Gemfile`. Here's an example:
|
110
105
|
|
111
|
-
|
112
|
-
|
106
|
+
```rb
|
107
|
+
config.quarantine_database = {
|
108
|
+
type: :google_sheets,
|
109
|
+
authorization: {type: :service_account_key, file: "service_account.json"}, # also accepts `type: :config`
|
110
|
+
spreadsheet: {type: :by_key, "1Jb5fC6wSuIMnP85tUR5knuZ4f5fuu4nMzQF6-0l-EXAMPLE"}, # also accepts `type: :by_title` and `type: :by_url`
|
111
|
+
}
|
112
|
+
```
|
113
113
|
|
114
|
-
To
|
114
|
+
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:
|
115
115
|
|
116
|
-
|
116
|
+
```rb
|
117
|
+
class MyDatabase < Quarantine::Databases::Base
|
118
|
+
...
|
119
|
+
end
|
120
|
+
|
121
|
+
RSpec.configure do |config|
|
122
|
+
config.quarantine_database = MyDatabase.new(...)
|
123
|
+
end
|
124
|
+
```
|
125
|
+
|
126
|
+
### Extra attributes
|
127
|
+
|
128
|
+
Use `quarantine_extra_attributes` to store custom data with each test in the database, e.g. variables useful for your CI setup.
|
129
|
+
|
130
|
+
```rb
|
131
|
+
config.quarantine_extra_attributes = Proc.new do |example|
|
132
|
+
{
|
133
|
+
build_url: ENV['BUILDKITE_BUILD_URL'],
|
134
|
+
job_id: ENV['BUILDKITE_JOB_ID'],
|
135
|
+
}
|
136
|
+
end
|
137
|
+
```
|
117
138
|
|
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
139
|
---
|
130
140
|
|
131
141
|
## FAQs
|
132
142
|
|
133
143
|
#### Why are quarantined tests not being skipped locally?
|
134
144
|
|
135
|
-
|
145
|
+
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
146
|
|
137
147
|
```sh
|
138
|
-
CI=
|
148
|
+
CI=1 BRANCH=master bundle exec rspec
|
139
149
|
```
|
140
150
|
|
141
|
-
#### Why is
|
151
|
+
#### Why is Quarantine failing to connect to DynamoDB?
|
142
152
|
|
143
|
-
The AWS client loads credentials from the following locations:
|
153
|
+
The AWS client attempts to loads credentials from the following locations, in order:
|
154
|
+
- The optional `credentials` field in `RSpec.configuration.quarantine_database`
|
144
155
|
- `ENV['AWS_ACCESS_KEY_ID']` and `ENV['AWS_SECRET_ACCESS_KEY']`
|
145
156
|
- `Aws.config[:credentials]`
|
146
157
|
- The shared credentials ini file at `~/.aws/credentials`
|
147
158
|
|
148
|
-
|
149
|
-
|
150
|
-
More detailed information can be found: [AWS documentation](https://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Client.html)
|
159
|
+
More detailed information can be found in the [AWS SDK documentation](https://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Client.html)
|
151
160
|
|
152
161
|
#### Why is `example.clear_exception` failing locally?
|
153
|
-
|
154
|
-
|
162
|
+
|
163
|
+
`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,141 +1,159 @@
|
|
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'
|
4
8
|
require 'quarantine/databases/base'
|
5
9
|
require 'quarantine/databases/dynamo_db'
|
10
|
+
require 'quarantine/databases/google_sheets'
|
11
|
+
|
12
|
+
module RSpec
|
13
|
+
module Core
|
14
|
+
class Example
|
15
|
+
extend T::Sig
|
16
|
+
|
17
|
+
# The implementation of clear_exception in rspec-retry doesn't work
|
18
|
+
# for examples that use `it_behaves_like`, so we implement our own version that
|
19
|
+
# clear the exception field recursively.
|
20
|
+
sig { void }
|
21
|
+
def clear_exception!
|
22
|
+
@exception = T.let(nil, T.untyped)
|
23
|
+
T.unsafe(self).example.clear_exception! if defined?(example)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
6
28
|
|
7
29
|
class Quarantine
|
8
|
-
extend
|
9
|
-
|
10
|
-
attr_accessor :database
|
11
|
-
attr_reader :quarantine_map
|
12
|
-
attr_reader :failed_tests
|
13
|
-
attr_reader :flaky_tests
|
14
|
-
attr_reader :duplicate_tests
|
15
|
-
attr_reader :buildkite_build_number
|
16
|
-
attr_reader :summary
|
17
|
-
|
18
|
-
def initialize(options = {})
|
19
|
-
case options[:database]
|
20
|
-
# default database option is dynamodb
|
21
|
-
when :dynamodb, nil
|
22
|
-
@database = Quarantine::Databases::DynamoDB.new(options)
|
23
|
-
else
|
24
|
-
raise Quarantine::UnsupportedDatabaseError.new("Quarantine does not support #{options[:database]}")
|
25
|
-
end
|
30
|
+
extend T::Sig
|
26
31
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
+
sig { returns(T::Hash[String, Quarantine::Test]) }
|
33
|
+
attr_reader :tests
|
34
|
+
|
35
|
+
sig { returns(T::Hash[String, Quarantine::Test]) }
|
36
|
+
attr_reader :old_tests
|
37
|
+
|
38
|
+
sig { params(options: T::Hash[T.untyped, T.untyped]).void }
|
39
|
+
def initialize(options)
|
40
|
+
@options = options
|
41
|
+
@old_tests = T.let({}, T::Hash[String, Quarantine::Test])
|
42
|
+
@tests = T.let({}, T::Hash[String, Quarantine::Test])
|
43
|
+
@database_failures = T.let([], T::Array[String])
|
44
|
+
@database = T.let(nil, T.nilable(Quarantine::Databases::Base))
|
45
|
+
end
|
46
|
+
|
47
|
+
sig { returns(Quarantine::Databases::Base) }
|
48
|
+
def database
|
49
|
+
@database ||=
|
50
|
+
case @options[:database]
|
51
|
+
when Quarantine::Databases::Base
|
52
|
+
@options[:database]
|
53
|
+
else
|
54
|
+
database_options = @options[:database].dup
|
55
|
+
type = database_options.delete(:type)
|
56
|
+
case type
|
57
|
+
when :dynamodb
|
58
|
+
Quarantine::Databases::DynamoDB.new(database_options)
|
59
|
+
when :google_sheets
|
60
|
+
Quarantine::Databases::GoogleSheets.new(database_options)
|
61
|
+
else
|
62
|
+
raise Quarantine::UnsupportedDatabaseError.new("Quarantine does not support database type: #{type.inspect}")
|
63
|
+
end
|
64
|
+
end
|
32
65
|
end
|
33
66
|
|
34
|
-
# Scans the
|
35
|
-
|
67
|
+
# Scans the test_statuses from the database and store their IDs in quarantined_ids
|
68
|
+
sig { void }
|
69
|
+
def fetch_test_statuses
|
36
70
|
begin
|
37
|
-
|
71
|
+
test_statuses = database.fetch_items(@options[:test_statuses_table_name])
|
38
72
|
rescue Quarantine::DatabaseError => e
|
39
|
-
|
73
|
+
@database_failures << "#{e.cause&.class}: #{e.cause&.message}"
|
40
74
|
raise Quarantine::DatabaseError.new(
|
41
75
|
<<~ERROR_MSG
|
42
|
-
Failed to pull the quarantine list from #{
|
43
|
-
because of #{e
|
76
|
+
Failed to pull the quarantine list from #{@options[:test_statuses_table_name]}
|
77
|
+
because of #{e.cause&.class}: #{e.cause&.message}
|
44
78
|
ERROR_MSG
|
45
79
|
)
|
46
80
|
end
|
47
81
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
82
|
+
pairs =
|
83
|
+
test_statuses
|
84
|
+
.group_by { |t| t['id'] }
|
85
|
+
.map { |_id, tests| tests.max_by { |t| t['created_at'] } }
|
86
|
+
.compact
|
87
|
+
.filter { |t| t['last_status'] == 'quarantined' }
|
88
|
+
.map do |t|
|
89
|
+
[
|
90
|
+
t['id'],
|
91
|
+
Quarantine::Test.new(
|
92
|
+
id: t['id'],
|
93
|
+
status: t['last_status'].to_sym,
|
94
|
+
consecutive_passes: t['consecutive_passes'].to_i,
|
95
|
+
full_description: t['full_description'],
|
96
|
+
location: t['location'],
|
97
|
+
extra_attributes: t['extra_attributes']
|
98
|
+
)
|
99
|
+
]
|
100
|
+
end
|
101
|
+
|
102
|
+
@old_tests = Hash[pairs]
|
61
103
|
end
|
62
104
|
|
63
|
-
|
64
|
-
def upload_tests
|
65
|
-
if
|
66
|
-
tests = failed_tests
|
67
|
-
table_name = RSpec.configuration.quarantine_failed_tests_table
|
68
|
-
elsif type == :flaky
|
69
|
-
tests = flaky_tests
|
70
|
-
table_name = RSpec.configuration.quarantine_list_table
|
71
|
-
else
|
72
|
-
raise Quarantine::UnknownUploadError.new(
|
73
|
-
"Quarantine gem did not know how to handle #{type} upload of tests to dynamodb"
|
74
|
-
)
|
75
|
-
end
|
76
|
-
|
77
|
-
return unless tests.length < 10 && tests.length > 0
|
105
|
+
sig { void }
|
106
|
+
def upload_tests
|
107
|
+
return if @tests.empty? || @tests.values.count { |test| test.status == :quarantined } >= @options[:failsafe_limit]
|
78
108
|
|
79
109
|
begin
|
80
110
|
timestamp = Time.now.to_i / 1000 # Truncated millisecond from timestamp for reasons specific to Flexport
|
81
|
-
database.
|
82
|
-
|
83
|
-
tests
|
84
|
-
{
|
85
|
-
build_job_id: ENV['BUILDKITE_JOB_ID'] || '-1',
|
86
|
-
created_at: timestamp,
|
87
|
-
updated_at: timestamp
|
88
|
-
}
|
111
|
+
database.write_items(
|
112
|
+
@options[:test_statuses_table_name],
|
113
|
+
@tests.values.map { |item| item.to_hash.merge('updated_at' => timestamp) }
|
89
114
|
)
|
90
115
|
rescue Quarantine::DatabaseError => e
|
91
|
-
|
116
|
+
@database_failures << "#{e.cause&.class}: #{e.cause&.message}"
|
92
117
|
end
|
93
118
|
end
|
94
119
|
|
95
120
|
# Param: RSpec::Core::Example
|
96
|
-
# Add the example to the internal
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
121
|
+
# Add the example to the internal tests list
|
122
|
+
sig { params(example: T.untyped, status: Symbol, passed: T::Boolean).void }
|
123
|
+
def record_test(example, status, passed:)
|
124
|
+
extra_attributes = @options[:extra_attributes] ? @options[:extra_attributes].call(example) : {}
|
125
|
+
|
126
|
+
new_consecutive_passes = passed ? (@old_tests[example.id]&.consecutive_passes || 0) + 1 : 0
|
127
|
+
release_at = @options[:release_at_consecutive_passes]
|
128
|
+
new_status = !release_at.nil? && new_consecutive_passes >= release_at ? :passing : status
|
129
|
+
test = Quarantine::Test.new(
|
130
|
+
id: example.id,
|
131
|
+
status: new_status,
|
132
|
+
consecutive_passes: new_consecutive_passes,
|
133
|
+
full_description: example.full_description,
|
134
|
+
location: example.location,
|
135
|
+
extra_attributes: extra_attributes
|
103
136
|
)
|
104
|
-
end
|
105
137
|
|
106
|
-
|
107
|
-
# Add the example to the internal flaky tests list
|
108
|
-
def record_flaky_test(example)
|
109
|
-
flaky_test = Quarantine::Test.new(
|
110
|
-
example.id,
|
111
|
-
example.full_description,
|
112
|
-
example.location,
|
113
|
-
buildkite_build_number
|
114
|
-
)
|
115
|
-
|
116
|
-
flaky_tests << flaky_test
|
117
|
-
add_to_summary(:flaky_tests, flaky_test.id)
|
138
|
+
@tests[test.id] = test
|
118
139
|
end
|
119
140
|
|
120
141
|
# Param: RSpec::Core::Example
|
121
|
-
#
|
122
|
-
|
123
|
-
# example.clear_exception is tightly coupled with the rspec-retry gem and will only exist if
|
124
|
-
# the rspec-retry gem is enabled
|
125
|
-
def pass_flaky_test(example)
|
126
|
-
example.clear_exception
|
127
|
-
add_to_summary(:quarantined_tests, example.id)
|
128
|
-
end
|
129
|
-
|
130
|
-
# Param: RSpec::Core::Example
|
131
|
-
# Check the internal quarantine_map to see if this test should be quarantined
|
142
|
+
# Check the internal old_tests to see if this test should be quarantined
|
143
|
+
sig { params(example: T.untyped).returns(T::Boolean) }
|
132
144
|
def test_quarantined?(example)
|
133
|
-
|
145
|
+
@old_tests[example.id]&.status == :quarantined
|
134
146
|
end
|
135
147
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
148
|
+
sig { returns(String) }
|
149
|
+
def summary
|
150
|
+
quarantined_tests = @tests.values.select { |test| test.status == :quarantined }.sort_by(&:id)
|
151
|
+
<<~MESSAGE
|
152
|
+
\n[quarantine] Quarantined tests:
|
153
|
+
#{quarantined_tests.map { |test| "#{test.id} #{test.full_description}" }.join("\n ")}
|
154
|
+
|
155
|
+
[quarantine] Database errors:
|
156
|
+
#{@database_failures.join("\n ")}
|
157
|
+
MESSAGE
|
140
158
|
end
|
141
159
|
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,25 @@
|
|
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
|
-
|
10
|
-
|
9
|
+
abstract!
|
10
|
+
|
11
|
+
Item = T.type_alias { T::Hash[String, T.untyped] } # TODO: must have `id` key
|
12
|
+
|
13
|
+
sig { abstract.params(table_name: String).returns(T::Enumerable[Item]) }
|
14
|
+
def fetch_items(table_name); end
|
11
15
|
|
12
|
-
|
13
|
-
|
16
|
+
sig do
|
17
|
+
abstract.params(
|
18
|
+
table_name: String,
|
19
|
+
items: T::Array[Item]
|
20
|
+
).void
|
14
21
|
end
|
22
|
+
def write_items(table_name, items); end
|
15
23
|
end
|
16
24
|
end
|
17
25
|
end
|
@@ -1,57 +1,71 @@
|
|
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
|
-
|
20
|
+
sig { params(options: T::Hash[T.untyped, T.untyped]).void }
|
21
|
+
def initialize(options)
|
22
|
+
super()
|
23
|
+
|
24
|
+
@dynamodb = T.let(Aws::DynamoDB::Client.new(options), Aws::DynamoDB::Client)
|
12
25
|
end
|
13
26
|
|
14
|
-
|
27
|
+
sig { override.params(table_name: String).returns(T::Enumerable[Item]) }
|
28
|
+
def fetch_items(table_name)
|
15
29
|
begin
|
16
|
-
result = dynamodb.scan(
|
30
|
+
result = @dynamodb.scan(table_name: table_name)
|
17
31
|
rescue Aws::DynamoDB::Errors::ServiceError
|
18
32
|
raise Quarantine::DatabaseError
|
19
33
|
end
|
20
34
|
|
21
|
-
result
|
35
|
+
result.items
|
22
36
|
end
|
23
37
|
|
24
|
-
|
25
|
-
|
26
|
-
|
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(
|
46
|
+
request_items: {
|
27
47
|
table_name => items.map do |item|
|
28
48
|
{
|
29
49
|
put_request: {
|
30
|
-
item:
|
50
|
+
item: item
|
31
51
|
}
|
32
52
|
}
|
33
53
|
end
|
34
|
-
} }
|
35
|
-
)
|
36
|
-
rescue Aws::DynamoDB::Errors::ServiceError
|
37
|
-
raise Quarantine::DatabaseError
|
38
|
-
end
|
39
|
-
|
40
|
-
def delete_item(table_name, keys)
|
41
|
-
dynamodb.delete_item(
|
42
|
-
{
|
43
|
-
table_name: table_name,
|
44
|
-
key: {
|
45
|
-
**keys
|
46
|
-
}
|
47
54
|
}
|
48
55
|
)
|
49
56
|
rescue Aws::DynamoDB::Errors::ServiceError
|
50
57
|
raise Quarantine::DatabaseError
|
51
58
|
end
|
52
59
|
|
60
|
+
sig do
|
61
|
+
params(
|
62
|
+
table_name: String,
|
63
|
+
attributes: T::Array[Attribute],
|
64
|
+
additional_arguments: T::Hash[T.untyped, T.untyped]
|
65
|
+
).void
|
66
|
+
end
|
53
67
|
def create_table(table_name, attributes, additional_arguments = {})
|
54
|
-
dynamodb.create_table(
|
68
|
+
@dynamodb.create_table(
|
55
69
|
{
|
56
70
|
table_name: table_name,
|
57
71
|
attribute_definitions: attributes.map do |attribute|
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'google_drive'
|
5
|
+
rescue LoadError
|
6
|
+
end
|
7
|
+
require 'quarantine/databases/base'
|
8
|
+
|
9
|
+
class Quarantine
|
10
|
+
module Databases
|
11
|
+
class GoogleSheets < Base
|
12
|
+
extend T::Sig
|
13
|
+
|
14
|
+
sig { params(options: T::Hash[T.untyped, T.untyped]).void }
|
15
|
+
def initialize(options)
|
16
|
+
super()
|
17
|
+
|
18
|
+
@options = options
|
19
|
+
end
|
20
|
+
|
21
|
+
sig { override.params(table_name: String).returns(T::Enumerable[Item]) }
|
22
|
+
def fetch_items(table_name)
|
23
|
+
parse_rows(spreadsheet.worksheet_by_title(table_name))
|
24
|
+
end
|
25
|
+
|
26
|
+
sig do
|
27
|
+
override.params(
|
28
|
+
table_name: String,
|
29
|
+
items: T::Array[Item]
|
30
|
+
).void
|
31
|
+
end
|
32
|
+
def write_items(table_name, items)
|
33
|
+
worksheet = spreadsheet.worksheet_by_title(table_name)
|
34
|
+
headers = worksheet.rows.first.reject(&:empty?)
|
35
|
+
new_rows = []
|
36
|
+
|
37
|
+
# Map existing ID to row index
|
38
|
+
indexes = Hash[parse_rows(worksheet).each_with_index.map { |item, idx| [item['id'], idx] }]
|
39
|
+
|
40
|
+
items.each do |item|
|
41
|
+
cells = headers.map { |header| item[header].to_s }
|
42
|
+
row_idx = indexes[item['id']]
|
43
|
+
if row_idx
|
44
|
+
# Overwrite existing row
|
45
|
+
headers.each_with_index do |_header, col_idx|
|
46
|
+
worksheet[row_idx + 2, col_idx + 1] = cells[col_idx]
|
47
|
+
end
|
48
|
+
else
|
49
|
+
new_rows << cells
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Insert any items whose IDs weren't found in existing rows at the end
|
54
|
+
worksheet.insert_rows(worksheet.rows.count + 1, new_rows)
|
55
|
+
worksheet.save
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
sig { returns(GoogleDrive::Session) }
|
61
|
+
def session
|
62
|
+
@session = T.let(@session, T.nilable(GoogleDrive::Session))
|
63
|
+
@session ||= begin
|
64
|
+
authorization = @options[:authorization]
|
65
|
+
case authorization[:type]
|
66
|
+
when :service_account_key
|
67
|
+
GoogleDrive::Session.from_service_account_key(authorization[:file])
|
68
|
+
when :config
|
69
|
+
GoogleDrive::Session.from_config(authorization[:file])
|
70
|
+
else
|
71
|
+
raise "Invalid authorization type: #{authorization[:type]}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
sig { returns(GoogleDrive::Spreadsheet) }
|
77
|
+
def spreadsheet
|
78
|
+
@spreadsheet = T.let(@spreadsheet, T.nilable(GoogleDrive::Spreadsheet))
|
79
|
+
@spreadsheet ||= begin
|
80
|
+
spreadsheet = @options[:spreadsheet]
|
81
|
+
case spreadsheet[:type]
|
82
|
+
when :by_key
|
83
|
+
session.spreadsheet_by_key(spreadsheet[:key])
|
84
|
+
when :by_title
|
85
|
+
session.spreadsheet_by_title(spreadsheet[:title])
|
86
|
+
when :by_url
|
87
|
+
session.spreadsheet_by_url(spreadsheet[:url])
|
88
|
+
else
|
89
|
+
raise "Invalid spreadsheet type: #{spreadsheet[:type]}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
sig { params(worksheet: GoogleDrive::Worksheet).returns(T::Enumerable[Item]) }
|
95
|
+
def parse_rows(worksheet)
|
96
|
+
headers, *rows = worksheet.rows
|
97
|
+
|
98
|
+
rows.map do |row|
|
99
|
+
hash_row = Hash[headers.zip(row)]
|
100
|
+
# TODO: use Google Sheets developer metadata to store type information
|
101
|
+
hash_row['extra_attributes'] = JSON.parse(hash_row['extra_attributes']) if hash_row['extra_attributes']
|
102
|
+
hash_row
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
data/lib/quarantine/error.rb
CHANGED
@@ -1,86 +1,107 @@
|
|
1
|
-
|
2
|
-
# Purpose: create an instance of Quarantine which contains information
|
3
|
-
# about the test suite (ie. quarantined tests) and binds RSpec configurations
|
4
|
-
# and hooks onto the global RSpec class
|
5
|
-
def bind(options = {})
|
6
|
-
quarantine = Quarantine.new(options)
|
7
|
-
bind_rspec_configurations
|
8
|
-
bind_quarantine_list(quarantine)
|
9
|
-
bind_quarantine_checker(quarantine)
|
10
|
-
bind_quarantine_record_tests(quarantine)
|
11
|
-
bind_logger(quarantine)
|
12
|
-
end
|
1
|
+
# typed: strict
|
13
2
|
|
14
|
-
|
3
|
+
class Quarantine
|
4
|
+
module RSpecAdapter
|
5
|
+
extend T::Sig
|
15
6
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
7
|
+
# Purpose: create an instance of Quarantine which contains information
|
8
|
+
# about the test suite (ie. quarantined tests) and binds RSpec configurations
|
9
|
+
# and hooks onto the global RSpec class
|
10
|
+
sig { void }
|
11
|
+
def self.bind
|
12
|
+
bind_rspec_configurations
|
13
|
+
bind_fetch_test_statuses
|
14
|
+
bind_record_tests
|
15
|
+
bind_upload_tests
|
16
|
+
bind_logger
|
17
|
+
end
|
18
|
+
|
19
|
+
sig { returns(Quarantine) }
|
20
|
+
def self.quarantine
|
21
|
+
@quarantine = T.let(@quarantine, T.nilable(Quarantine))
|
22
|
+
@quarantine ||= Quarantine.new(
|
23
|
+
database: RSpec.configuration.quarantine_database,
|
24
|
+
test_statuses_table_name: RSpec.configuration.quarantine_test_statuses,
|
25
|
+
extra_attributes: RSpec.configuration.quarantine_extra_attributes,
|
26
|
+
failsafe_limit: RSpec.configuration.quarantine_failsafe_limit
|
27
|
+
)
|
25
28
|
end
|
26
|
-
end
|
27
29
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
30
|
+
# Purpose: binds rspec configuration variables
|
31
|
+
sig { void }
|
32
|
+
def self.bind_rspec_configurations
|
33
|
+
::RSpec.configure do |config|
|
34
|
+
config.add_setting(:quarantine_database, default: { type: :dynamodb, region: 'us-west-1' })
|
35
|
+
config.add_setting(:quarantine_test_statuses, { default: 'test_statuses' })
|
36
|
+
config.add_setting(:skip_quarantined_tests, { default: true })
|
37
|
+
config.add_setting(:quarantine_record_tests, { default: true })
|
38
|
+
config.add_setting(:quarantine_logging, { default: true })
|
39
|
+
config.add_setting(:quarantine_extra_attributes)
|
40
|
+
config.add_setting(:quarantine_failsafe_limit, default: 10)
|
41
|
+
config.add_setting(:quarantine_release_at_consecutive_passes)
|
33
42
|
end
|
34
43
|
end
|
35
|
-
end
|
36
44
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
quarantine.
|
45
|
+
# Purpose: binds quarantine to fetch the test_statuses from dynamodb in the before suite
|
46
|
+
sig { void }
|
47
|
+
def self.bind_fetch_test_statuses
|
48
|
+
::RSpec.configure do |config|
|
49
|
+
config.before(:suite) do
|
50
|
+
Quarantine::RSpecAdapter.quarantine.fetch_test_statuses
|
43
51
|
end
|
44
52
|
end
|
45
53
|
end
|
46
|
-
end
|
47
54
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
55
|
+
# Purpose: binds quarantine to record test statuses
|
56
|
+
sig { void }
|
57
|
+
def self.bind_record_tests
|
58
|
+
::RSpec.configure do |config|
|
59
|
+
config.after(:each) do |example|
|
60
|
+
metadata = example.metadata
|
53
61
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
62
|
+
# optionally, the upstream RSpec configuration could define an after hook that marks an example as flaky in
|
63
|
+
# the example's metadata
|
64
|
+
quarantined = Quarantine::RSpecAdapter.quarantine.test_quarantined?(example) || metadata[:flaky]
|
65
|
+
if example.exception
|
66
|
+
if metadata[:retry_attempts] + 1 == metadata[:retry]
|
67
|
+
# will record the failed test if it's final retry from the rspec-retry gem
|
68
|
+
if RSpec.configuration.skip_quarantined_tests && quarantined
|
69
|
+
example.clear_exception!
|
70
|
+
Quarantine::RSpecAdapter.quarantine.record_test(example, :quarantined, passed: false)
|
71
|
+
else
|
72
|
+
Quarantine::RSpecAdapter.quarantine.record_test(example, :failing, passed: false)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
elsif metadata[:retry_attempts] > 0
|
76
|
+
# will record the flaky test if it failed the first run but passed a subsequent run
|
77
|
+
Quarantine::RSpecAdapter.quarantine.record_test(example, :quarantined, passed: false)
|
78
|
+
elsif quarantined
|
79
|
+
Quarantine::RSpecAdapter.quarantine.record_test(example, :quarantined, passed: true)
|
80
|
+
else
|
81
|
+
Quarantine::RSpecAdapter.quarantine.record_test(example, :passing, passed: true)
|
82
|
+
end
|
83
|
+
end
|
67
84
|
end
|
68
85
|
end
|
69
86
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
87
|
+
sig { void }
|
88
|
+
def self.bind_upload_tests
|
89
|
+
::RSpec.configure do |config|
|
90
|
+
config.after(:suite) do
|
91
|
+
Quarantine::RSpecAdapter.quarantine.upload_tests if RSpec.configuration.quarantine_record_tests
|
92
|
+
end
|
75
93
|
end
|
76
94
|
end
|
77
|
-
end
|
78
95
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
96
|
+
# Purpose: binds quarantine logger to output test to RSpec formatter messages
|
97
|
+
sig { void }
|
98
|
+
def self.bind_logger
|
99
|
+
::RSpec.configure do |config|
|
100
|
+
config.after(:suite) do
|
101
|
+
if RSpec.configuration.quarantine_logging
|
102
|
+
RSpec.configuration.reporter.message(Quarantine::RSpecAdapter.quarantine.summary)
|
103
|
+
end
|
104
|
+
end
|
84
105
|
end
|
85
106
|
end
|
86
107
|
end
|
data/lib/quarantine/test.rb
CHANGED
@@ -1,23 +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
16
|
{
|
17
|
-
id
|
18
|
-
|
19
|
-
|
20
|
-
|
17
|
+
'id' => id,
|
18
|
+
'last_status' => status.to_s,
|
19
|
+
'consecutive_passes' => consecutive_passes,
|
20
|
+
'full_description' => full_description,
|
21
|
+
'location' => location,
|
22
|
+
'extra_attributes' => extra_attributes
|
21
23
|
}
|
22
24
|
end
|
23
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,58 +1,58 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: quarantine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Flexport Engineering, Eric Zhu
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-04-14 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:
|
55
|
-
description:
|
54
|
+
version: 0.5.6338
|
55
|
+
description:
|
56
56
|
email:
|
57
57
|
- ericzhu77@gmail.com
|
58
58
|
executables:
|
@@ -67,6 +67,7 @@ files:
|
|
67
67
|
- lib/quarantine/cli.rb
|
68
68
|
- lib/quarantine/databases/base.rb
|
69
69
|
- lib/quarantine/databases/dynamo_db.rb
|
70
|
+
- lib/quarantine/databases/google_sheets.rb
|
70
71
|
- lib/quarantine/error.rb
|
71
72
|
- lib/quarantine/rspec_adapter.rb
|
72
73
|
- lib/quarantine/test.rb
|
@@ -76,7 +77,7 @@ homepage: https://github.com/flexport/quarantine
|
|
76
77
|
licenses:
|
77
78
|
- MIT
|
78
79
|
metadata: {}
|
79
|
-
post_install_message:
|
80
|
+
post_install_message:
|
80
81
|
rdoc_options: []
|
81
82
|
require_paths:
|
82
83
|
- lib
|
@@ -91,8 +92,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
91
92
|
- !ruby/object:Gem::Version
|
92
93
|
version: '0'
|
93
94
|
requirements: []
|
94
|
-
rubygems_version: 3.0.
|
95
|
-
signing_key:
|
95
|
+
rubygems_version: 3.0.8
|
96
|
+
signing_key:
|
96
97
|
specification_version: 4
|
97
|
-
summary: Quarantine flaky
|
98
|
+
summary: Quarantine flaky RSpec tests
|
98
99
|
test_files: []
|