quarantine 2.1.0 → 2.2.1
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/lib/quarantine.rb +31 -39
- data/lib/quarantine/databases/google_sheets.rb +20 -6
- data/lib/quarantine/rspec_adapter.rb +69 -45
- data/lib/quarantine/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 31ffed67bd1774918fd52c8bfe309398d43b8c218175f2a414725382df680ee7
|
4
|
+
data.tar.gz: aca3eab589422f555224ce2638e979aff7f6fc2f17d4b33ac91e2f0177d4fb9b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '0894a8c2561b741d3f9466d28e8424cde69ece0b7f228d700b485f748eb26531bd81ac549493a91641010ceb6b34eb1d7abbaf49475f4874d6f67db297d522ec'
|
7
|
+
data.tar.gz: 281ba613e8806b8d3f1e4318a65492435d971e0e89138cb87c44de2c7709a983672930e00cdfddb46bf245da4aa2e52804b2cf47d908fe9f0dd62eae3e7ac5fd
|
data/lib/quarantine.rb
CHANGED
@@ -9,23 +9,6 @@ require 'quarantine/databases/base'
|
|
9
9
|
require 'quarantine/databases/dynamo_db'
|
10
10
|
require 'quarantine/databases/google_sheets'
|
11
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
|
28
|
-
|
29
12
|
class Quarantine
|
30
13
|
extend T::Sig
|
31
14
|
|
@@ -66,7 +49,7 @@ class Quarantine
|
|
66
49
|
|
67
50
|
# Scans the test_statuses from the database and store their IDs in quarantined_ids
|
68
51
|
sig { void }
|
69
|
-
def
|
52
|
+
def on_start
|
70
53
|
begin
|
71
54
|
test_statuses = database.fetch_items(@options[:test_statuses_table_name])
|
72
55
|
rescue Quarantine::DatabaseError => e
|
@@ -103,24 +86,40 @@ class Quarantine
|
|
103
86
|
end
|
104
87
|
|
105
88
|
sig { void }
|
106
|
-
def
|
107
|
-
|
89
|
+
def on_complete
|
90
|
+
quarantined_tests = @tests.values.select { |test| test.status == :quarantined }.sort_by(&:id)
|
108
91
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
)
|
115
|
-
|
116
|
-
|
92
|
+
if !@options[:record_tests]
|
93
|
+
log('Recording tests disabled; skipping')
|
94
|
+
elsif @tests.empty?
|
95
|
+
log('No tests found; skipping recording')
|
96
|
+
elsif quarantined_tests.count { |test| old_tests[test.id]&.status != :quarantined } >= @options[:failsafe_limit]
|
97
|
+
log('Number of quarantined tests above failsafe limit; skipping recording')
|
98
|
+
else
|
99
|
+
begin
|
100
|
+
timestamp = Time.now.to_i / 1000 # Truncated millisecond from timestamp for reasons specific to Flexport
|
101
|
+
database.write_items(
|
102
|
+
@options[:test_statuses_table_name],
|
103
|
+
@tests.values.map { |item| item.to_hash.merge('updated_at' => timestamp) }
|
104
|
+
)
|
105
|
+
rescue Quarantine::DatabaseError => e
|
106
|
+
@database_failures << "#{e.cause&.class}: #{e.cause&.message}"
|
107
|
+
end
|
117
108
|
end
|
109
|
+
|
110
|
+
log(<<~MESSAGE)
|
111
|
+
\n[quarantine] Quarantined tests:
|
112
|
+
#{quarantined_tests.map { |test| "#{test.id} #{test.full_description}" }.join("\n ")}
|
113
|
+
|
114
|
+
[quarantine] Database errors:
|
115
|
+
#{@database_failures.join("\n ")}
|
116
|
+
MESSAGE
|
118
117
|
end
|
119
118
|
|
120
119
|
# Param: RSpec::Core::Example
|
121
120
|
# Add the example to the internal tests list
|
122
121
|
sig { params(example: T.untyped, status: Symbol, passed: T::Boolean).void }
|
123
|
-
def
|
122
|
+
def on_test(example, status, passed:)
|
124
123
|
extra_attributes = @options[:extra_attributes] ? @options[:extra_attributes].call(example) : {}
|
125
124
|
|
126
125
|
new_consecutive_passes = passed ? (@old_tests[example.id]&.consecutive_passes || 0) + 1 : 0
|
@@ -145,15 +144,8 @@ class Quarantine
|
|
145
144
|
@old_tests[example.id]&.status == :quarantined
|
146
145
|
end
|
147
146
|
|
148
|
-
sig {
|
149
|
-
def
|
150
|
-
|
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
|
147
|
+
sig { params(message: String).void }
|
148
|
+
def log(message)
|
149
|
+
@options[:log].call(message) if @options[:logging]
|
158
150
|
end
|
159
151
|
end
|
@@ -21,6 +21,8 @@ class Quarantine
|
|
21
21
|
sig { override.params(table_name: String).returns(T::Enumerable[Item]) }
|
22
22
|
def fetch_items(table_name)
|
23
23
|
parse_rows(spreadsheet.worksheet_by_title(table_name))
|
24
|
+
rescue GoogleDrive::Error, Google::Apis::Error
|
25
|
+
raise Quarantine::DatabaseError
|
24
26
|
end
|
25
27
|
|
26
28
|
sig do
|
@@ -35,10 +37,16 @@ class Quarantine
|
|
35
37
|
new_rows = []
|
36
38
|
|
37
39
|
# Map existing ID to row index
|
38
|
-
|
40
|
+
parsed_rows = parse_rows(worksheet)
|
41
|
+
indexes = Hash[parsed_rows.each_with_index.map { |item, idx| [item['id'], idx] }]
|
39
42
|
|
40
43
|
items.each do |item|
|
41
|
-
cells = headers.map
|
44
|
+
cells = headers.map do |header|
|
45
|
+
match = header.match(/^(extra_)?(.+)/)
|
46
|
+
extra, name = match[1..2]
|
47
|
+
value = extra ? item['extra_attributes'][name] : item[name]
|
48
|
+
value.to_s
|
49
|
+
end
|
42
50
|
row_idx = indexes[item['id']]
|
43
51
|
if row_idx
|
44
52
|
# Overwrite existing row
|
@@ -51,8 +59,10 @@ class Quarantine
|
|
51
59
|
end
|
52
60
|
|
53
61
|
# Insert any items whose IDs weren't found in existing rows at the end
|
54
|
-
worksheet.insert_rows(
|
62
|
+
worksheet.insert_rows(parsed_rows.count + 2, new_rows)
|
55
63
|
worksheet.save
|
64
|
+
rescue GoogleDrive::Error, Google::Apis::Error
|
65
|
+
raise Quarantine::DatabaseError
|
56
66
|
end
|
57
67
|
|
58
68
|
private
|
@@ -98,9 +108,13 @@ class Quarantine
|
|
98
108
|
rows.map do |row|
|
99
109
|
hash_row = Hash[headers.zip(row)]
|
100
110
|
# TODO: use Google Sheets developer metadata to store type information
|
101
|
-
|
102
|
-
|
103
|
-
|
111
|
+
next nil if hash_row['id'].empty?
|
112
|
+
|
113
|
+
extra_values, base_values = hash_row.partition { |k, _v| k.start_with?('extra_') }
|
114
|
+
base_hash = Hash[base_values]
|
115
|
+
base_hash['extra_attributes'] = Hash[extra_values]
|
116
|
+
base_hash
|
117
|
+
end.compact
|
104
118
|
end
|
105
119
|
end
|
106
120
|
end
|
@@ -1,19 +1,32 @@
|
|
1
1
|
# typed: strict
|
2
2
|
|
3
|
+
module RSpec
|
4
|
+
module Core
|
5
|
+
class Example
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
# The implementation of clear_exception in rspec-retry doesn't work
|
9
|
+
# for examples that use `it_behaves_like`, so we implement our own version that
|
10
|
+
# clear the exception field recursively.
|
11
|
+
sig { void }
|
12
|
+
def clear_exception!
|
13
|
+
@exception = T.let(nil, T.untyped)
|
14
|
+
T.unsafe(self).example.clear_exception! if defined?(example)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
3
20
|
class Quarantine
|
4
21
|
module RSpecAdapter
|
5
22
|
extend T::Sig
|
6
23
|
|
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
24
|
sig { void }
|
11
25
|
def self.bind
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
bind_logger
|
26
|
+
register_rspec_configurations
|
27
|
+
bind_on_start
|
28
|
+
bind_on_test
|
29
|
+
bind_on_complete
|
17
30
|
end
|
18
31
|
|
19
32
|
sig { returns(Quarantine) }
|
@@ -23,13 +36,17 @@ class Quarantine
|
|
23
36
|
database: RSpec.configuration.quarantine_database,
|
24
37
|
test_statuses_table_name: RSpec.configuration.quarantine_test_statuses,
|
25
38
|
extra_attributes: RSpec.configuration.quarantine_extra_attributes,
|
26
|
-
failsafe_limit: RSpec.configuration.quarantine_failsafe_limit
|
39
|
+
failsafe_limit: RSpec.configuration.quarantine_failsafe_limit,
|
40
|
+
release_at_consecutive_passes: RSpec.configuration.quarantine_release_at_consecutive_passes,
|
41
|
+
logging: RSpec.configuration.quarantine_logging,
|
42
|
+
log: method(:log),
|
43
|
+
record_tests: RSpec.configuration.quarantine_record_tests
|
27
44
|
)
|
28
45
|
end
|
29
46
|
|
30
47
|
# Purpose: binds rspec configuration variables
|
31
48
|
sig { void }
|
32
|
-
def self.
|
49
|
+
def self.register_rspec_configurations
|
33
50
|
::RSpec.configure do |config|
|
34
51
|
config.add_setting(:quarantine_database, default: { type: :dynamodb, region: 'us-west-1' })
|
35
52
|
config.add_setting(:quarantine_test_statuses, { default: 'test_statuses' })
|
@@ -44,65 +61,72 @@ class Quarantine
|
|
44
61
|
|
45
62
|
# Purpose: binds quarantine to fetch the test_statuses from dynamodb in the before suite
|
46
63
|
sig { void }
|
47
|
-
def self.
|
64
|
+
def self.bind_on_start
|
48
65
|
::RSpec.configure do |config|
|
49
66
|
config.before(:suite) do
|
50
|
-
Quarantine::RSpecAdapter.quarantine.
|
67
|
+
Quarantine::RSpecAdapter.quarantine.on_start
|
51
68
|
end
|
52
69
|
end
|
53
70
|
end
|
54
71
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
72
|
+
sig { params(example: RSpec::Core::Example).returns(T.nilable([Symbol, T::Boolean])) }
|
73
|
+
def self.final_status(example)
|
74
|
+
metadata = example.metadata
|
75
|
+
|
76
|
+
# The user may define their own after hook that marks an example as flaky in its metadata.
|
77
|
+
previously_quarantined = Quarantine::RSpecAdapter.quarantine.test_quarantined?(example) || metadata[:flaky]
|
61
78
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
if
|
66
|
-
|
67
|
-
|
68
|
-
|
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)
|
79
|
+
if example.exception
|
80
|
+
# The example failed _this try_.
|
81
|
+
if metadata[:retry_attempts] + 1 == metadata[:retry]
|
82
|
+
# The example failed all its retries - if it's already quarantined, keep it that way;
|
83
|
+
# otherwise, mark it as failing.
|
84
|
+
if RSpec.configuration.skip_quarantined_tests && previously_quarantined
|
85
|
+
return [:quarantined, false]
|
80
86
|
else
|
81
|
-
|
87
|
+
return [:failing, false]
|
82
88
|
end
|
83
89
|
end
|
90
|
+
# The example failed, but it's not the final retry yet, so return nil.
|
91
|
+
return nil # rubocop:disable Style/RedundantReturn
|
92
|
+
elsif metadata[:retry_attempts] > 0
|
93
|
+
# The example passed this time, but failed one or more times before - the definition of a flaky test.
|
94
|
+
return [:quarantined, false] # rubocop:disable Style/RedundantReturn
|
95
|
+
elsif previously_quarantined
|
96
|
+
# The example passed the first time, but it's already marked quarantined, so keep it that way.
|
97
|
+
return [:quarantined, true] # rubocop:disable Style/RedundantReturn
|
98
|
+
else
|
99
|
+
return [:passing, true] # rubocop:disable Style/RedundantReturn
|
84
100
|
end
|
85
101
|
end
|
86
102
|
|
103
|
+
# Purpose: binds quarantine to record test statuses
|
87
104
|
sig { void }
|
88
|
-
def self.
|
105
|
+
def self.bind_on_test
|
89
106
|
::RSpec.configure do |config|
|
90
|
-
config.after(:
|
91
|
-
Quarantine::RSpecAdapter.
|
107
|
+
config.after(:each) do |example|
|
108
|
+
result = Quarantine::RSpecAdapter.final_status(example)
|
109
|
+
if result
|
110
|
+
status, passed = result
|
111
|
+
example.clear_exception! if status == :quarantined && !passed
|
112
|
+
Quarantine::RSpecAdapter.quarantine.on_test(example, status, passed: passed)
|
113
|
+
end
|
92
114
|
end
|
93
115
|
end
|
94
116
|
end
|
95
117
|
|
96
|
-
# Purpose: binds quarantine logger to output test to RSpec formatter messages
|
97
118
|
sig { void }
|
98
|
-
def self.
|
119
|
+
def self.bind_on_complete
|
99
120
|
::RSpec.configure do |config|
|
100
121
|
config.after(:suite) do
|
101
|
-
|
102
|
-
RSpec.configuration.reporter.message(Quarantine::RSpecAdapter.quarantine.summary)
|
103
|
-
end
|
122
|
+
Quarantine::RSpecAdapter.quarantine.on_complete
|
104
123
|
end
|
105
124
|
end
|
106
125
|
end
|
126
|
+
|
127
|
+
sig { params(message: String).void }
|
128
|
+
def self.log(message)
|
129
|
+
RSpec.configuration.reporter.message(message)
|
130
|
+
end
|
107
131
|
end
|
108
132
|
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.1
|
4
|
+
version: 2.2.1
|
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-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|