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.
@@ -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
@@ -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>
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rspec/flaky/cli'
3
+
4
+ Flaky::CLI.new.run(ARGV)
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ module RSpecFlaky
3
+ VERSION = '0.0.1'
4
+ end
5
+
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: []