quarantine 1.0.7 → 2.1.3

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: 4b012ecdd8fb1b5f4b8d014d294bdce9e02370d46afd79bf1f69577c4821f9dc
4
- data.tar.gz: 6e9ac0c7edfb4d38db0487db389623b7ad75bdf342f74e97e471a6073d81357e
3
+ metadata.gz: 6ab8e4aa2ba3c2ab497cb3adfb08b6c14459eab525f26a00f4bc1fb9b27e1084
4
+ data.tar.gz: ffafe0bdf3508389ae614ab5b8946c4cb9b5303f7697ca4b378b6284f335375f
5
5
  SHA512:
6
- metadata.gz: e63db8d4dfc55809f9099fde29350ad86d88d6fb5617bfff576263bbd9ebb155f76d0f38c6e5dc9c7b465c969a8b21714ebacde8c7f8534c207aada2337d632b
7
- data.tar.gz: 06ee957f72bcdf07da8b3b67d0bb5cbf62aa970c81dd991eb2a28c962d69c232b7f06a37062cf288b74531099e4aa7f2d0935fa00e296961d095c814ede34faf
6
+ metadata.gz: c4d12718e41b13558f30add75b2a5d5e25f5d29d4e19b55704e7500c1980f7192ed1a8fa3e4fc83e45329486c94bae42bd56f99a718144f42c6884878b926faf
7
+ data.tar.gz: 283aa10d1dafe98d3e5ed08f1add97facdcfbe6f4a1761759e42677b7dd26fafbc323d958ab81965076a1d19e951e18c67e35e01679ad43e44d622876649641f
data/CHANGELOG.md CHANGED
@@ -1,21 +1,20 @@
1
1
  ### 1.0.7
2
- Support `it_behaves_like` behavior
2
+ Support `it_behaves_like` behavior
3
3
 
4
4
  ### 1.0.6
5
- Update DynamoDB batch_write_item implementation to check for duplicates based on different keys before uploading
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
- Add aws_credentials argument during dynamodb initialization to override the AWS SDK credential chain
8
+ Add aws_credentials argument during dynamodb initialization to override the AWS SDK credential chain
9
9
 
10
10
  ### 1.0.4
11
- 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
12
12
 
13
13
  ### 1.0.3
14
- Only require dynamodb instead of full aws-sdk
14
+ Only require dynamodb instead of full aws-sdk
15
15
 
16
16
  ### 1.0.2
