quarantine 2.0.0 → 2.1.0

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: 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