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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 16d6eb14a6e87360b3f61ea7e35ff7659ed3ac2c4593e4b63db6aef23841e592
4
- data.tar.gz: 1acc1bd90b2d3849f3c9cbfb0210ddf5d7cafe8ce3254376c736d67268a12f55
3
+ metadata.gz: ffb721a4514eda413ea4c096913b5d0c3a672b23fe6aefee44393062f202e453
4
+ data.tar.gz: eec03b0ec1533643ae86245e76a69b51fdbf871e6e621293fa542b1b252fc123
5
5
  SHA512:
6
- metadata.gz: 21256b85495c744328f43b1b2cb4094179d78689352757dcdeb067f822f402f9c28cf4fb12231e60ed1e1c6864ad2a48f8b7f7aa909e0c9d16a772dcde5e4c62
7
- data.tar.gz: 1f50bbccf8273705894b2ebfb2ef9a6bdb5807ff4b2ea6f1e4dd5e3fc2cab3f2303b9ba8e9a5d5768a3e3ba293a074698bd28725865258dcaedfc7483cfa1c55
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
- Enable upstream callers to mark an example as flaky through the example's metadata
11
+ Enable upstream callers to mark an example as flaky through the example's metadata
3
12
 
4
13
  ### 1.0.3
5
- Only require dynamodb instead of full aws-sdk
14
+ Only require dynamodb instead of full aws-sdk
6
15
 
7
16
  ### 1.0.2
8
- Relax Aws gem version constraint as aws-sdk v3 is 100% backwards
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
- Initial Release
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 provides a run-time solution to diagnosing and disabling flaky tests and automates the workflow around test suite maintenance.
5
+ Quarantine automatically detects flaky tests (i.e. those which fail non-deterministically) and disables them until they're proven reliable.
5
6
 
6
- The quarantine gem supports testing frameworks:
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
- The quarantine gem supports CI pipelines:
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
- ![ideal workflow](misc/flexport_workflow.png)
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
- And then execute:
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-retry'
27
+ require 'rspec/retry'
41
28
 
42
- Quarantine.bind({database: :dynamodb, aws_region: 'us-west-1'})
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 |ex|
47
- ex.run_with_retry(retry: 3)
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
- Consider wrapping `Quarantine.bind` in if statements so local flaky tests don't pollute the list of quarantined tests
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 # see all options
46
+ bundle exec quarantine_dynamodb -h # See all options
64
47
 
65
- bundle exec quarantine_dynamodb \ # create the tables in us-west-1 in aws dynamodb
66
- --aws_region us-west-1 # with "quarantine_list" and "master_failed_tests"
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
- You are all set to start quarantining tests!
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 "this test should flake 33% of the time" do
79
- Random.rand(3).to eq(3)
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
- If the test fails and passes on the test run (rspec-retry re-ran the test), the test should be quarantined and uploaded to DynamoDB. Check the `quarantine_list` table in DynamoDB.
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
- Go to `spec/spec_helper.rb` and set configuration variables through:
79
+ In `spec_helper.rb`, you can set configuration variables by doing:
80
+
94
81
  ```rb
95
82
  RSpec.configure do |config|
96
- config.VAR_NAME = VALUE
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
- - Table name where failed test are uploaded `:quarantine_failed_tests_table, default: "master_failed_tests"`
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
- - Quarantined tests are not skipped automatically `:skip_quarantined_tests, default: true`
96
+ ### Databases
104
97
 
105
- - Recording failed tests `:quarantine_record_failed_tests, default: true`
98
+ Quarantine comes with built-in support for the following database types:
99
+ - `:dynamodb`
100
+ - `:google_sheets`
106
101
 
107
- - Recording flaky tests `:quarantine_record_flaky_tests, default: true`
102
+ To use `:dynamodb`, be sure to add `gem 'aws-sdk-dynamodb', '~> 1', group: :test` to your `Gemfile`.
108
103
 
109
- - Outputting quarantined gem info `:quarantine_logging, default: true`
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
- ## Setup Jira Workflow
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 automatically create Jira tickets, take a look at: `examples/create_tickets.rb`
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
- To automatically unquarantine tests on Jira ticket completion, take a look at: `examples/unquarantine.rb`
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
- The `quarantine` gem may be configured to only run on certain environments. Make sure you pass all these `ENV` variables to `rspec` when you call it locally
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="1" BRANCH="master" rspec
148
+ CI=1 BRANCH=master bundle exec rspec
139
149
  ```
