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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 47aa8e8df1db802259f0107a7a06c2d05162899b803ffd224da8a53246c1e105
4
- data.tar.gz: 8cb6c6c9c5b79666c10feaf38e0077eab27b57d494dc08e0c385925de1c89583
3
+ metadata.gz: ffb721a4514eda413ea4c096913b5d0c3a672b23fe6aefee44393062f202e453
4
+ data.tar.gz: eec03b0ec1533643ae86245e76a69b51fdbf871e6e621293fa542b1b252fc123
5
5
  SHA512:
6
- metadata.gz: e3f9ba5314c425861f18b7451a7f28f8ced4af50479ad880f0be263a9104ba3c6dd85bcada8a0c89d194e9487d02702c9fe4930caceebf835413891fc31d6a37
7
- data.tar.gz: 3177827c0ff075302fa8e62c0b3480728769a3cb6199cb8eb0e510237df9839d0efc10db0180d32016d1ced933d9d50f87336f5f2d6ff3f741622a5aed3364b2
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: timestamp) }
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 do
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
- module Quarantine::RSpecAdapter # rubocop:disable Style/ClassAndModuleChildren
4
- extend T::Sig
3
+ class Quarantine
4
+ module RSpecAdapter
5
+ extend T::Sig
5
6
 
6
- # Purpose: create an instance of Quarantine which contains information
7
- # about the test suite (ie. quarantined tests) and binds RSpec configurations
8
- # and hooks onto the global RSpec class
9
- sig { void }
10
- def self.bind
11
- bind_rspec_configurations
12
- bind_fetch_test_statuses
13
- bind_record_tests
14
- bind_upload_tests
15
- bind_logger
16
- end
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
- sig { returns(Quarantine) }
19
- def self.quarantine
20
- @quarantine = T.let(@quarantine, T.nilable(Quarantine))
21
- @quarantine ||= Quarantine.new(
22
- database: RSpec.configuration.quarantine_database,
23
- test_statuses_table_name: RSpec.configuration.quarantine_test_statuses,
24
- extra_attributes: RSpec.configuration.quarantine_extra_attributes,
25
- failsafe_limit: RSpec.configuration.quarantine_failsafe_limit
26
- )
27
- end
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
- # Purpose: binds rspec configuration variables
30
- sig { void }
31
- def self.bind_rspec_configurations
32
- ::RSpec.configure do |config|
33
- config.add_setting(:quarantine_database, default: { type: :dynamodb, region: 'us-west-1' })
34
- config.add_setting(:quarantine_test_statuses, { default: 'test_statuses' })
35
- config.add_setting(:skip_quarantined_tests, { default: true })
36
- config.add_setting(:quarantine_record_tests, { default: true })
37
- config.add_setting(:quarantine_logging, { default: true })
38
- config.add_setting(:quarantine_extra_attributes)
39
- config.add_setting(:quarantine_failsafe_limit, default: 10)
40
- config.add_setting(:quarantine_release_at_consecutive_passes)
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
- # Purpose: binds quarantine to fetch the test_statuses from dynamodb in the before suite
45
- sig { void }
46
- def self.bind_fetch_test_statuses
47
- ::RSpec.configure do |config|
48
- config.before(:suite) do
49
- Quarantine::RSpecAdapter.quarantine.fetch_test_statuses
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
- # Purpose: binds quarantine to record test statuses
55
- sig { void }
56
- def self.bind_record_tests
57
- ::RSpec.configure do |config|
58
- config.after(:each) do |example|
59
- metadata = example.metadata
55
+ # Purpose: binds quarantine to record test statuses
56
+ sig { void }
57
+ def self.bind_record_tests
58
+ ::RSpec.configure do |config|
59
+ config.after(:each) do |example|
60
+ metadata = example.metadata
60
61
 
61
- # optionally, the upstream RSpec configuration could define an after hook that marks an example as flaky in
62
- # the example's metadata
63
- quarantined = Quarantine::RSpecAdapter.quarantine.test_quarantined?(example) || metadata[:flaky]
64
- if example.exception
65
- if metadata[:retry_attempts] + 1 == metadata[:retry]
66
- # will record the failed test if it's final retry from the rspec-retry gem
67
- if RSpec.configuration.skip_quarantined_tests && quarantined
68
- example.clear_exception!
69
- Quarantine::RSpecAdapter.quarantine.record_test(example, :quarantined, passed: false)
70
- else
71
- Quarantine::RSpecAdapter.quarantine.record_test(example, :failing, passed: false)
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
- sig { void }
87
- def self.bind_upload_tests
88
- ::RSpec.configure do |config|
89
- config.after(:suite) do
90
- Quarantine::RSpecAdapter.quarantine.upload_tests if RSpec.configuration.quarantine_record_tests
87
+ sig { void }
88
+ def self.bind_upload_tests
89
+ ::RSpec.configure do |config|
90
+ config.after(:suite) do
91
+ Quarantine::RSpecAdapter.quarantine.upload_tests if RSpec.configuration.quarantine_record_tests
92
+ end
91
93
  end
92
94
  end
93
- end
94
95
 
95
- # Purpose: binds quarantine logger to output test to RSpec formatter messages
96
- sig { void }
97
- def self.bind_logger
98
- ::RSpec.configure do |config|
99
- config.after(:suite) do
100
- if RSpec.configuration.quarantine_logging
101
- RSpec.configuration.reporter.message(Quarantine::RSpecAdapter.quarantine.summary)
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
@@ -1,3 +1,3 @@
1
1
  class Quarantine
2
- VERSION = '2.0.0'.freeze
2
+ VERSION = '2.1.0'.freeze
3
3
  end
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.0.0
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 00:00:00.000000000 Z
11
+ date: 2021-04-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
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