quarantine 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +11 -0
- data/lib/quarantine.rb +4 -1
- data/lib/quarantine/databases/base.rb +1 -10
- data/lib/quarantine/databases/dynamo_db.rb +0 -12
- data/lib/quarantine/databases/google_sheets.rb +107 -0
- data/lib/quarantine/rspec_adapter.rb +83 -81
- data/lib/quarantine/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ffb721a4514eda413ea4c096913b5d0c3a672b23fe6aefee44393062f202e453
|
4
|
+
data.tar.gz: eec03b0ec1533643ae86245e76a69b51fdbf871e6e621293fa542b1b252fc123
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 67f36af412a0f1b23aaa018402e81b087f9595d3257726aac3b4e6472f232a498be52d683fd6b0216f652ad3f2b702d6a5f582de70a62a8fb25fc29ec5a21df1
|
7
|
+
data.tar.gz: 3aabf31b49b00cded2af25bac1b8d9f3c1c1bb89a202689a87ded5e23369fb6e4d32b82f664d3c4d3b1f314cae02f34dc80ca0a0380e6c034466e7ae8ec15300
|
data/README.md
CHANGED
@@ -97,9 +97,20 @@ end
|
|
97
97
|
|
98
98
|
Quarantine comes with built-in support for the following database types:
|
99
99
|
- `:dynamodb`
|
100
|
+
- `:google_sheets`
|
100
101
|
|
101
102
|
To use `:dynamodb`, be sure to add `gem 'aws-sdk-dynamodb', '~> 1', group: :test` to your `Gemfile`.
|
102
103
|
|
104
|
+
To use `:google_sheets`, be sure to add `gem 'google_drive', '~> 2', group: :test` to your `Gemfile`. Here's an example:
|
105
|
+
|
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
|
+
|
103
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:
|
104
115
|
|
105
116
|
```rb
|
data/lib/quarantine.rb
CHANGED
@@ -7,6 +7,7 @@ require 'quarantine/rspec_adapter'
|
|
7
7
|
require 'quarantine/test'
|
8
8
|
require 'quarantine/databases/base'
|
9
9
|
require 'quarantine/databases/dynamo_db'
|
10
|
+
require 'quarantine/databases/google_sheets'
|
10
11
|
|
11
12
|
module RSpec
|
12
13
|
module Core
|
@@ -55,6 +56,8 @@ class Quarantine
|
|
55
56
|
case type
|
56
57
|
when :dynamodb
|
57
58
|
Quarantine::Databases::DynamoDB.new(database_options)
|
59
|
+
when :google_sheets
|
60
|
+
Quarantine::Databases::GoogleSheets.new(database_options)
|
58
61
|
else
|
59
62
|
raise Quarantine::UnsupportedDatabaseError.new("Quarantine does not support database type: #{type.inspect}")
|
60
63
|
end
|
@@ -107,7 +110,7 @@ class Quarantine
|
|
107
110
|
timestamp = Time.now.to_i / 1000 # Truncated millisecond from timestamp for reasons specific to Flexport
|
108
111
|
database.write_items(
|
109
112
|
@options[:test_statuses_table_name],
|
110
|
-
@tests.values.map { |item| item.to_hash.merge(updated_at
|
113
|
+
@tests.values.map { |item| item.to_hash.merge('updated_at' => timestamp) }
|
111
114
|
)
|
112
115
|
rescue Quarantine::DatabaseError => e
|
113
116
|
@database_failures << "#{e.cause&.class}: #{e.cause&.message}"
|
@@ -8,16 +8,7 @@ class Quarantine
|
|
8
8
|
|
9
9
|
abstract!
|
10
10
|
|
11
|
-
Item = T.type_alias
|
12
|
-
{
|
13
|
-
'id' => String,
|
14
|
-
'last_status' => String,
|
15
|
-
'consecutive_passes' => Integer,
|
16
|
-
'full_description' => String,
|
17
|
-
'location' => String,
|
18
|
-
'extra_attributes' => T.untyped
|
19
|
-
}
|
20
|
-
end
|
11
|
+
Item = T.type_alias { T::Hash[String, T.untyped] } # TODO: must have `id` key
|
21
12
|
|
22
13
|
sig { abstract.params(table_name: String).returns(T::Enumerable[Item]) }
|
23
14
|
def fetch_items(table_name); end
|
@@ -57,18 +57,6 @@ class Quarantine
|
|
57
57
|
raise Quarantine::DatabaseError
|
58
58
|
end
|
59
59
|
|
60
|
-
sig { params(table_name: String, keys: T::Hash[T.untyped, T.untyped]).void }
|
61
|
-
def delete_items(table_name, keys)
|
62
|
-
@dynamodb.delete_item(
|
63
|
-
table_name: table_name,
|
64
|
-
key: {
|
65
|
-
**keys
|
66
|
-
}
|
67
|
-
)
|
68
|
-
rescue Aws::DynamoDB::Errors::ServiceError
|
69
|
-
raise Quarantine::DatabaseError
|
70
|
-
end
|
71
|
-
|
72
60
|
sig do
|
73
61
|
params(
|
74
62
|
table_name: String,
|
@@ -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,104 +1,106 @@
|
|
1
1
|
# typed: strict
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
class Quarantine
|
4
|
+
module RSpecAdapter
|
5
|
+
extend T::Sig
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
+
)
|
28
|
+
end
|
28
29
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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)
|
42
|
+
end
|
41
43
|
end
|
42
|
-
end
|
43
44
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
51
|
+
end
|
50
52
|
end
|
51
53
|
end
|
52
|
-
end
|
53
54
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
60
61
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
72
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)
|
73
82
|
end
|
74
|
-
elsif metadata[:retry_attempts] > 0
|
75
|
-
# will record the flaky test if it failed the first run but passed a subsequent run
|
76
|
-
Quarantine::RSpecAdapter.quarantine.record_test(example, :quarantined, passed: false)
|
77
|
-
elsif quarantined
|
78
|
-
Quarantine::RSpecAdapter.quarantine.record_test(example, :quarantined, passed: true)
|
79
|
-
else
|
80
|
-
Quarantine::RSpecAdapter.quarantine.record_test(example, :passing, passed: true)
|
81
83
|
end
|
82
84
|
end
|
83
85
|
end
|
84
|
-
end
|
85
86
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
91
93
|
end
|
92
94
|
end
|
93
|
-
end
|
94
95
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
102
104
|
end
|
103
105
|
end
|
104
106
|
end
|
data/lib/quarantine/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: quarantine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Flexport Engineering, Eric Zhu
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-04-
|
11
|
+
date: 2021-04-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -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
|