140
150
 
141
- #### Why is dynamodb failing to connect?
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
- To get AWS credentials, please contact your AWS administrator to get access to dynamodb and create your credentials through IAM.
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
- `example.clear_exception` is an attribute added through `rspec_retry`. Make sure `rspec-retry` has been installed and configured.
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 RSpecAdapter
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
- @quarantine_map = {}
28
- @failed_tests = []
29
- @flaky_tests = []
30
- @buildkite_build_number = ENV['BUILDKITE_BUILD_NUMBER'] || '-1'
31
- @summary = { id: 'quarantine', quarantined_tests: [], flaky_tests: [], database_failures: [] }
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 quarantine_list from the database and store the individual tests in quarantine_map
35
- def fetch_quarantine_list
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
- quarantine_list = database.scan(RSpec.configuration.quarantine_list_table)
71
+ test_statuses = database.fetch_items(@options[:test_statuses_table_name])
38
72
  rescue Quarantine::DatabaseError => e
39
- add_to_summary(:database_failures, "#{e&.cause&.class}: #{e&.cause&.message}")
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 #{RSpec.configuration.quarantine_list_table}
43
- because of #{e&.cause&.class}: #{e&.cause&.message}
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
- quarantine_list.each do |example|
49
- # on the rare occassion there are duplicate tests ids in the quarantine_list,
50
- # quarantine the most recent instance of the test (det. through build_number)
51
- # and ignore the older instance of the test
52
- next if
53
- quarantine_map.key?(example['id']) &&
54
- example['build_number'].to_i < quarantine_map[example['id']].build_number.to_i
55
-
56
- quarantine_map.store(
57
- example['id'],
58
- Quarantine::Test.new(example['id'], example['full_description'], example['location'], example['build_number'])
59
- )
60
- end
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
- # Based off the type, upload a list of tests to a particular database table
64
- def upload_tests(type)
65
- if type == :failed
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.batch_write_item(
82
- table_name,
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
- add_to_summary(:database_failures, "#{e&.cause&.class}: #{e&.cause&.message}")
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 failed tests list
97
- def record_failed_test(example)
98
- failed_tests << Quarantine::Test.new(
99
- example.id,
100
- example.full_description,
101
- example.location,
102
- buildkite_build_number
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
- # Param: RSpec::Core::Example
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
- # Clear exceptions on a flaky tests that has been quarantined
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
- quarantine_map.key?(example.id)
145
+ @old_tests[example.id]&.status == :quarantined
134
146
  end
135
147
 
136
- # Param: Symbol, Any
137
- # Adds the item to the specified attribute in summary
138
- def add_to_summary(attribute, item)
139
- summary[attribute] << item if summary.key?(attribute)
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
@@ -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
- attr_accessor :options
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
- quarantine_list_table_name: 'quarantine_list',
13
- failed_test_table_name: 'master_failed_tests'
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', '--aws_region=REGION', String, 'Specify the aws region for DynamoDB') do |aws_region|
22
- options[:aws_region] = aws_region
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[:quarantine_list_table_name]}"
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[:failed_test_table_name] = table_name
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[:aws_region].nil?
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[:quarantine_list_table_name], attributes, additional_arguments)
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&.cause&.class}: #{e&.cause&.message}"
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
- def initialize
5
- raise NotImplementedError
6
- end
6
+ extend T::Sig
7
+ extend T::Helpers
7
8
 
8
- def scan
9
- raise NotImplementedError
10
- end
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
- def batch_write_item
13
- raise NotImplementedError
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
- require 'aws-sdk-dynamodb'
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
- def initialize(aws_region: 'us-west-1', **_additional_arguments)
11
- @dynamodb = Aws::DynamoDB::Client.new({ region: aws_region })
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
- def scan(table_name)
27
+ sig { override.params(table_name: String).returns(T::Enumerable[Item]) }
28
+ def fetch_items(table_name)
15
29
  begin
16
- result = dynamodb.scan({ table_name: table_name })
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&.items
35
+ result.items
22
36
  end
23
37
 
