Class: Quarantine

Inherits:
Object
  • Object
show all
Extended by:
RSpecAdapter
Defined in:
lib/quarantine.rb,
lib/quarantine/cli.rb,
lib/quarantine/test.rb,
lib/quarantine/error.rb,
lib/quarantine/version.rb,
lib/quarantine/databases/base.rb,
lib/quarantine/databases/dynamo_db.rb

Defined Under Namespace

Modules: Databases Classes: CLI, DatabaseError, Error, Test, UnknownUploadError, UnsupportedDatabaseError

Constant Summary collapse

VERSION =
'1.0.1'.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from RSpecAdapter

bind

Constructor Details

#initialize(options = {}) ⇒ Quarantine

Returns a new instance of Quarantine.



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/quarantine.rb', line 19

def initialize(options = {})
  case options[:database]
  # default database option is dynamodb
  when :dynamodb, nil
    @database = Quarantine::Databases::DynamoDB.new(options)
  else
    raise Quarantine::UnsupportedDatabaseError.new("Quarantine does not support #{options[:database]}")
  end

  @quarantine_map = {}
  @failed_tests = []
  @flaky_tests = []
  @buildkite_build_number = ENV['BUILDKITE_BUILD_NUMBER'] || '-1'
  @summary = { id: 'quarantine', quarantined_tests: [], flaky_tests: [], database_failures: [] }
end

Instance Attribute Details

#buildkite_build_numberObject (readonly)

Returns the value of attribute buildkite_build_number.



16
17
18
# File 'lib/quarantine.rb', line 16

def buildkite_build_number
  @buildkite_build_number
end

#databaseObject

Returns the value of attribute database.



11
12
13
# File 'lib/quarantine.rb', line 11

def database
  @database
end

#duplicate_testsObject (readonly)

Returns the value of attribute duplicate_tests.



15
16
17
# File 'lib/quarantine.rb', line 15

def duplicate_tests
  @duplicate_tests
end

#failed_testsObject (readonly)

Returns the value of attribute failed_tests.



13
14
15
# File 'lib/quarantine.rb', line 13

def failed_tests
  @failed_tests
end

#flaky_testsObject (readonly)

Returns the value of attribute flaky_tests.



14
15
16
# File 'lib/quarantine.rb', line 14

def flaky_tests
  @flaky_tests
end

#quarantine_mapObject (readonly)

Returns the value of attribute quarantine_map.



12
13
14
# File 'lib/quarantine.rb', line 12

def quarantine_map
  @quarantine_map
end

#summaryObject (readonly)

Returns the value of attribute summary.



17
18
19
# File 'lib/quarantine.rb', line 17

def summary
  @summary
end

Instance Method Details

#add_to_summary(attribute, item) ⇒ Object

Param: Symbol, Any Adds the item to the specified attribute in summary



139
140
141
# File 'lib/quarantine.rb', line 139

def add_to_summary(attribute, item)
  summary[attribute] << item if summary.key?(attribute)
end

#fetch_quarantine_listObject

Scans the quarantine_list from the database and store the individual tests in quarantine_map



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/quarantine.rb', line 36

def fetch_quarantine_list
  begin
    quarantine_list = database.scan(RSpec.configuration.quarantine_list_table)
  rescue Quarantine::DatabaseError => e
    add_to_summary(:database_failures, "#{e&.cause&.class}: #{e&.cause&.message}")
    raise Quarantine::DatabaseError.new(
      <<~ERROR_MSG
        Failed to pull the quarantine list from #{RSpec.configuration.quarantine_list_table}
        because of #{e&.cause&.class}: #{e&.cause&.message}
      ERROR_MSG
    )
  end

  quarantine_list.each do |example|
    # on the rare occassion there are duplicate tests ids in the quarantine_list,
    # quarantine the most recent instance of the test (det. through build_number)
    # and ignore the older instance of the test
    next if
      quarantine_map.key?(example['id']) &&
      example['build_number'].to_i < quarantine_map[example['id']].build_number.to_i

    quarantine_map.store(
      example['id'],
      Quarantine::Test.new(example['id'], example['full_description'], example['location'], example['build_number'])
    )
  end
end

#pass_flaky_test(example) ⇒ Object

Param: RSpec::Core::Example Clear exceptions on a flaky tests that has been quarantined

example.clear_exception is tightly coupled with the rspec-retry gem and will only exist if the rspec-retry gem is enabled



126
127
128
129
# File 'lib/quarantine.rb', line 126

def pass_flaky_test(example)
  example.clear_exception
  add_to_summary(:quarantined_tests, example.id)
end

#record_failed_test(example) ⇒ Object

Param: RSpec::Core::Example Add the example to the internal failed tests list



98
99
100
101
102
103
104
105
# File 'lib/quarantine.rb', line 98

def record_failed_test(example)
  failed_tests << Quarantine::Test.new(
    example.id,
    example.full_description,
    example.location,
    buildkite_build_number
  )
end

#record_flaky_test(example) ⇒ Object

Param: RSpec::Core::Example Add the example to the internal flaky tests list



109
110
111
112
113
114
115
116
117
118
119
# File 'lib/quarantine.rb', line 109

def record_flaky_test(example)
  flaky_test = Quarantine::Test.new(
    example.id,
    example.full_description,
    example.location,
    buildkite_build_number
  )

  flaky_tests << flaky_test
  add_to_summary(:flaky_tests, flaky_test.id)
end

#test_quarantined?(example) ⇒ Boolean

Param: RSpec::Core::Example Check the internal quarantine_map to see if this test should be quarantined

Returns:

  • (Boolean)


133
134
135
# File 'lib/quarantine.rb', line 133

def test_quarantined?(example)
  quarantine_map.key?(example.id)
end

#upload_tests(type) ⇒ Object

Based off the type, upload a list of tests to a particular database table



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/quarantine.rb', line 65

def upload_tests(type)
  if type == :failed
    tests = failed_tests
    table_name = RSpec.configuration.quarantine_failed_tests_table
  elsif type == :flaky
    tests = flaky_tests
    table_name = RSpec.configuration.quarantine_list_table
  else
    raise Quarantine::UnknownUploadError.new(
      "Quarantine gem did not know how to handle #{type} upload of tests to dynamodb"
    )
  end

  return unless tests.length < 10 && tests.length > 0

  begin
    timestamp = Time.now.to_i / 1000 # Truncated millisecond from timestamp for reasons specific to Flexport
    database.batch_write_item(
      table_name,
      tests,
      {
        build_job_id: ENV['BUILDKITE_JOB_ID'] || '-1',
        created_at: timestamp,
        updated_at: timestamp
      }
    )
  rescue Quarantine::DatabaseError => e
    add_to_summary(:database_failures, "#{e&.cause&.class}: #{e&.cause&.message}")
  end
end