rspec-flake-classifier 0.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.
Files changed (31) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +301 -0
  4. data/Rakefile +8 -0
  5. data/exe/rspec-flake +6 -0
  6. data/lib/rspec/flake/classifier/classify/classifier.rb +228 -0
  7. data/lib/rspec/flake/classifier/classify/context.rb +41 -0
  8. data/lib/rspec/flake/classifier/classify/result.rb +44 -0
  9. data/lib/rspec/flake/classifier/cli.rb +298 -0
  10. data/lib/rspec/flake/classifier/configuration.rb +40 -0
  11. data/lib/rspec/flake/classifier/coverage_snapshot.rb +89 -0
  12. data/lib/rspec/flake/classifier/deflaker.rb +102 -0
  13. data/lib/rspec/flake/classifier/evaluation.rb +127 -0
  14. data/lib/rspec/flake/classifier/example_history.rb +24 -0
  15. data/lib/rspec/flake/classifier/features.rb +42 -0
  16. data/lib/rspec/flake/classifier/formatter.rb +194 -0
  17. data/lib/rspec/flake/classifier/integrations.rb +247 -0
  18. data/lib/rspec/flake/classifier/predictor.rb +144 -0
  19. data/lib/rspec/flake/classifier/probe_evidence.rb +77 -0
  20. data/lib/rspec/flake/classifier/rerun/bisect_dependency_search.rb +81 -0
  21. data/lib/rspec/flake/classifier/rerun/isolated_runner.rb +69 -0
  22. data/lib/rspec/flake/classifier/rerun/protocol.rb +83 -0
  23. data/lib/rspec/flake/classifier/rerun/result.rb +82 -0
  24. data/lib/rspec/flake/classifier/runtime_controls.rb +63 -0
  25. data/lib/rspec/flake/classifier/sensitivity.rb +82 -0
  26. data/lib/rspec/flake/classifier/signature.rb +59 -0
  27. data/lib/rspec/flake/classifier/store/jsonl_store.rb +131 -0
  28. data/lib/rspec/flake/classifier/version.rb +13 -0
  29. data/lib/rspec/flake/classifier.rb +285 -0
  30. data/sig/rspec/flake/classifier.rbs +176 -0
  31. metadata +135 -0