24
- def batch_write_item(table_name, items, additional_attributes = {})
25
- dynamodb.batch_write_item(
26
- { request_items: {
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: { **item.to_hash, **additional_attributes }
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
@@ -1,3 +1,5 @@
1
+ # typed: strict
2
+
1
3
  class Quarantine
2
4
  class Error < StandardError; end
3
5
 
@@ -1,86 +1,107 @@
1
- module RSpecAdapter
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
- private
3
+ class Quarantine
4
+ module RSpecAdapter
5
+ extend T::Sig
15
6
 
16
- # Purpose: binds rspec configuration variables
17
- def bind_rspec_configurations
18
- ::RSpec.configure do |config|
19
- config.add_setting(:quarantine_list_table, { default: 'quarantine_list' })
20
- config.add_setting(:quarantine_failed_tests_table, { default: 'master_failed_tests' })
21
- config.add_setting(:skip_quarantined_tests, { default: true })
22
- config.add_setting(:quarantine_record_failed_tests, { default: true })
23
- config.add_setting(:quarantine_record_flaky_tests, { default: true })
24
- config.add_setting(:quarantine_logging, { default: true })
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
- # Purpose: binds quarantine to fetch the quarantine_list from dynamodb in the before suite
29
- def bind_quarantine_list(quarantine)
30
- ::RSpec.configure do |config|
31
- config.before(:suite) do
32
- quarantine.fetch_quarantine_list
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
- # Purpose: binds quarantine to skip and pass tests that have been quarantined in the after suite
38
- def bind_quarantine_checker(quarantine)
39
- ::RSpec.configure do |config|
40
- config.after(:each) do |example|
41
- if RSpec.configuration.skip_quarantined_tests && quarantine.test_quarantined?(example)
42
- quarantine.pass_flaky_test(example)
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
- # Purpose: binds quarantine to record failed and flaky tests
49
- def bind_quarantine_record_tests(quarantine)
50
- ::RSpec.configure do |config|
51
- config.after(:each) do |example|
52
- metadata = example.metadata
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
- # 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
- # optionally, the upstream RSpec configuration could define an after hook that marks an example as flaky in
62
- # the example's metadata
63
- quarantine.record_flaky_test(example) if
64
- RSpec.configuration.quarantine_record_flaky_tests &&
65
- !quarantine.test_quarantined?(example) &&
66
- (metadata[:retry_attempts] > 0 && example.exception.nil?) || metadata[:flaky]
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
- ::RSpec.configure do |config|
71
- config.after(:suite) do
72
- quarantine.upload_tests(:failed) if RSpec.configuration.quarantine_record_failed_tests
73
-
74
- quarantine.upload_tests(:flaky) if RSpec.configuration.quarantine_record_flaky_tests
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
- # Purpose: binds quarantine logger to output test to RSpec formatter messages
80
- def bind_logger(quarantine)
81
- ::RSpec.configure do |config|
82
- config.after(:suite) do
83
- RSpec.configuration.reporter.message(quarantine.summary) if RSpec.configuration.quarantine_logging
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
@@ -1,23 +1,25 @@
1
+ # typed: strict
2
+
1
3
  class Quarantine
2
- class Test
3
- attr_accessor :id
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
- def initialize(id, full_description, location, build_number)
9
- @id = id
10
- @full_description = full_description
11
- @location = location
12
- @build_number = build_number
13
- end
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: id,
18
- full_description: full_description,
19
- location: location,
20
- build_number: build_number
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
@@ -1,3 +1,3 @@
1
1
  class Quarantine
2
- VERSION = '1.0.4'.freeze
2
+ VERSION = '2.1.0'.freeze
3
3
  end
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 Ruby Rspec tests'
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
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: 2020-03-19 00:00:00.000000000 Z
11
+ date: 2021-04-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: aws-sdk-dynamodb
14
+ name: rspec
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1'
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: '1'
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: '3.0'
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: '3.0'
40
+ version: '0.6'
41
41
  - !ruby/object:Gem::Dependency
42
- name: rspec-retry
42
+ name: sorbet-runtime
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - '='
46
46
  - !ruby/object:Gem::Version
47
- version: '0.6'
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: '0.6'
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.2
95
- signing_key:
95
+ rubygems_version: 3.0.8
96
+ signing_key:
96
97
  specification_version: 4
97
- summary: Quarantine flaky Ruby Rspec tests
98
+ summary: Quarantine flaky RSpec tests
98
99
  test_files: []