rspec-flaky 0.0.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 +7 -0
- data/README.md +39 -0
- data/assets/layout.html.erb +149 -0
- data/bin/rspec-flaky +4 -0
- data/lib/rspec/flaky.rb +32 -0
- data/lib/rspec/flaky/cli.rb +64 -0
- data/lib/rspec/flaky/differ.rb +61 -0
- data/lib/rspec/flaky/drawer.rb +21 -0
- data/lib/rspec/flaky/dumper/db.rb +25 -0
- data/lib/rspec/flaky/dumper/json.rb +23 -0
- data/lib/rspec/flaky/pathes.rb +27 -0
- data/lib/rspec/flaky/tables.rb +22 -0
- data/lib/rspec/flaky/version.rb +5 -0
- metadata +71 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4d80b7e5a54c2a9d25326907dc0ff92304f410041c55e24d030bc99451370273
|
4
|
+
data.tar.gz: 6bf0165957822e3af4e57f477c18fac6f37cd756f275f78126265b085d365696
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 43df5f4540f9cde2c791cd194068dcc4b912dd63f1429d4121166469bac2a216426ab896cc97e946903c098cf3100355bc2592f82f23abce2691112c60b35fa0
|
7
|
+
data.tar.gz: ccddd3c02724b590f8c2798ad99c0488670719676afa6dc6aaae4f87e274f9058b925b9c8ecc48417139ba988c27b2ef0a0152f8e64b68442c199bec0a619f6c
|
data/README.md
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# RSpecFlaky
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to test group of your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'rspec-flaky'
|
10
|
+
|
11
|
+
Install the gem:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
And then add this line to your spec/spec_helper.rb:
|
16
|
+
|
17
|
+
require 'rspec/flaky'
|
18
|
+
|
19
|
+
Select the model whose attributes will be dumped:
|
20
|
+
|
21
|
+
it 'is flaky test', tables: [User, Post] do
|
22
|
+
expect([true, false]).to be true
|
23
|
+
end
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
Run the command to iteratively run flaky example:
|
28
|
+
|
29
|
+
rspec-flaky path/to/flaky_spec.rb:12 -i 10
|
30
|
+
|
31
|
+
If at least one example is failed you can compare pointed model's attributes dumped to tmp/flaky_test folder.
|
32
|
+
|
33
|
+
## Contributing
|
34
|
+
|
35
|
+
1. Fork it
|
36
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
37
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
38
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
39
|
+
5. Create new Pull Request
|
@@ -0,0 +1,149 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
+
<link href="application.css" rel='stylesheet' type='text/css' />
|
7
|
+
<title>Flaky Result</title>
|
8
|
+
<style>
|
9
|
+
html {
|
10
|
+
line-height: 1.15;
|
11
|
+
}
|
12
|
+
|
13
|
+
body {
|
14
|
+
margin: auto;
|
15
|
+
width: 80%;
|
16
|
+
}
|
17
|
+
|
18
|
+
|
19
|
+
h1 {
|
20
|
+
font-size: 2em;
|
21
|
+
margin: .67em 0
|
22
|
+
}
|
23
|
+
|
24
|
+
pre {
|
25
|
+
font-family: monospace, monospace;
|
26
|
+
font-size: 1em
|
27
|
+
}
|
28
|
+
|
29
|
+
html {
|
30
|
+
font-family: sans-serif
|
31
|
+
}
|
32
|
+
|
33
|
+
.pure-table {
|
34
|
+
border-collapse: collapse;
|
35
|
+
border-spacing: 0;
|
36
|
+
empty-cells: show;
|
37
|
+
border: 1px solid #cbcbcb
|
38
|
+
}
|
39
|
+
|
40
|
+
table {
|
41
|
+
table-layout: fixed;
|
42
|
+
width: 100%
|
43
|
+
}
|
44
|
+
|
45
|
+
.pure-table caption {
|
46
|
+
color: #000;
|
47
|
+
font: italic 85%/1 arial, sans-serif;
|
48
|
+
padding: 1em 0;
|
49
|
+
text-align: center
|
50
|
+
}
|
51
|
+
|
52
|
+
.pure-table td,
|
53
|
+
.pure-table th {
|
54
|
+
border-left: 1px solid #cbcbcb;
|
55
|
+
border-width: 0 0 0 1px;
|
56
|
+
font-size: inherit;
|
57
|
+
margin: 0;
|
58
|
+
overflow: visible;
|
59
|
+
padding: .5em 1em
|
60
|
+
}
|
61
|
+
|
62
|
+
.pure-table thead {
|
63
|
+
background-color: #e0e0e0;
|
64
|
+
color: #000;
|
65
|
+
text-align: left;
|
66
|
+
vertical-align: bottom
|
67
|
+
}
|
68
|
+
|
69
|
+
.pure-table td {
|
70
|
+
background-color: transparent;
|
71
|
+
max-width: 600px;
|
72
|
+
overflow: hidden;
|
73
|
+
}
|
74
|
+
|
75
|
+
.pure-table-odd td {
|
76
|
+
background-color: #f2f2f2
|
77
|
+
}
|
78
|
+
|
79
|
+
.pure-table-striped tr:nth-child(2n-1) td {
|
80
|
+
background-color: #f2f2f2
|
81
|
+
}
|
82
|
+
|
83
|
+
.pure-table-bordered td {
|
84
|
+
border-bottom: 1px solid #cbcbcb
|
85
|
+
}
|
86
|
+
|
87
|
+
.pure-table-bordered tbody>tr:last-child>td {
|
88
|
+
border-bottom-width: 0
|
89
|
+
}
|
90
|
+
|
91
|
+
.pure-table-horizontal td,
|
92
|
+
.pure-table-horizontal th {
|
93
|
+
border-width: 0 0 1px 0;
|
94
|
+
border-bottom: 1px solid #cbcbcb
|
95
|
+
}
|
96
|
+
|
97
|
+
.pure-table-horizontal tbody>tr:last-child>td {
|
98
|
+
border-bottom-width: 0
|
99
|
+
}
|
100
|
+
</style>
|
101
|
+
</head>
|
102
|
+
<body>
|
103
|
+
<% @diffs.each do |diffs| -%>
|
104
|
+
<div class="example">
|
105
|
+
<h2>Location: <%= diffs[:location] -%></h2>
|
106
|
+
<h2>Table: <%= diffs[:table] %></h2>
|
107
|
+
<% if diffs[:result] == "no_content" -%>
|
108
|
+
There were no failed and passed tests during the testing process. Try to increase the number of iterations
|
109
|
+
<% elsif diffs[:result] == "empty_table" -%>
|
110
|
+
Table is empty
|
111
|
+
<% else -%>
|
112
|
+
<p>Diffs:</p>
|
113
|
+
<% diffs[:result].each do |diff| -%>
|
114
|
+
<div>
|
115
|
+
<table class="pure-table">
|
116
|
+
<thead>
|
117
|
+
<th>
|
118
|
+
<%=diffs[:table]%>'s Attribute
|
119
|
+
</th>
|
120
|
+
<th>
|
121
|
+
Passed value
|
122
|
+
</th>
|
123
|
+
<th>
|
124
|
+
Failed value
|
125
|
+
</th>
|
126
|
+
</thead>
|
127
|
+
<tbody>
|
128
|
+
<% diff.each do |attribute| -%>
|
129
|
+
<tr>
|
130
|
+
<td>
|
131
|
+
<span><%= attribute[1] || "Record"%></span>
|
132
|
+
</td>
|
133
|
+
<td style="background-color: lightgreen">
|
134
|
+
<span><pre><%= prettify attribute[2]%></pre></span>
|
135
|
+
</td>
|
136
|
+
<td style="background-color: rosybrown">
|
137
|
+
<span><pre><%= prettify attribute[3]%></pre></span>
|
138
|
+
</td>
|
139
|
+
</tr>
|
140
|
+
<% end -%>
|
141
|
+
</tbody>
|
142
|
+
</table>
|
143
|
+
</div>
|
144
|
+
<% end -%>
|
145
|
+
<% end -%>
|
146
|
+
</div>
|
147
|
+
<%end -%>
|
148
|
+
</body>
|
149
|
+
</html>
|
data/bin/rspec-flaky
ADDED
data/lib/rspec/flaky.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'rspec/flaky/version'
|
2
|
+
require 'rspec/flaky/differ'
|
3
|
+
require 'rspec/flaky/tables'
|
4
|
+
require 'rspec/flaky/pathes'
|
5
|
+
require 'rspec/flaky/drawer'
|
6
|
+
require 'rspec'
|
7
|
+
require 'fileutils'
|
8
|
+
require 'open3'
|
9
|
+
|
10
|
+
module RSpec::Flaky
|
11
|
+
|
12
|
+
def self.run_spec locations, options
|
13
|
+
FileUtils.rm_rf(Pathes.summary_path)
|
14
|
+
rspec_options = options.delete(:rspec_options) || ""
|
15
|
+
options[:iterations].times do
|
16
|
+
if options[:silent]
|
17
|
+
Open3.capture2e("FLAKY_SPEC=1 rspec #{locations} #{rspec_options}")
|
18
|
+
else
|
19
|
+
system("FLAKY_SPEC=1 rspec #{locations} #{rspec_options}")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
Differ.get_result
|
23
|
+
Pathes.summary_path.children.each do |child|
|
24
|
+
if child.basename.to_s.start_with?(".:")
|
25
|
+
FileUtils.rm_rf(child) unless options[:save_jsons]
|
26
|
+
elsif child.basename.to_s == 'database_dumps'
|
27
|
+
FileUtils.rm_rf(child) unless options[:dump_db]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'rspec/flaky'
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
module Flaky
|
5
|
+
class CLI
|
6
|
+
|
7
|
+
DEFAULT_OPTIONS = {
|
8
|
+
iterations: 1,
|
9
|
+
silent: false,
|
10
|
+
save_jsons: false,
|
11
|
+
dump_db: false
|
12
|
+
}
|
13
|
+
|
14
|
+
def run argv
|
15
|
+
locations = get_location(argv) unless argv.any?{|arg| arg == '-h' || arg == '--help'}
|
16
|
+
options = parse_options(argv).reverse_merge(DEFAULT_OPTIONS)
|
17
|
+
options[:rspec_options] = extract_rspec_options argv
|
18
|
+
RSpec::Flaky.run_spec(locations, options)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def get_location(argv)
|
24
|
+
first_arg = argv.shift
|
25
|
+
raise 'You need to specify location first' unless Pathname.new(first_arg).exist?
|
26
|
+
return first_arg
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def parse_options argv
|
31
|
+
options = {}
|
32
|
+
OptionParser.new do |opts|
|
33
|
+
opts.banner = "Usage: rspec-flaky path/to/flaky_spec.rb:12 [options] -- [rspec options]"
|
34
|
+
|
35
|
+
opts.on("-i", "--iterations [NUMBER]", Integer, "Execute spec a given number of times") do |v|
|
36
|
+
options[:iterations] = v
|
37
|
+
end
|
38
|
+
|
39
|
+
opts.on("--silent", "Silent mode (no output)") do |v|
|
40
|
+
options[:silent] = v
|
41
|
+
end
|
42
|
+
opts.on("-j", "--jsons", "Save pointed models attributes") do |v|
|
43
|
+
options[:save_jsons] = v
|
44
|
+
end
|
45
|
+
opts.on("-d", "--dump", "Dump database to sql-file after each example") do |v|
|
46
|
+
options[:dump_db] = v
|
47
|
+
end
|
48
|
+
|
49
|
+
opts.on('-h', '--help', 'Displays Help') do
|
50
|
+
puts opts
|
51
|
+
exit
|
52
|
+
end
|
53
|
+
end.parse!(argv)
|
54
|
+
options
|
55
|
+
end
|
56
|
+
|
57
|
+
def extract_rspec_options argv
|
58
|
+
idx = argv.index('--') || -1
|
59
|
+
rspec_options = argv[idx+1..-1]
|
60
|
+
return if rspec_options.empty?
|
61
|
+
rspec_options.compact.join(' ')
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'hashdiff'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module Differ
|
5
|
+
|
6
|
+
EXPECTED_CONTENT = %w(failed.json passed.json)
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def get_result
|
10
|
+
create_summary_folder
|
11
|
+
@diffs = []
|
12
|
+
Pathes.summary_path.children.each do |example_dir|
|
13
|
+
next unless example_dir.directory?
|
14
|
+
next unless example_dir.basename.to_s.start_with?(".:")
|
15
|
+
example_dir.children.select do |tables_dir|
|
16
|
+
next unless tables_dir.directory?
|
17
|
+
@diffs << {
|
18
|
+
location: Pathes.relative_path(example_dir),
|
19
|
+
table: tables_dir.basename.to_s,
|
20
|
+
result: get_diffs(tables_dir)
|
21
|
+
}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
Drawer.draw @diffs
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_summary_folder
|
28
|
+
FileUtils.mkdir_p(Pathes.summary_path)
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_diffs tables_dir
|
32
|
+
return 'no_content' unless contains_passed_and_failed_jsons? tables_dir
|
33
|
+
result = []
|
34
|
+
jsons = tables_dir.children.select { |child| EXPECTED_CONTENT.include? child.basename.to_s }
|
35
|
+
jsons = read_jsons(jsons)
|
36
|
+
|
37
|
+
return 'empty_table' if jsons.values.all? &:empty?
|
38
|
+
jsons.values.map(&:length).max.times do |idx|
|
39
|
+
passed_record = jsons["passed"].try(:[], idx) || {}
|
40
|
+
failed_record = jsons["failed"].try(:[], idx) || {}
|
41
|
+
result << Hashdiff.diff(passed_record, failed_record)
|
42
|
+
end
|
43
|
+
result
|
44
|
+
end
|
45
|
+
|
46
|
+
def read_jsons pathes
|
47
|
+
{}.tap do |hash|
|
48
|
+
pathes.each do |path|
|
49
|
+
hash[path.basename.to_s.split('.')[0]] = JSON.parse(File.read(Pathes.base_path.join(path)))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def contains_passed_and_failed_jsons? tables_dir
|
55
|
+
actual_content = tables_dir.children.map(&:basename).map(&:to_s)
|
56
|
+
EXPECTED_CONTENT & actual_content == EXPECTED_CONTENT
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Drawer
|
2
|
+
|
3
|
+
class << self
|
4
|
+
def draw diffs
|
5
|
+
@diffs = diffs
|
6
|
+
erb_str = File.read(Pathes.template_path)
|
7
|
+
result = ERB.new(erb_str, nil, '-').result(binding)
|
8
|
+
File.open(Pathes.summary_path.join('result.html'), 'w') do |f|
|
9
|
+
f.write(result)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def prettify(data)
|
14
|
+
return '-' if data.nil?
|
15
|
+
return JSON.pretty_generate(data) if data.is_a? Hash
|
16
|
+
data
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rails'
|
2
|
+
|
3
|
+
module Dumper
|
4
|
+
class Db
|
5
|
+
|
6
|
+
def dump!(location, status)
|
7
|
+
path = dump_path(location)
|
8
|
+
FileUtils.mkdir_p(path) unless File.exists?(path)
|
9
|
+
#TODO adapter for mysql and sqlite3
|
10
|
+
#TODO username from config
|
11
|
+
system "pg_dump -U postgres -d #{db_name} > #{path}/#{status}.sql"
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def db_name
|
17
|
+
Rails.configuration.database_configuration["test"]["database"]
|
18
|
+
end
|
19
|
+
|
20
|
+
def dump_path location
|
21
|
+
"tmp/flaky_tests/database_dumps/#{location}"
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module Dumper
|
5
|
+
class Json
|
6
|
+
|
7
|
+
def dump!(location, status, tables)
|
8
|
+
tables.each do |table|
|
9
|
+
json = JSON.pretty_generate(table.all.map(&:attributes))
|
10
|
+
FileUtils.mkdir_p(dump_path(location, table)) unless File.exists?(dump_path(location, table))
|
11
|
+
File.open("#{dump_path(location, table)}/#{status}.json", 'w') do |f|
|
12
|
+
f.write(json)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def dump_path location, table
|
20
|
+
"tmp/flaky_tests/#{location}/#{table}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Pathes
|
2
|
+
|
3
|
+
class << self
|
4
|
+
|
5
|
+
def template_path
|
6
|
+
assets_path.join("layout.html.erb")
|
7
|
+
end
|
8
|
+
|
9
|
+
def summary_path
|
10
|
+
base_path.join('tmp/flaky_tests')
|
11
|
+
end
|
12
|
+
|
13
|
+
def base_path
|
14
|
+
Pathname.new(FileUtils.pwd)
|
15
|
+
end
|
16
|
+
|
17
|
+
def relative_path path
|
18
|
+
path.relative_path_from(summary_path).to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
def assets_path
|
22
|
+
Pathname.new(__dir__).join("../../../assets/")
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
require 'rspec/flaky/dumper/json'
|
3
|
+
require 'rspec/flaky/dumper/db'
|
4
|
+
|
5
|
+
RSpec.configure do |config|
|
6
|
+
if ENV["FLAKY_SPEC"] == "1"
|
7
|
+
config.before(:suite, table: true) do
|
8
|
+
config.after(:each, tables: true) do |example|
|
9
|
+
location = example.metadata[:location].gsub('/', ':')
|
10
|
+
status = example.exception.nil? ? 'passed' : 'failed'
|
11
|
+
tables = Array.wrap(example.metadata[:tables].select{|t| t < ApplicationRecord})
|
12
|
+
|
13
|
+
Dumper::Json.new.dump!(location, status, tables)
|
14
|
+
Dumper::Db.new.dump!(location, status)
|
15
|
+
end
|
16
|
+
|
17
|
+
config.before(:each, tables: true) do
|
18
|
+
DatabaseCleaner.strategy = :truncation
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rspec-flaky
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- maratz
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-12-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: hashdiff
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.0.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.0.1
|
27
|
+
description: Gem wraps every runned example to dump pointed database tables to json
|
28
|
+
files
|
29
|
+
email:
|
30
|
+
- mzasorinwd@gmail.com
|
31
|
+
executables:
|
32
|
+
- rspec-flaky
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files: []
|
35
|
+
files:
|
36
|
+
- README.md
|
37
|
+
- assets/layout.html.erb
|
38
|
+
- bin/rspec-flaky
|
39
|
+
- lib/rspec/flaky.rb
|
40
|
+
- lib/rspec/flaky/cli.rb
|
41
|
+
- lib/rspec/flaky/differ.rb
|
42
|
+
- lib/rspec/flaky/drawer.rb
|
43
|
+
- lib/rspec/flaky/dumper/db.rb
|
44
|
+
- lib/rspec/flaky/dumper/json.rb
|
45
|
+
- lib/rspec/flaky/pathes.rb
|
46
|
+
- lib/rspec/flaky/tables.rb
|
47
|
+
- lib/rspec/flaky/version.rb
|
48
|
+
homepage:
|
49
|
+
licenses:
|
50
|
+
- MIT
|
51
|
+
metadata: {}
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
requirements: []
|
67
|
+
rubygems_version: 3.0.3
|
68
|
+
signing_key:
|
69
|
+
specification_version: 4
|
70
|
+
summary: Gem for catching flaky specs
|
71
|
+
test_files: []
|