17
- Relax Aws gem version constraint as aws-sdk v3 is 100% backwards
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
- Initial Release
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 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'
@@ -29,123 +20,144 @@ group :test do
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'}) # Also accepts aws_credentials to override the standard AWS credential chain
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
- - Skipping quarantined tests during test runs `: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 (in order of precedence):
144
- - The optional `aws_credentials` parameter passed into `Quarantine.bind`
153
+ The AWS client attempts to loads credentials from the following locations, in order:
154
+ - The optional `credentials` field in `RSpec.configuration.quarantine_database`
145
155
  - `ENV['AWS_ACCESS_KEY_ID']` and `ENV['AWS_SECRET_ACCESS_KEY']`
146
156
  - `Aws.config[:credentials]`
147
157
  - The shared credentials ini file at `~/.aws/credentials`
148
158
 
149
- To get AWS credentials, please contact your AWS administrator to get access to dynamodb and create your credentials through IAM.
159
+ More detailed information can be found in the [AWS SDK documentation](https://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Client.html)
160
+
161
+ #### Why is `example.clear_exception` failing locally?
150
162
 
151
- More detailed information can be found: [AWS documentation](https://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Client.html)
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,152 +1,168 @@
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'
6
11
 
7
12
  module RSpec
8
13
  module Core
9
14
  class Example
15
+ extend T::Sig
16
+
10
17
  # The implementation of clear_exception in rspec-retry doesn't work
11
18
  # for examples that use `it_behaves_like`, so we implement our own version that
12
19
  # clear the exception field recursively.
20
+ sig { void }
13
21
  def clear_exception!
14
- @exception = nil
15
- example.clear_exception! if defined?(example)
22
+ @exception = T.let(nil, T.untyped)
23
+ T.unsafe(self).example.clear_exception! if defined?(example)
16
24
  end
17
25
  end
18
26
  end
19
27
  end
20
28
 
21
29
  class Quarantine
22
- extend RSpecAdapter
23
-
24
- attr_accessor :database
25
- attr_reader :quarantine_map
26
- attr_reader :failed_tests
27
- attr_reader :flaky_tests
28
- attr_reader :duplicate_tests
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
30
+ extend T::Sig
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
40
37
 
41
- @quarantine_map = {}
42
- @failed_tests = []
43
- @flaky_tests = []
44
- @buildkite_build_number = ENV['BUILDKITE_BUILD_NUMBER'] || '-1'
45
- @summary = { id: 'quarantine', quarantined_tests: [], flaky_tests: [], database_failures: [] }
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))
46
45
  end
47
46
 
48
- # Scans the quarantine_list from the database and store the individual tests in quarantine_map
49
- def fetch_quarantine_list
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
65
+ end
66
+
67
+ # Scans the test_statuses from the database and store their IDs in quarantined_ids
68
+ sig { void }
69
+ def on_start
50
70
  begin
51
- quarantine_list = database.scan(RSpec.configuration.quarantine_list_table)
71
+ test_statuses = database.fetch_items(@options[:test_statuses_table_name])
52
72
  rescue Quarantine::DatabaseError => e
53
- add_to_summary(:database_failures, "#{e&.cause&.class}: #{e&.cause&.message}")
73
+ @database_failures << "#{e.cause&.class}: #{e.cause&.message}"
54
74
  raise Quarantine::DatabaseError.new(
55
75
  <<~ERROR_MSG
56
- Failed to pull the quarantine list from #{RSpec.configuration.quarantine_list_table}
57
- 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}
58
78
  ERROR_MSG
59
79
  )
60
80
  end
61
81
 
62
- quarantine_list.each do |example|
63
- # on the rare occassion there are duplicate tests ids in the quarantine_list,
64
- # quarantine the most recent instance of the test (det. through build_number)
65
- # and ignore the older instance of the test
66
- next if
67
- quarantine_map.key?(example['id']) &&
68
- example['build_number'].to_i < quarantine_map[example['id']].build_number.to_i
69
-
70
- quarantine_map.store(
71
- example['id'],
72
- Quarantine::Test.new(example['id'], example['full_description'], example['location'], example['build_number'])
73
- )
74
- 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]
75
103
  end
76
104
 
77
- # Based off the type, upload a list of tests to a particular database table
78
- def upload_tests(type)
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
105
+ sig { void }
106
+ def on_complete
107
+ quarantined_tests = @tests.values.select { |test| test.status == :quarantined }.sort_by(&:id)
108
+
109
+ if !@options[:record_tests]
110
+ log('Recording tests disabled; skipping')
111
+ elsif @tests.empty?
112
+ log('No tests found; skipping recording')
113
+ elsif quarantined_tests.count { |test| old_tests[test.id]&.status != :quarantined } >= @options[:failsafe_limit]
114
+ log('Number of quarantined tests above failsafe limit; skipping recording')
85
115
  else
86
- raise Quarantine::UnknownUploadError.new(
87
- "Quarantine gem did not know how to handle #{type} upload of tests to dynamodb"
88
- )
116
+ begin
117
+ timestamp = Time.now.to_i / 1000 # Truncated millisecond from timestamp for reasons specific to Flexport
118
+ database.write_items(
119
+ @options[:test_statuses_table_name],
120
+ @tests.values.map { |item| item.to_hash.merge('updated_at' => timestamp) }
121
+ )
122
+ rescue Quarantine::DatabaseError => e
123
+ @database_failures << "#{e.cause&.class}: #{e.cause&.message}"
124
+ end
89
125
  end
90
126
 
91
- return unless tests.length < 10 && tests.length > 0
92
-
93
- begin
94
- timestamp = Time.now.to_i / 1000 # Truncated millisecond from timestamp for reasons specific to Flexport
95
- database.batch_write_item(
96
- table_name,
97
- tests,
98
- {
99
- build_job_id: ENV['BUILDKITE_JOB_ID'] || '-1',
100
- created_at: timestamp,
101
- updated_at: timestamp
102
- }
103
- )
104
- rescue Quarantine::DatabaseError => e
105
- add_to_summary(:database_failures, "#{e&.cause&.class}: #{e&.cause&.message}")
106
- end
107
- end
127
+ log(<<~MESSAGE)
128
+ \n[quarantine] Quarantined tests:
129
+ #{quarantined_tests.map { |test| "#{test.id} #{test.full_description}" }.join("\n ")}
108
130
 
109
- # Param: RSpec::Core::Example
110
- # Add the example to the internal failed tests list
111
- def record_failed_test(example)
112
- failed_tests << Quarantine::Test.new(
113
- example.id,
114
- example.full_description,
115
- example.location,
116
- buildkite_build_number
117
- )
131
+ [quarantine] Database errors:
132
+ #{@database_failures.join("\n ")}
133
+ MESSAGE
118
134
  end
119
135
 
120
136
  # Param: RSpec::Core::Example
121
- # Add the example to the internal flaky tests list
122
- def record_flaky_test(example)
123
- flaky_test = Quarantine::Test.new(
124
- example.id,
125
- example.full_description,
126
- example.location,
127
- buildkite_build_number
137
+ # Add the example to the internal tests list
138
+ sig { params(example: T.untyped, status: Symbol, passed: T::Boolean).void }
139
+ def on_test(example, status, passed:)
140
+ extra_attributes = @options[:extra_attributes] ? @options[:extra_attributes].call(example) : {}
141
+
142
+ new_consecutive_passes = passed ? (@old_tests[example.id]&.consecutive_passes || 0) + 1 : 0
143
+ release_at = @options[:release_at_consecutive_passes]
144
+ new_status = !release_at.nil? && new_consecutive_passes >= release_at ? :passing : status
145
+ test = Quarantine::Test.new(
146
+ id: example.id,
147
+ status: new_status,
148
+ consecutive_passes: new_consecutive_passes,
149
+ full_description: example.full_description,
150
+ location: example.location,
151
+ extra_attributes: extra_attributes
128
152
  )
129
153
 
130
- flaky_tests << flaky_test
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)
154
+ @tests[test.id] = test
139
155
  end
140
156
 
141
157
  # Param: RSpec::Core::Example
142
- # Check the internal quarantine_map to see if this test should be quarantined
158
+ # Check the internal old_tests to see if this test should be quarantined
159
+ sig { params(example: T.untyped).returns(T::Boolean) }
143
160
  def test_quarantined?(example)
144
- quarantine_map.key?(example.id)
161
+ @old_tests[example.id]&.status == :quarantined
145
162
  end
146
163
 
147
- # Param: Symbol, Any
148
- # Adds the item to the specified attribute in summary
149
- def add_to_summary(attribute, item)
150
- summary[attribute] << item if summary.key?(attribute)
164
+ sig { params(message: String).void }
165
+ def log(message)
166
+ @options[:log].call(message) if @options[:logging]
151
167
  end
152
168
  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,51 +1,53 @@
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', aws_credentials: nil, **_additional_arguments)
11
- options = { region: aws_region }
12
- options[:credentials] = aws_credentials if aws_credentials
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
- def scan(table_name)
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&.items
35
+ result.items
25
36
  end
26
37
 
27
- def batch_write_item(table_name, items, additional_attributes = {}, dedup_keys = %w[id full_description])
28
- return if items.empty?
29
-
30
- # item_a is a duplicate of item_b if all values for each dedup_key in both item_a and item_b match
31
- is_a_duplicate = ->(item_a, item_b) { dedup_keys.all? { |key| item_a[key] == item_b[key] } }
32
-
33
- scanned_items = scan(table_name)
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 => deduped_items.map do |item|
47
+ table_name => items.map do |item|
46
48
  {
47
49
  put_request: {
48
- item: { **item.to_hash, **additional_attributes }
50
+ item: item
49
51
  }
50
52
  }
51
53
  end
@@ -55,19 +57,15 @@ class Quarantine
55
57
  raise Quarantine::DatabaseError
56
58
  end
57
59
 
58
- def delete_item(table_name, keys)
59
- dynamodb.delete_item(
60
- table_name: table_name,
61
- key: {
62
- **keys
63
- }
64
- )
65
- rescue Aws::DynamoDB::Errors::ServiceError
66
- raise Quarantine::DatabaseError
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
67
66
  end
68
-
69
67
  def create_table(table_name, attributes, additional_arguments = {})
70
- dynamodb.create_table(
68
+ @dynamodb.create_table(
71
69
  {
72
70
  table_name: table_name,
73
71
  attribute_definitions: attributes.map do |attribute|
@@ -0,0 +1,114 @@
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
+ rescue GoogleDrive::Error, Google::Apis::Error
25
+ raise Quarantine::DatabaseError
26
+ end
27
+
28
+ sig do
29
+ override.params(
30
+ table_name: String,
31
+ items: T::Array[Item]
32
+ ).void
33
+ end
34
+ def write_items(table_name, items)
35
+ worksheet = spreadsheet.worksheet_by_title(table_name)
36
+ headers = worksheet.rows.first.reject(&:empty?)
37
+ new_rows = []
38
+
39
+ # Map existing ID to row index
40
+ parsed_rows = parse_rows(worksheet)
41
+ indexes = Hash[parsed_rows.each_with_index.map { |item, idx| [item['id'], idx] }]
42
+
43
+ items.each do |item|
44
+ cells = headers.map { |header| item[header].to_s }
45
+ row_idx = indexes[item['id']]
46
+ if row_idx
47
+ # Overwrite existing row
48
+ headers.each_with_index do |_header, col_idx|
49
+ worksheet[row_idx + 2, col_idx + 1] = cells[col_idx]
50
+ end
51
+ else
52
+ new_rows << cells
53
+ end
54
+ end
55
+
56
+ # Insert any items whose IDs weren't found in existing rows at the end
57
+ worksheet.insert_rows(parsed_rows.count + 2, new_rows)
58
+ worksheet.save
59
+ rescue GoogleDrive::Error, Google::Apis::Error
60
+ raise Quarantine::DatabaseError
61
+ end
62
+
63
+ private
64
+
65
+ sig { returns(GoogleDrive::Session) }
66
+ def session
67
+ @session = T.let(@session, T.nilable(GoogleDrive::Session))
68
+ @session ||= begin
69
+ authorization = @options[:authorization]
70
+ case authorization[:type]
71
+ when :service_account_key
72
+ GoogleDrive::Session.from_service_account_key(authorization[:file])
73
+ when :config
74
+ GoogleDrive::Session.from_config(authorization[:file])
75
+ else
76
+ raise "Invalid authorization type: #{authorization[:type]}"
77
+ end
78
+ end
79
+ end
80
+
81
+ sig { returns(GoogleDrive::Spreadsheet) }
82
+ def spreadsheet
83
+ @spreadsheet = T.let(@spreadsheet, T.nilable(GoogleDrive::Spreadsheet))
84
+ @spreadsheet ||= begin
85
+ spreadsheet = @options[:spreadsheet]
86
+ case spreadsheet[:type]
87
+ when :by_key
88
+ session.spreadsheet_by_key(spreadsheet[:key])
89
+ when :by_title
90
+ session.spreadsheet_by_title(spreadsheet[:title])
91
+ when :by_url
92
+ session.spreadsheet_by_url(spreadsheet[:url])
93
+ else
94
+ raise "Invalid spreadsheet type: #{spreadsheet[:type]}"
95
+ end
96
+ end
97
+ end
98
+
99
+ sig { params(worksheet: GoogleDrive::Worksheet).returns(T::Enumerable[Item]) }
100
+ def parse_rows(worksheet)
101
+ headers, *rows = worksheet.rows
102
+
103
+ rows.map do |row|
104
+ hash_row = Hash[headers.zip(row)]
105
+ # TODO: use Google Sheets developer metadata to store type information
106
+ unless hash_row['id'].empty?
107
+ hash_row['extra_attributes'] = JSON.parse(hash_row['extra_attributes']) if hash_row['extra_attributes']
108
+ hash_row
109
+ end
110
+ end.compact
111
+ end
112
+ end
113
+ end
114
+ end
@@ -1,3 +1,5 @@
1
+ # typed: strict
2
+
1
3
  class Quarantine
2
4
  class Error < StandardError; end
3
5
 
@@ -1,87 +1,101 @@
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
+ sig { void }
8
+ def self.bind
9
+ register_rspec_configurations
10
+ bind_on_start
11
+ bind_on_test
12
+ bind_on_complete
13
+ end
14
+
15
+ sig { returns(Quarantine) }
16
+ def self.quarantine
17
+ @quarantine = T.let(@quarantine, T.nilable(Quarantine))
18
+ @quarantine ||= Quarantine.new(
19
+ database: RSpec.configuration.quarantine_database,
20
+ test_statuses_table_name: RSpec.configuration.quarantine_test_statuses,
21
+ extra_attributes: RSpec.configuration.quarantine_extra_attributes,
22
+ failsafe_limit: RSpec.configuration.quarantine_failsafe_limit,
23
+ release_at_consecutive_passes: RSpec.configuration.quarantine_release_at_consecutive_passes,
24
+ logging: RSpec.configuration.quarantine_logging,
25
+ log: method(:log),
26
+ record_tests: RSpec.configuration.quarantine_record_tests
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.register_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_on_start
48
+ ::RSpec.configure do |config|
49
+ config.before(:suite) do
50
+ Quarantine::RSpecAdapter.quarantine.on_start
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_on_test
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.on_test(example, :quarantined, passed: false)
71
+ else
72
+ Quarantine::RSpecAdapter.quarantine.on_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.on_test(example, :quarantined, passed: false)
78
+ elsif quarantined
79
+ Quarantine::RSpecAdapter.quarantine.on_test(example, :quarantined, passed: true)
80
+ else
81
+ Quarantine::RSpecAdapter.quarantine.on_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_on_complete
89
+ ::RSpec.configure do |config|
90
+ config.after(:suite) do
91
+ Quarantine::RSpecAdapter.quarantine.on_complete
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
84
- end
96
+ sig { params(message: String).void }
97
+ def self.log(message)
98
+ RSpec.configuration.reporter.message(message)
85
99
  end
86
100
  end
87
101
  end
@@ -1,32 +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
- {
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
- 'build_number' => build_number
22
+ 'extra_attributes' => extra_attributes
30
23
  }
31
24
  end
32
25
  end
@@ -1,3 +1,3 @@
1
1
  class Quarantine
2
- VERSION = '1.0.7'.freeze
2
+ VERSION = '2.1.3'.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,57 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quarantine
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.7
4
+ version: 2.1.3
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: 2020-12-01 00:00:00.000000000 Z
11
+ date: 2021-04-15 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'
54
+ version: 0.5.6338
55
55
  description:
56
56
  email:
57
57
  - ericzhu77@gmail.com
@@ -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
@@ -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
+ rubygems_version: 3.0.8
95
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: []