quarantine 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +154 -0
- data/bin/quarantine_dynamodb +10 -0
- data/lib/quarantine/cli.rb +85 -0
- data/lib/quarantine/databases/base.rb +17 -0
- data/lib/quarantine/databases/dynamo_db.rb +77 -0
- data/lib/quarantine/error.rb +13 -0
- data/lib/quarantine/rspec_adapter.rb +85 -0
- data/lib/quarantine/test.rb +24 -0
- data/lib/quarantine/version.rb +3 -0
- data/lib/quarantine.rb +142 -0
- data/quarantine.gemspec +23 -0
- metadata +97 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 85d2c80359510de3dfd3d4c84cea8c04efed552bc11b44eed887f87caf7cd556
|
4
|
+
data.tar.gz: 2b075d8cde72b8451c9decbfacc24f0f18022fe446abf13c1a835cc9abd4a39b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 57babcb9ddb17b96c48d3800f9af910998efd2a52cc68ad0b982edf7d0e84fa0f43f2918db039e59bf3ef855861602b662dd0e5beb3d9073e5f5e5247ba18c6a
|
7
|
+
data.tar.gz: 0ac7fa45415f46529fee23088a41746e80fd096cb5aea55d181b9147add6b70d67fe5b48407e1508390a69c310a878f96887149b7767170b66a5d97e4efa967d
|
data/README.md
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
# Quarantine
|
2
|
+
[![Build Status](https://travis-ci.com/flexport/quarantine.svg?branch=master)](https://travis-ci.com/flexport/quarantine)
|
3
|
+
|
4
|
+
Quarantine provides a run-time solution to diagnosing and disabling flaky tests and automates the workflow around test suite maintenance.
|
5
|
+
|
6
|
+
The quarantine gem supports testing frameworks:
|
7
|
+
- [RSpec](http://rspec.info/)
|
8
|
+
|
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:
|
18
|
+
|
19
|
+
![ideal workflow](misc/flexport_workflow.png)
|
20
|
+
|
21
|
+
---
|
22
|
+
## Installation and Setup
|
23
|
+
|
24
|
+
Add these lines to your application's Gemfile:
|
25
|
+
```
|
26
|
+
group :test do
|
27
|
+
gem 'quarantine'
|
28
|
+
gem 'rspec-retry
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
And then execute:
|
33
|
+
```
|
34
|
+
bundle install
|
35
|
+
```
|
36
|
+
|
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
|
+
```
|
39
|
+
require 'quarantine'
|
40
|
+
require 'rspec-retry'
|
41
|
+
|
42
|
+
Quarantine.bind({database: :dynamodb, aws_region: 'us-west-1'})
|
43
|
+
|
44
|
+
RSpec.configure do |config|
|
45
|
+
|
46
|
+
config.around(:each) do |ex|
|
47
|
+
ex.run_with_retry(retry: 3)
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
Consider wrapping `Quarantine.bind` in if statements so local flaky tests don't pollute the list of quarantined tests
|
54
|
+
|
55
|
+
```
|
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
|
+
```
|
63
|
+
bundle exec quarantine_dynamodb -h # see all options
|
64
|
+
|
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
|
68
|
+
```
|
69
|
+
|
70
|
+
You are all set to start quarantining tests!
|
71
|
+
|
72
|
+
## Try Quarantining Tests Locally
|
73
|
+
Add a test that will flake
|
74
|
+
```
|
75
|
+
require "spec_helper"
|
76
|
+
|
77
|
+
describe Quarantine do
|
78
|
+
it "this test should flake 33% of the time" do
|
79
|
+
Random.rand(3).to eq(3)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
Run `rspec` on the test
|
85
|
+
```
|
86
|
+
CI=1 BRANCH=master rspec <filename>
|
87
|
+
```
|
88
|
+
|
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.
|
90
|
+
|
91
|
+
## Configuration
|
92
|
+
|
93
|
+
Go to `spec/spec_helper.rb` and set configuration variables through:
|
94
|
+
```
|
95
|
+
RSpec.configure do |config|
|
96
|
+
config.VAR_NAME = VALUE
|
97
|
+
end
|
98
|
+
```
|
99
|
+
- Table name for quarantined tests `:quarantine_list_table, default: "quarantine_list"`
|
100
|
+
|
101
|
+
- Table name where failed test are uploaded `:quarantine_failed_tests_table, default: "master_failed_tests"`
|
102
|
+
|
103
|
+
- Quarantined tests are not skipped automatically `:skip_quarantined_tests, default: true`
|
104
|
+
|
105
|
+
- Recording failed tests `:quarantine_record_failed_tests, default: true`
|
106
|
+
|
107
|
+
- Recording flaky tests `:quarantine_record_flaky_tests, default: true`
|
108
|
+
|
109
|
+
- Outputting quarantined gem info `:quarantine_logging, default: true`
|
110
|
+
|
111
|
+
---
|
112
|
+
## Setup Jira Workflow
|
113
|
+
|
114
|
+
To automatically create Jira tickets, take a look at: `examples/create_tickets.rb`
|
115
|
+
|
116
|
+
To automatically unquarantine tests on Jira ticket completion, take a look at: `examples/unquarantine.rb`
|
117
|
+
|
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
|
+
---
|
130
|
+
|
131
|
+
## FAQs
|
132
|
+
|
133
|
+
#### Why are quarantined tests not being skipped locally?
|
134
|
+
|
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
|
136
|
+
|
137
|
+
```
|
138
|
+
CI="1" BRANCH="master" rspec
|
139
|
+
```
|
140
|
+
|
141
|
+
#### Why is dynamodb failing to connect?
|
142
|
+
|
143
|
+
The AWS client loads credentials from the following locations:
|
144
|
+
- `ENV['AWS_ACCESS_KEY_ID']` and `ENV['AWS_SECRET_ACCESS_KEY']`
|
145
|
+
- `Aws.config[:credentials]`
|
146
|
+
- The shared credentials ini file at `~/.aws/credentials`
|
147
|
+
|
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)
|
151
|
+
|
152
|
+
#### 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.
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require_relative 'databases/base'
|
3
|
+
require_relative 'databases/dynamo_db'
|
4
|
+
|
5
|
+
class Quarantine
|
6
|
+
class CLI
|
7
|
+
attr_accessor :options
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
# default options
|
11
|
+
@options = {
|
12
|
+
quarantine_list_table_name: 'quarantine_list',
|
13
|
+
failed_test_table_name: 'master_failed_tests'
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse
|
18
|
+
OptionParser.new do |parser|
|
19
|
+
parser.banner = 'Usage: quarantine_dynamodb [options]'
|
20
|
+
|
21
|
+
parser.on('-rREGION', '--aws_region=REGION', String, 'Specify the aws region for DynamoDB') do |aws_region|
|
22
|
+
options[:aws_region] = aws_region
|
23
|
+
end
|
24
|
+
|
25
|
+
parser.on(
|
26
|
+
'-qTABLE',
|
27
|
+
'--quarantine_table=TABLE',
|
28
|
+
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]}"
|
39
|
+
) do |table_name|
|
40
|
+
options[:failed_test_table_name] = table_name
|
41
|
+
end
|
42
|
+
|
43
|
+
parser.on('-h', '--help', 'Prints help page') do
|
44
|
+
puts parser # rubocop:disable Rails/Output
|
45
|
+
exit
|
46
|
+
end
|
47
|
+
end.parse!
|
48
|
+
|
49
|
+
if options[:aws_region].nil?
|
50
|
+
error_msg = 'Failed to specify the required aws region with -r option'.freeze
|
51
|
+
warn error_msg
|
52
|
+
raise ArgumentError.new(error_msg)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# TODO: eventually move to a separate file & create_table by db type when my db adapters
|
57
|
+
def create_tables
|
58
|
+
dynamodb = Quarantine::Databases::DynamoDB.new(options)
|
59
|
+
|
60
|
+
attributes = [
|
61
|
+
{ attribute_name: 'id', attribute_type: 'S', key_type: 'HASH' },
|
62
|
+
{ attribute_name: 'build_number', attribute_type: 'S', key_type: 'RANGE' }
|
63
|
+
]
|
64
|
+
|
65
|
+
additional_arguments = {
|
66
|
+
provisioned_throughput: {
|
67
|
+
read_capacity_units: 10,
|
68
|
+
write_capacity_units: 5
|
69
|
+
}
|
70
|
+
}
|
71
|
+
|
72
|
+
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)
|
80
|
+
rescue Quarantine::DatabaseError => e
|
81
|
+
warn "#{e&.cause&.class}: #{e&.cause&.message}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
require 'quarantine/databases/base'
|
3
|
+
require 'quarantine/error'
|
4
|
+
|
5
|
+
class Quarantine
|
6
|
+
module Databases
|
7
|
+
class DynamoDB < Base
|
8
|
+
attr_accessor :dynamodb
|
9
|
+
|
10
|
+
def initialize(aws_region: 'us-west-1', **_additional_arguments)
|
11
|
+
@dynamodb = Aws::DynamoDB::Client.new({ region: aws_region })
|
12
|
+
end
|
13
|
+
|
14
|
+
def scan(table_name)
|
15
|
+
begin
|
16
|
+
result = dynamodb.scan({ table_name: table_name })
|
17
|
+
rescue Aws::DynamoDB::Errors::ServiceError
|
18
|
+
raise Quarantine::DatabaseError
|
19
|
+
end
|
20
|
+
|
21
|
+
result&.items
|
22
|
+
end
|
23
|
+
|
24
|
+
def batch_write_item(table_name, items, additional_attributes = {})
|
25
|
+
dynamodb.batch_write_item(
|
26
|
+
{ request_items: {
|
27
|
+
table_name => items.map do |item|
|
28
|
+
{
|
29
|
+
put_request: {
|
30
|
+
item: { **item.to_hash, **additional_attributes }
|
31
|
+
}
|
32
|
+
}
|
33
|
+
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
|
+
}
|
48
|
+
)
|
49
|
+
rescue Aws::DynamoDB::Errors::ServiceError
|
50
|
+
raise Quarantine::DatabaseError
|
51
|
+
end
|
52
|
+
|
53
|
+
def create_table(table_name, attributes, additional_arguments = {})
|
54
|
+
dynamodb.create_table(
|
55
|
+
{
|
56
|
+
table_name: table_name,
|
57
|
+
attribute_definitions: attributes.map do |attribute|
|
58
|
+
{
|
59
|
+
attribute_name: attribute[:attribute_name],
|
60
|
+
attribute_type: attribute[:attribute_type]
|
61
|
+
}
|
62
|
+
end,
|
63
|
+
key_schema: attributes.map do |attribute|
|
64
|
+
{
|
65
|
+
attribute_name: attribute[:attribute_name],
|
66
|
+
key_type: attribute[:key_type]
|
67
|
+
}
|
68
|
+
end,
|
69
|
+
**additional_arguments
|
70
|
+
}
|
71
|
+
)
|
72
|
+
rescue Aws::DynamoDB::Errors::ServiceError
|
73
|
+
raise Quarantine::DatabaseError
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class Quarantine
|
2
|
+
class Error < StandardError; end
|
3
|
+
|
4
|
+
# Raised when a database error has occured
|
5
|
+
# TODO(ezhu): expand error messages to cover more specific error messages
|
6
|
+
class DatabaseError < Error; end
|
7
|
+
|
8
|
+
# Raised when quarantine does not know how to upload a specific test
|
9
|
+
class UnknownUploadError < Error; end
|
10
|
+
|
11
|
+
# Quarantine does not work with the specificed database
|
12
|
+
class UnsupportedDatabaseError < Error; end
|
13
|
+
end
|
@@ -0,0 +1,85 @@
|
|
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
|
13
|
+
|
14
|
+
private
|
15
|
+
|
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 })
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
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
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
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)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Purpose: binds quarantine to record failed and flaky tests
|
49
|
+
def bind_quarantine_record_tests(quarantine)
|
50
|
+
::RSpec.configure do |config|
|
51
|
+
config.after(:each) do |example|
|
52
|
+
metadata = example.metadata
|
53
|
+
|
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
|
+
quarantine.record_flaky_test(example) if
|
62
|
+
RSpec.configuration.quarantine_record_flaky_tests &&
|
63
|
+
!quarantine.test_quarantined?(example) &&
|
64
|
+
metadata[:retry_attempts] > 0 && example.exception.nil?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
::RSpec.configure do |config|
|
69
|
+
config.after(:suite) do
|
70
|
+
quarantine.upload_tests(:failed) if RSpec.configuration.quarantine_record_failed_tests
|
71
|
+
|
72
|
+
quarantine.upload_tests(:flaky) if RSpec.configuration.quarantine_record_flaky_tests
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Purpose: binds quarantine logger to output test to RSpec formatter messages
|
78
|
+
def bind_logger(quarantine)
|
79
|
+
::RSpec.configure do |config|
|
80
|
+
config.after(:suite) do
|
81
|
+
RSpec.configuration.reporter.message(quarantine.summary) if RSpec.configuration.quarantine_logging
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class Quarantine
|
2
|
+
class Test
|
3
|
+
attr_accessor :id
|
4
|
+
attr_accessor :full_description
|
5
|
+
attr_accessor :location
|
6
|
+
attr_accessor :build_number
|
7
|
+
|
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
|
14
|
+
|
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
|
+
end
|
24
|
+
end
|
data/lib/quarantine.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
require 'rspec/retry'
|
3
|
+
require 'quarantine/rspec_adapter'
|
4
|
+
require 'quarantine/test'
|
5
|
+
require 'quarantine/databases/base'
|
6
|
+
require 'quarantine/databases/dynamo_db'
|
7
|
+
|
8
|
+
class Quarantine
|
9
|
+
extend RSpecAdapter
|
10
|
+
|
11
|
+
attr_accessor :database
|
12
|
+
attr_reader :quarantine_map
|
13
|
+
attr_reader :failed_tests
|
14
|
+
attr_reader :flaky_tests
|
15
|
+
attr_reader :duplicate_tests
|
16
|
+
attr_reader :buildkite_build_number
|
17
|
+
attr_reader :summary
|
18
|
+
|
19
|
+
def initialize(options = {})
|
20
|
+
case options[:database]
|
21
|
+
# default database option is dynamodb
|
22
|
+
when :dynamodb, nil
|
23
|
+
@database = Quarantine::Databases::DynamoDB.new(options)
|
24
|
+
else
|
25
|
+
raise Quarantine::UnsupportedDatabaseError.new("Quarantine does not support #{options[:database]}")
|
26
|
+
end
|
27
|
+
|
28
|
+
@quarantine_map = {}
|
29
|
+
@failed_tests = []
|
30
|
+
@flaky_tests = []
|
31
|
+
@buildkite_build_number = ENV['BUILDKITE_BUILD_NUMBER'] || '-1'
|
32
|
+
@summary = { id: 'quarantine', quarantined_tests: [], flaky_tests: [], database_failures: [] }
|
33
|
+
end
|
34
|
+
|
35
|
+
# Scans the quarantine_list from the database and store the individual tests in quarantine_map
|
36
|
+
def fetch_quarantine_list
|
37
|
+
begin
|
38
|
+
quarantine_list = database.scan(RSpec.configuration.quarantine_list_table)
|
39
|
+
rescue Quarantine::DatabaseError => e
|
40
|
+
add_to_summary(:database_failures, "#{e&.cause&.class}: #{e&.cause&.message}")
|
41
|
+
raise Quarantine::DatabaseError.new(
|
42
|
+
<<~ERROR_MSG
|
43
|
+
Failed to pull the quarantine list from #{RSpec.configuration.quarantine_list_table}
|
44
|
+
because of #{e&.cause&.class}: #{e&.cause&.message}
|
45
|
+
ERROR_MSG
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
quarantine_list.each do |example|
|
50
|
+
# on the rare occassion there are duplicate tests ids in the quarantine_list,
|
51
|
+
# quarantine the most recent instance of the test (det. through build_number)
|
52
|
+
# and ignore the older instance of the test
|
53
|
+
next if
|
54
|
+
quarantine_map.key?(example['id']) &&
|
55
|
+
example['build_number'].to_i < quarantine_map[example['id']].build_number.to_i
|
56
|
+
|
57
|
+
quarantine_map.store(
|
58
|
+
example['id'],
|
59
|
+
Quarantine::Test.new(example['id'], example['full_description'], example['location'], example['build_number'])
|
60
|
+
)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Based off the type, upload a list of tests to a particular database table
|
65
|
+
def upload_tests(type)
|
66
|
+
if type == :failed
|
67
|
+
tests = failed_tests
|
68
|
+
table_name = RSpec.configuration.quarantine_failed_tests_table
|
69
|
+
elsif type == :flaky
|
70
|
+
tests = flaky_tests
|
71
|
+
table_name = RSpec.configuration.quarantine_list_table
|
72
|
+
else
|
73
|
+
raise Quarantine::UnknownUploadError.new(
|
74
|
+
"Quarantine gem did not know how to handle #{type} upload of tests to dynamodb"
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
return unless tests.length < 10 && tests.length > 0
|
79
|
+
|
80
|
+
begin
|
81
|
+
timestamp = Time.now.to_i / 1000 # Truncated millisecond from timestamp for reasons specific to Flexport
|
82
|
+
database.batch_write_item(
|
83
|
+
table_name,
|
84
|
+
tests,
|
85
|
+
{
|
86
|
+
build_job_id: ENV['BUILDKITE_JOB_ID'] || '-1',
|
87
|
+
created_at: timestamp,
|
88
|
+
updated_at: timestamp
|
89
|
+
}
|
90
|
+
)
|
91
|
+
rescue Quarantine::DatabaseError => e
|
92
|
+
add_to_summary(:database_failures, "#{e&.cause&.class}: #{e&.cause&.message}")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Param: RSpec::Core::Example
|
97
|
+
# Add the example to the internal failed tests list
|
98
|
+
def record_failed_test(example)
|
99
|
+
failed_tests << Quarantine::Test.new(
|
100
|
+
example.id,
|
101
|
+
example.full_description,
|
102
|
+
example.location,
|
103
|
+
buildkite_build_number
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Param: RSpec::Core::Example
|
108
|
+
# Add the example to the internal flaky tests list
|
109
|
+
def record_flaky_test(example)
|
110
|
+
flaky_test = Quarantine::Test.new(
|
111
|
+
example.id,
|
112
|
+
example.full_description,
|
113
|
+
example.location,
|
114
|
+
buildkite_build_number
|
115
|
+
)
|
116
|
+
|
117
|
+
flaky_tests << flaky_test
|
118
|
+
add_to_summary(:flaky_tests, flaky_test.id)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Param: RSpec::Core::Example
|
122
|
+
# Clear exceptions on a flaky tests that has been quarantined
|
123
|
+
#
|
124
|
+
# example.clear_exception is tightly coupled with the rspec-retry gem and will only exist if
|
125
|
+
# the rspec-retry gem is enabled
|
126
|
+
def pass_flaky_test(example)
|
127
|
+
example.clear_exception
|
128
|
+
add_to_summary(:quarantined_tests, example.id)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Param: RSpec::Core::Example
|
132
|
+
# Check the internal quarantine_map to see if this test should be quarantined
|
133
|
+
def test_quarantined?(example)
|
134
|
+
quarantine_map.key?(example.id)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Param: Symbol, Any
|
138
|
+
# Adds the item to the specified attribute in summary
|
139
|
+
def add_to_summary(attribute, item)
|
140
|
+
summary[attribute] << item if summary.key?(attribute)
|
141
|
+
end
|
142
|
+
end
|
data/quarantine.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# TEAM: backend_infra
|
2
|
+
#
|
3
|
+
$LOAD_PATH.push(File.expand_path('lib', __dir__))
|
4
|
+
|
5
|
+
# Maintain your gem's version:
|
6
|
+
require 'quarantine/version'
|
7
|
+
|
8
|
+
# Describe your gem and declare its dependencies:
|
9
|
+
Gem::Specification.new do |s|
|
10
|
+
s.name = 'quarantine'
|
11
|
+
s.version = Quarantine::VERSION
|
12
|
+
s.authors = ['Flexport Engineering, Eric Zhu']
|
13
|
+
s.email = ['ericzhu77@gmail.com']
|
14
|
+
s.summary = 'Quarantine flaky Ruby Rspec tests'
|
15
|
+
s.homepage = 'https://github.com/flexport/quarantine'
|
16
|
+
s.license = 'MIT'
|
17
|
+
s.files = Dir['{lib, bin}/**/*', '*.md', '*.gemspec']
|
18
|
+
s.executables = ['quarantine_dynamodb']
|
19
|
+
|
20
|
+
s.add_dependency('aws-sdk', '~> 2.11.41')
|
21
|
+
s.add_dependency('rspec', '>= 3.0')
|
22
|
+
s.add_dependency('rspec-retry', '>= 0.6.1')
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: quarantine
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.7
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Flexport Engineering, Eric Zhu
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-04-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: aws-sdk
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.11.41
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.11.41
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec-retry
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.6.1
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.6.1
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- ericzhu77@gmail.com
|
58
|
+
executables:
|
59
|
+
- quarantine_dynamodb
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- README.md
|
64
|
+
- bin/quarantine_dynamodb
|
65
|
+
- lib/quarantine.rb
|
66
|
+
- lib/quarantine/cli.rb
|
67
|
+
- lib/quarantine/databases/base.rb
|
68
|
+
- lib/quarantine/databases/dynamo_db.rb
|
69
|
+
- lib/quarantine/error.rb
|
70
|
+
- lib/quarantine/rspec_adapter.rb
|
71
|
+
- lib/quarantine/test.rb
|
72
|
+
- lib/quarantine/version.rb
|
73
|
+
- quarantine.gemspec
|
74
|
+
homepage: https://github.com/flexport/quarantine
|
75
|
+
licenses:
|
76
|
+
- MIT
|
77
|
+
metadata: {}
|
78
|
+
post_install_message:
|
79
|
+
rdoc_options: []
|
80
|
+
require_paths:
|
81
|
+
- lib
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
requirements: []
|
93
|
+
rubygems_version: 3.0.2
|
94
|
+
signing_key:
|
95
|
+
specification_version: 4
|
96
|
+
summary: Quarantine flaky Ruby Rspec tests
|
97
|
+
test_files: []
|