@@ -0,0 +1,285 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "classifier/classify/classifier"
4
+ require_relative "classifier/configuration"
5
+ require_relative "classifier/coverage_snapshot"
6
+ require_relative "classifier/deflaker"
7
+ require_relative "classifier/example_history"
8
+ require_relative "classifier/evaluation"
9
+ require_relative "classifier/features"
10
+ require_relative "classifier/integrations"
11
+ require_relative "classifier/predictor"
12
+ require_relative "classifier/probe_evidence"
13
+ require_relative "classifier/rerun/isolated_runner"
14
+ require_relative "classifier/rerun/protocol"
15
+ require_relative "classifier/runtime_controls"
16
+ require_relative "classifier/sensitivity"
17
+ require_relative "classifier/signature"
18
+ require_relative "classifier/store/jsonl_store"
19
+ require_relative "classifier/version"
20
+
21
+ module RSpec
22
+ module FlakeClassifier
23
+ class Error < StandardError; end
24
+
25
+ RuntimeControls.activate!
26
+
27
+ class << self
28
+ def configuration
29
+ @configuration ||= Configuration.new
30
+ end
31
+
32
+ def configure(rspec_config = nil)
33
+ yield(configuration) if block_given?
34
+ RSpecIntegration.install(rspec_config, configuration) if rspec_config
35
+ configuration
36
+ end
37
+
38
+ def signature_for(exception)
39
+ Signature.from_exception(exception)
40
+ end
41
+
42
+ def store
43
+ Store::JSONLStore.new(configuration.store_path)
44
+ end
45
+
46
+ def classify(input = nil, **attributes)
47
+ Classify::Classifier.new.classify(input, **attributes)
48
+ end
49
+
50
+ def investigate(example_id, seed: nil, prior_examples: [], runner: nil)
51
+ runner ||= Rerun::IsolatedRunner.new(command: configuration.rspec_command)
52
+ Rerun::Protocol.new(
53
+ runner: runner,
54
+ same_order_runs: configuration.auto_rerun_attempts
55
+ ).investigate(example_id, seed: seed, prior_examples: prior_examples)
56
+ end
57
+
58
+ def sensitivity(example_id, factors: configuration.sensitivity_factors, seed: nil, runner: nil)
59
+ runner ||= Rerun::IsolatedRunner.new(command: configuration.rspec_command)
60
+ Sensitivity.new(runner: runner).analyze(example_id, factors: factors, seed: seed)
61
+ end
62
+ end
63
+
64
+ module RSpecIntegration
65
+ module_function
66
+
67
+ def install(rspec_config, configuration)
68
+ return if ENV[Rerun::IsolatedRunner::DISABLE_ENV] == "1"
69
+ return if rspec_config.instance_variable_defined?(:@flake_classifier_installed)
70
+
71
+ rspec_config.instance_variable_set(:@flake_classifier_installed, true)
72
+ reset!
73
+ install_around_hook(rspec_config) do |example|
74
+ prior_examples = RSpecIntegration.prior_examples
75
+ coverage_before = CoverageSnapshot.capture
76
+ example.run
77
+ coverage = RSpecIntegration.coverage_for(example, configuration, coverage_before)
78
+ if example.exception
79
+ RSpecIntegration.handle_failure(
80
+ example,
81
+ configuration,
82
+ prior_examples: prior_examples,
83
+ coverage: coverage
84
+ )
85
+ end
86
+ ensure
87
+ RSpecIntegration.record_example(example)
88
+ end
89
+ end
90
+
91
+ def install_around_hook(rspec_config, &hook)
92
+ if rspec_config.respond_to?(:hooks) && rspec_config.hooks.respond_to?(:register)
93
+ rspec_config.hooks.register(:append, :around, :each, &hook)
94
+ else
95
+ rspec_config.around(:each, &hook)
96
+ end
97
+ end
98
+
99
+ def handle_failure(example, configuration, prior_examples: [], coverage: {})
100
+ signature = Signature.from_exception(example.exception)
101
+ store = Store::JSONLStore.new(configuration.store_path)
102
+ known_flake = store.known_flake?(signature.digest)
103
+ probes = ProbeEvidence.new(provider: configuration.probe_provider).collect(example)
104
+ deflaker = deflaker_result(configuration, coverage)
105
+ sensitivity = nil
106
+ metadata = failure_metadata(example, prior_examples, coverage, probes, deflaker, sensitivity)
107
+
108
+ if known_flake && configuration.skip_known_flakes
109
+ store.record(signature, classification: known_classification, metadata: metadata)
110
+ clear_exception(example)
111
+ return
112
+ end
113
+
114
+ sensitivity = sensitivity_result(example, configuration)
115
+ metadata = failure_metadata(example, prior_examples, coverage, probes, deflaker, sensitivity)
116
+
117
+ investigation = nil
118
+ if configuration.auto_rerun
119
+ investigation = rerun_example(example, configuration, prior_examples)
120
+ clear_exception(example) if investigation.flaky?
121
+ end
122
+
123
+ classification = classify_failure(
124
+ example,
125
+ investigation,
126
+ probes: probes,
127
+ deflaker: deflaker,
128
+ sensitivity: sensitivity
129
+ )
130
+ Integrations.tag_ci_execution(example: example, signature: signature, classification: classification)
131
+ store.record(signature, classification: classification, metadata: metadata)
132
+ example.metadata[:flake_classifier] = {
133
+ signature: signature.digest,
134
+ known_flake: known_flake,
135
+ coverage: coverage,
136
+ deflaker: deflaker&.to_h,
137
+ sensitivity: sensitivity&.to_h,
138
+ probes: probes,
139
+ investigation: investigation&.to_h,
140
+ classification: classification.to_h
141
+ }
142
+ end
143
+
144
+ def rerun_example(example, configuration, prior_examples)
145
+ RSpec::FlakeClassifier.investigate(
146
+ example.id,
147
+ seed: current_seed,
148
+ prior_examples: prior_examples,
149
+ runner: Rerun::IsolatedRunner.new(command: configuration.rspec_command)
150
+ )
151
+ end
152
+
153
+ def classify_failure(example, investigation, probes:, deflaker:, sensitivity:)
154
+ Classify::Classifier.new.classify(
155
+ message: example.exception.message,
156
+ backtrace: example.exception.backtrace,
157
+ order_dependent: investigation&.order_type == "od",
158
+ duration_samples: investigation&.runs&.map(&:duration),
159
+ resources: probes.fetch(:resources, []),
160
+ files: probes.fetch(:files, []),
161
+ sockets: probes.fetch(:sockets, []),
162
+ source: probes[:source],
163
+ metadata: {
164
+ deflaker: deflaker&.to_h,
165
+ sensitivity: sensitivity&.to_h
166
+ }.compact
167
+ )
168
+ end
169
+
170
+ def coverage_for(example, configuration, coverage_before)
171
+ provided = provider_value(configuration.coverage_provider, example)
172
+ return provided if provided
173
+
174
+ integration_coverage = Integrations.coverage_for(example)
175
+ return integration_coverage if integration_coverage
176
+
177
+ metadata_coverage = example.metadata[:flake_classifier_coverage] || example.metadata[:covered_lines]
178
+ return metadata_coverage if metadata_coverage
179
+
180
+ coverage_before.delta
181
+ end
182
+
183
+ def deflaker_result(configuration, coverage)
184
+ return nil unless configuration.deflaker
185
+ return nil if coverage.nil? || coverage.empty?
186
+
187
+ Deflaker.new.suspect?(
188
+ coverage: coverage,
189
+ changed_lines: changed_lines(configuration),
190
+ base: deflaker_base(configuration)
191
+ )
192
+ end
193
+
194
+ def sensitivity_result(example, configuration)
195
+ return nil unless configuration.run_sensitivity
196
+
197
+ RSpec::FlakeClassifier.sensitivity(
198
+ example.id,
199
+ factors: configuration.sensitivity_factors,
200
+ seed: current_seed,
201
+ runner: Rerun::IsolatedRunner.new(command: configuration.rspec_command)
202
+ )
203
+ end
204
+
205
+ def changed_lines(configuration)
206
+ provider_value(configuration.changed_lines_provider)
207
+ end
208
+
209
+ def deflaker_base(configuration)
210
+ return "HEAD" unless configuration.deflaker.is_a?(Hash)
211
+
212
+ configuration.deflaker.fetch(:base, configuration.deflaker.fetch("base", "HEAD"))
213
+ end
214
+
215
+ def provider_value(provider, *args)
216
+ return nil unless provider
217
+
218
+ return provider.call if provider.respond_to?(:arity) && provider.arity.zero?
219
+
220
+ provider.call(*args)
221
+ rescue StandardError
222
+ nil
223
+ end
224
+
225
+ def failure_metadata(example, prior_examples, coverage, probes, deflaker, sensitivity)
226
+ {
227
+ example_id: example.id,
228
+ file_path: example.metadata[:file_path],
229
+ line_number: example.metadata[:line_number],
230
+ prior_examples: prior_examples,
231
+ coverage: coverage,
232
+ probes: probes,
233
+ deflaker: deflaker&.to_h,
234
+ sensitivity: sensitivity&.to_h
235
+ }.compact
236
+ end
237
+
238
+ def known_classification
239
+ Classify::Result.new(
240
+ status: "flaky",
241
+ labels: [
242
+ Classify::Label.new(
243
+ category: "known_flaky",
244
+ confidence: 1.0,
245
+ evidence: ["signature matched the flake store"],
246
+ action: "Inspect the stored classification and linked commits."
247
+ )
248
+ ]
249
+ )
250
+ end
251
+
252
+ def clear_exception(example)
253
+ example.instance_variable_set(:@exception, nil)
254
+ end
255
+
256
+ def prior_examples
257
+ history.prior_examples
258
+ end
259
+
260
+ def record_example(example)
261
+ history.record(example)
262
+ end
263
+
264
+ def reset!
265
+ @history = ExampleHistory.new
266
+ end
267
+
268
+ def history
269
+ @history ||= ExampleHistory.new
270
+ end
271
+
272
+ def current_seed
273
+ return unless defined?(::RSpec) && ::RSpec.respond_to?(:configuration)
274
+
275
+ ::RSpec.configuration.seed
276
+ end
277
+ end
278
+ end
279
+ end
280
+
281
+ module Rspec
282
+ module Flake
283
+ Classifier = ::RSpec::FlakeClassifier unless const_defined?(:Classifier, false)
284
+ end
285
+ end
@@ -0,0 +1,176 @@
1
+ module RSpec
2
+ module FlakeClassifier
3
+ VERSION: String
4
+
5
+ def self.configuration: () -> Configuration
6
+ def self.configure: (?untyped rspec_config) { (Configuration) -> void } -> Configuration
7
+ def self.signature_for: (Exception exception) -> Signature
8
+ def self.store: () -> Store::JSONLStore
9
+ def self.classify: (?untyped input, **untyped attributes) -> Classify::Result
10
+ def self.investigate: (String example_id, ?seed: Integer?, ?prior_examples: Array[String], ?runner: untyped) -> Rerun::Investigation
11
+ def self.sensitivity: (String example_id, ?factors: Array[Symbol], ?seed: Integer?, ?runner: untyped) -> Sensitivity::Result
12
+
13
+ class Configuration
14
+ attr_accessor store: String
15
+ attr_accessor auto_rerun: bool | Hash[Symbol, untyped]
16
+ attr_accessor deflaker: bool
17
+ attr_accessor rspec_command: Array[String]?
18
+ attr_accessor same_order_runs: Integer
19
+ attr_accessor skip_known_flakes: bool
20
+ attr_accessor coverage_provider: untyped
21
+ attr_accessor changed_lines_provider: untyped
22
+ attr_accessor probe_provider: untyped
23
+ attr_accessor run_sensitivity: bool
24
+ attr_accessor sensitivity_factors: Array[Symbol]
25
+
26
+ def initialize: () -> void
27
+ def auto_rerun_attempts: () -> Integer
28
+ def store_path: () -> String
29
+ end
30
+
31
+ class Signature
32
+ attr_reader message: String
33
+ attr_reader backtrace: Array[String]
34
+ attr_reader normalized: String
35
+ attr_reader digest: String
36
+
37
+ def self.from_exception: (Exception exception) -> Signature
38
+ def initialize: (message: String, ?backtrace: Array[String]) -> void
39
+ def to_h: () -> Hash[String, untyped]
40
+ end
41
+
42
+ class Deflaker
43
+ def suspect?: (coverage: untyped, ?changed_lines: untyped, ?base: String) -> Deflaker::Result
44
+ def git_changed_lines: (?String base) -> Hash[String, Array[Integer]]
45
+ def parse_diff: (String diff) -> Hash[String, Array[Integer]]
46
+
47
+ Result: singleton(::Struct)
48
+ end
49
+
50
+ class Sensitivity
51
+ def initialize: (runner: untyped) -> void
52
+ def analyze: (String example_id, ?factors: Array[Symbol], ?seed: Integer?) -> Sensitivity::Result
53
+
54
+ Result: singleton(::Struct)
55
+ end
56
+
57
+ class CoverageSnapshot
58
+ def self.capture: () -> CoverageSnapshot
59
+ def self.null: () -> CoverageSnapshot
60
+ def initialize: (Hash[String, untyped] data) -> void
61
+ def delta: () -> Hash[String, Array[Integer]]
62
+ end
63
+
64
+ class ProbeEvidence
65
+ def initialize: (?provider: untyped) -> void
66
+ def collect: (untyped example) -> Hash[Symbol, untyped]
67
+ end
68
+
69
+ class ExampleHistory
70
+ def initialize: () -> void
71
+ def prior_examples: () -> Array[String]
72
+ def record: (untyped example) -> void
73
+ def clear: () -> void
74
+ end
75
+
76
+ class Features
77
+ def extract: (?file: String?, ?source: String?, ?duration: Float?, ?metadata: Hash[Symbol, untyped]) -> Hash[String, untyped]
78
+ end
79
+
80
+ class Predictor
81
+ attr_reader weights: Hash[String, Float]
82
+
83
+ def self.train: (Array[Hash[String | Symbol, untyped]] examples) -> Predictor
84
+ def self.load: (String path) -> Predictor
85
+ def initialize: (?weights: Hash[String | Symbol, untyped]) -> void
86
+ def score: (Hash[String | Symbol, untyped] features) -> Float
87
+ def rank: (Array[Hash[String | Symbol, untyped]] feature_sets) -> Array[Hash[String, untyped]]
88
+ def to_h: () -> Hash[String, Hash[String, Float]]
89
+ end
90
+
91
+ class Evaluation
92
+ def classification: (predictions: Array[Hash[String | Symbol, untyped]], ground_truth: Array[Hash[String | Symbol, untyped]]) -> Hash[String, untyped]
93
+ def deflaker: (records: Array[Hash[String | Symbol, untyped]]) -> Hash[String, untyped]
94
+ def signatures: (records: Array[Hash[String | Symbol, untyped]]) -> Hash[String, untyped]
95
+ def idflakies: (records: Array[Hash[String | Symbol, untyped]]) -> Hash[String, untyped]
96
+ end
97
+
98
+ module Integrations
99
+ def self.coverage_for: (untyped example) -> untyped
100
+ def self.probe_for: (untyped example) -> untyped
101
+ def self.tag_ci_execution: (example: untyped, signature: Signature, classification: untyped) -> void
102
+ end
103
+
104
+ class Formatter
105
+ def initialize: (untyped output) -> void
106
+ def example_failed: (untyped notification) -> void
107
+ def dump_summary: (untyped notification) -> void
108
+ end
109
+
110
+ class JUnitFormatter
111
+ def initialize: (untyped output) -> void
112
+ def example_passed: (untyped notification) -> void
113
+ def example_failed: (untyped notification) -> void
114
+ def dump_summary: (untyped notification) -> void
115
+ end
116
+
117
+ module Store
118
+ class JSONLStore
119
+ attr_reader path: String
120
+
121
+ def initialize: (String path) -> void
122
+ def record: (Signature signature, ?classification: untyped, ?metadata: Hash[Symbol, untyped]) -> Hash[String, untyped]
123
+ def update_classification: (String digest, classification: untyped, ?metadata: Hash[Symbol, untyped]) -> Hash[String, untyped]?
124
+ def find: (String digest) -> Hash[String, untyped]?
125
+ def known_flake?: (String digest) -> bool
126
+ def all: () -> Hash[String, Hash[String, untyped]]
127
+ def entries: () -> Array[Hash[String, untyped]]
128
+ end
129
+ end
130
+
131
+ module Classify
132
+ Label: singleton(::Struct)
133
+
134
+ class Result
135
+ attr_reader status: String
136
+ attr_reader labels: Array[untyped]
137
+ attr_reader evidence: Array[String]
138
+
139
+ def initialize: (status: String, labels: Array[untyped], ?evidence: Array[String]) -> void
140
+ def primary_label: () -> untyped
141
+ def flaky?: () -> bool
142
+ def to_h: () -> Hash[String, untyped]
143
+ end
144
+ end
145
+
146
+ module Rerun
147
+ Result: singleton(::Struct)
148
+
149
+ class BisectDependencySearch
150
+ def initialize: (runner: untyped) -> void
151
+ def find: (String example_id, Array[String] prior_examples, ?seed: Integer?) -> Array[String]?
152
+ end
153
+
154
+ class Investigation
155
+ attr_reader example_id: String
156
+ attr_reader status: String
157
+ attr_reader order_type: String?
158
+ attr_reader runs: Array[untyped]
159
+ attr_reader dependency_examples: Array[String]
160
+ attr_reader dependency_role: String?
161
+
162
+ def flaky?: () -> bool
163
+ def true_failure?: () -> bool
164
+ def labels: () -> Array[String]
165
+ def to_h: () -> Hash[String, untyped]
166
+ end
167
+ end
168
+ end
169
+ end
170
+
171
+ module Rspec
172
+ module Flake
173
+ module Classifier
174
+ end
175
+ end
176
+ end
metadata ADDED
@@ -0,0 +1,135 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec-flake-classifier
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yudai Takada
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rspec-core
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '3.10'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '4.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '3.10'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '4.0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: rspec-covers
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: 0.1.0
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '1.0'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: 0.1.0
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '1.0'
52
+ - !ruby/object:Gem::Dependency
53
+ name: rspec-hermetic
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: 0.1.0
59
+ - - "<"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 0.1.0
69
+ - - "<"
70
+ - !ruby/object:Gem::Version
71
+ version: '1.0'
72
+ description: RSpec flake classifier records normalized failure signatures, reruns
73
+ failing examples in isolated processes, and assigns flaky-test cause labels.
74
+ email:
75
+ - t.yudai92@gmail.com
76
+ executables:
77
+ - rspec-flake
78
+ extensions: []
79
+ extra_rdoc_files: []
80
+ files:
81
+ - LICENSE.txt
82
+ - README.md
83
+ - Rakefile
84
+ - exe/rspec-flake
85
+ - lib/rspec/flake/classifier.rb
86
+ - lib/rspec/flake/classifier/classify/classifier.rb
87
+ - lib/rspec/flake/classifier/classify/context.rb
88
+ - lib/rspec/flake/classifier/classify/result.rb
89
+ - lib/rspec/flake/classifier/cli.rb
90
+ - lib/rspec/flake/classifier/configuration.rb
91
+ - lib/rspec/flake/classifier/coverage_snapshot.rb
92
+ - lib/rspec/flake/classifier/deflaker.rb
93
+ - lib/rspec/flake/classifier/evaluation.rb
94
+ - lib/rspec/flake/classifier/example_history.rb
95
+ - lib/rspec/flake/classifier/features.rb
96
+ - lib/rspec/flake/classifier/formatter.rb
97
+ - lib/rspec/flake/classifier/integrations.rb
98
+ - lib/rspec/flake/classifier/predictor.rb
99
+ - lib/rspec/flake/classifier/probe_evidence.rb
100
+ - lib/rspec/flake/classifier/rerun/bisect_dependency_search.rb
101
+ - lib/rspec/flake/classifier/rerun/isolated_runner.rb
102
+ - lib/rspec/flake/classifier/rerun/protocol.rb
103
+ - lib/rspec/flake/classifier/rerun/result.rb
104
+ - lib/rspec/flake/classifier/runtime_controls.rb
105
+ - lib/rspec/flake/classifier/sensitivity.rb
106
+ - lib/rspec/flake/classifier/signature.rb
107
+ - lib/rspec/flake/classifier/store/jsonl_store.rb
108
+ - lib/rspec/flake/classifier/version.rb
109
+ - sig/rspec/flake/classifier.rbs
110
+ homepage: https://github.com/ydah/rspec-flake-classifier
111
+ licenses:
112
+ - MIT
113
+ metadata:
114
+ allowed_push_host: https://rubygems.org
115
+ homepage_uri: https://github.com/ydah/rspec-flake-classifier
116
+ source_code_uri: https://github.com/ydah/rspec-flake-classifier
117
+ rubygems_mfa_required: 'true'
118
+ rdoc_options: []
119
+ require_paths:
120
+ - lib
121
+ required_ruby_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: 3.2.0
126
+ required_rubygems_version: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ requirements: []
132
+ rubygems_version: 4.0.6
133
+ specification_version: 4
134
+ summary: Detect, record, and classify flaky RSpec failures.
135
+ test_files: []