flores 0.0.2 → 0.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 93d262135443a719e7d26ac03f57cc016d63a216
4
- data.tar.gz: d92cf5cb04a2553c1369948f2cd13d452bee6bff
3
+ metadata.gz: c0fb409bcd13936e24dae9b9f932577b83321cfa
4
+ data.tar.gz: 0168c710951ccd26e4bff91434d4e811526cabe3
5
5
  SHA512:
6
- metadata.gz: 72e01594ea803e4fc901cb5e9e825524045cc9812cf9b7f2d1430363f8733907b1a5949677e902159198056af873c2ec677f392c41dd56f9cef01c3a4126ad0a
7
- data.tar.gz: 0e9b10da498eee2d17b2f3e7557f61e151c1adb2fa08a14ed7e4094cd7a38c4b15b83ee8516cb59d1734b2a145c0c52edad65d93ca0e34f407dc94ce2c71c8ed
6
+ metadata.gz: 9da3201822ace87268b2cd900d75c95ee7b9d02ac3b7801b5456039d3fc55e8a35739e8f88ff40524f4db7ba3c6bf6036967d652d2d94f19181eb4e67fdb3891
7
+ data.tar.gz: 1d79f887fbbeeb0cd4f641b31382b074fc20a3e7e2b6a3da55bfde4d35dbb491ef51463869981e9e53c862b506e4785182359cb2c084d64ca6d7ecedde0c603a
data/Gemfile CHANGED
@@ -2,4 +2,6 @@ source "https://rubygems.org"
2
2
 
3
3
  group "development" do
4
4
  gem "rspec", ">= 3.0.0"
5
+ gem "fuubar"
6
+ gem "pry"
5
7
  end
data/Gemfile.lock CHANGED
@@ -1,7 +1,22 @@
1
1
  GEM
2
2
  remote: https://rubygems.org/
3
3
  specs:
4
+ coderay (1.1.0)
4
5
  diff-lcs (1.2.5)
6
+ ffi (1.9.6-java)
7
+ fuubar (2.0.0)
8
+ rspec (~> 3.0)
9
+ ruby-progressbar (~> 1.4)
10
+ method_source (0.8.2)
11
+ pry (0.10.1)
12
+ coderay (~> 1.1.0)
13
+ method_source (~> 0.8.1)
14
+ slop (~> 3.4)
15
+ pry (0.10.1-java)
16
+ coderay (~> 1.1.0)
17
+ method_source (~> 0.8.1)
18
+ slop (~> 3.4)
19
+ spoon (~> 0.0)
5
20
  rspec (3.2.0)
6
21
  rspec-core (~> 3.2.0)
7
22
  rspec-expectations (~> 3.2.0)
@@ -15,9 +30,16 @@ GEM
15
30
  diff-lcs (>= 1.2.0, < 2.0)
16
31
  rspec-support (~> 3.2.0)
17
32
  rspec-support (3.2.1)
33
+ ruby-progressbar (1.7.1)
34
+ slop (3.6.0)
35
+ spoon (0.0.4)
36
+ ffi
18
37
 
19
38
  PLATFORMS
39
+ java
20
40
  ruby
21
41
 
22
42
  DEPENDENCIES
43
+ fuubar
44
+ pry
23
45
  rspec (>= 3.0.0)
data/Makefile CHANGED
@@ -29,3 +29,15 @@ install: $(GEM)
29
29
  .PHONY: clean
30
30
  clean:
31
31
  -rm -rf .yardoc $(GEM) $(NAME)-$(VERSION)/
32
+
33
+ .PHONY: rubocop
34
+ rubocop:
35
+ rubocop -D
36
+
37
+ .PHONY: test
38
+ test: rubocop
39
+ rspec -f Flores::RSpec::Formatters::Analyze
40
+
41
+ .PHONY: test
42
+ test-fast: rubocop
43
+ ITERATIONS=10 rspec -f Flores::RSpec::Formatters::Analyze
data/README.md ADDED
@@ -0,0 +1,177 @@
1
+ # Flores - a stress testing library
2
+
3
+ When writing tests, it is often good to test a wide variety of inputs to ensure
4
+ your entire input range behaves correctly.
5
+
6
+ Further, adding a bit of randomness in your tests can help find bugs.
7
+
8
+ ## Why Flores?
9
+
10
+ Randomization helps you cover a wider range of inputs to your tests to find bugs. Stress
11
+ testing (run a test repeatedly) helps you find bugs faster. We can use stress testing results
12
+ to find common patterns in failures!
13
+
14
+ Let's look at a sample situation. Ruby's TCPServer. Let's write a spec to cover a spec covering port binding:
15
+
16
+ ```ruby
17
+ describe TCPServer do
18
+ subject(:socket) { Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0) }
19
+ let(:port) { 5000 }
20
+ let(:sockaddr) { Socket.sockaddr_in(port, "127.0.0.1") }
21
+
22
+ after { socket.close}
23
+
24
+ it "should bind successfully" do
25
+ socket.bind(sockaddr)
26
+ expect(socket.local_address.ip_port).to(be == port)
27
+ end
28
+ end
29
+ ```
30
+
31
+ Running it:
32
+
33
+ ```
34
+ % rspec tcpserver_spec.rb
35
+ .
36
+
37
+ Finished in 0.00248 seconds (files took 0.16294 seconds to load)
38
+ 1 example, 0 failures
39
+ ```
40
+
41
+ That's cool. We now have some confidence that TCPServer on port 5000 will bind successfully.
42
+
43
+ What about the other ports? What ranges of values should work? What shouldn't?
44
+
45
+ Let's assume I don't know anything about tcp port ranges and test randomly in the range -100,000 to +100,000:
46
+
47
+ ```ruby
48
+ describe TCPServer do
49
+ let(:port) { Randomized.integer(-100_000..100_000) }
50
+ ...
51
+ end
52
+ ```
53
+
54
+ Running it:
55
+
56
+ ```
57
+ % rspec tcpserver_spec.rb
58
+ F
59
+
60
+ Failures:
61
+
62
+ 1) TCPServer should bind successfully
63
+ Failure/Error: expect(socket.local_address.ip_port).to(be == port)
64
+ expected: == 83359
65
+ got: 17823
66
+ # ./tcpserver_spec.rb:12:in `block (2 levels) in <top (required)>'
67
+
68
+ Finished in 0.00155 seconds (files took 0.10221 seconds to load)
69
+ 1 example, 1 failure
70
+ ```
71
+
72
+ Well that's weird. Binding port 83359 actually made it bind on port 17823!
73
+
74
+ If we run it more times, we'll see all kinds of different results:
75
+
76
+ * Run 1:
77
+ ```
78
+ Failure/Error: expect(socket.local_address.ip_port).to(be == port)
79
+ expected: == 83359
80
+ got: 17823
81
+ ```
82
+ * Run 2:
83
+ ```
84
+ Failure/Error: let(:sockaddr) { Socket.sockaddr_in(port, "127.0.0.1") }
85
+ SocketError:
86
+ getaddrinfo: nodename nor servname provided, or not known
87
+ ```
88
+ * Run 3:
89
+ ```
90
+ Errno::EACCES:
91
+ Permission denied - bind(2) for 127.0.0.1:615
92
+ ```
93
+ * Run 4:
94
+ ```
95
+ Finished in 0.00161 seconds (files took 0.10356 seconds to load)
96
+ 1 example, 0 failures
97
+ ```
98
+
99
+ ## Analyze the results
100
+
101
+ The above example showed that there were many different kinds of failures when
102
+ we introduced randomness to our test inputs.
103
+
104
+ We can go further and run a given spec example many times and group the
105
+ failures by similarity and include context (what the inputs were, etc)
106
+
107
+ This library provides an `analyze_it` helper which behaves similarly to rspec's
108
+ `it` except that it runs the block a random number of times and clears the `let` cache
109
+ each time. This lets you run a given test many times with many random inputs!
110
+
111
+ The result is grouped by failure and includes context. Let's see how it works:
112
+
113
+ We'll change `it` to use `analyze_it` instead:
114
+
115
+ ```diff
116
+ - it "should bind successfully" do
117
+ + analyze_it "should bind successfully", [:port] do
118
+ ```
119
+
120
+ Now rerunning the test. With barely any spec changes from the original, we have
121
+ now enough randomness and stress testing to identify many different failure cases
122
+ and input ranges for those failures.
123
+
124
+ ```
125
+ Failures:
126
+
127
+ 1) TCPServer should bind successfully
128
+ Failure/Error: raise StandardError, Analysis.new(results) if results.any? { |k, _| k != :success }
129
+ StandardError:
130
+ 31.14% tests successful of 2563 tests
131
+ Failure analysis:
132
+ 50.57% -> [1296] SocketError
133
+ Sample exception for {:port=>-94900}
134
+ getaddrinfo: nodename nor servname provided, or not known
135
+ Samples causing SocketError:
136
+ {:port=>-49441}
137
+ {:port=>-1991}
138
+ {:port=>-54074}
139
+ {:port=>-1733}
140
+ {:port=>-21868}
141
+ 16.89% -> [433] RSpec::Expectations::ExpectationNotMetError
142
+ Sample exception for {:port=>93844}
143
+ expected: == 93844
144
+ got: 28308
145
+ Samples causing RSpec::Expectations::ExpectationNotMetError:
146
+ {:port=>89451}
147
+ {:port=>95627}
148
+ {:port=>95225}
149
+ {:port=>73106}
150
+ {:port=>77167}
151
+ 1.01% -> [26] Errno::EACCES
152
+ Sample exception for {:port=>65649}
153
+ Permission denied - bind(2) for 127.0.0.1:113
154
+ Samples causing Errno::EACCES:
155
+ {:port=>913}
156
+ {:port=>141}
157
+ {:port=>66194}
158
+ {:port=>66217}
159
+ {:port=>66408}
160
+ 0.39% -> [10] Errno::EADDRINUSE
161
+ Sample exception for {:port=>34402}
162
+ Address already in use - bind(2) for 127.0.0.1:34402
163
+ Samples causing Errno::EADDRINUSE:
164
+ {:port=>50905}
165
+ {:port=>71202}
166
+ {:port=>34402}
167
+ {:port=>28235}
168
+ {:port=>85641}
169
+ # ./lib/rspec/stress_it.rb:103:in `block in analyze_it'
170
+
171
+ Finished in 0.0735 seconds (files took 0.10247 seconds to load)
172
+ 1 example, 1 failure
173
+
174
+ Failed examples:
175
+
176
+ rspec ./tcpserver_spec.rb:8 # TCPServer should bind successfully
177
+ ```
@@ -1,3 +1,4 @@
1
+ # encoding: utf-8
1
2
  # This file is part of ruby-flores.
2
3
  # Copyright (C) 2015 Jordan Sissel
3
4
  #
@@ -13,18 +14,20 @@
13
14
  #
14
15
  # You should have received a copy of the GNU Affero General Public License
15
16
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
- #
17
- # encoding: utf-8
18
- require "randomized"
19
- require "rspec/stress_it"
17
+ require "flores/rspec"
18
+ require "flores/random"
20
19
 
21
20
  RSpec.configure do |c|
22
- c.extend RSpec::StressIt
21
+ Flores::RSpec.configure(c)
22
+ c.add_formatter("Flores::RSpec::Formatters::Analyze")
23
23
  end
24
24
 
25
- describe "number" do
26
- let(:number) { Randomized.number(0..200) }
27
- analyze_it "should be less than 100", [:number] do
28
- expect(number).to(be < 100)
25
+ describe "a random number" do
26
+ context "between 0 and 200 inclusive" do
27
+ let(:number) { Flores::Random.number(0..200) }
28
+ analyze_results
29
+ stress_it "should be less than 100" do
30
+ expect(number).to(be < 100)
31
+ end
29
32
  end
30
33
  end
@@ -1,3 +1,4 @@
1
+ # encoding: utf-8
1
2
  # This file is part of ruby-flores.
2
3
  # Copyright (C) 2015 Jordan Sissel
3
4
  #
@@ -13,14 +14,12 @@
13
14
  #
14
15
  # You should have received a copy of the GNU Affero General Public License
15
16
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
- #
17
- # encoding: utf-8
18
- require "randomized"
17
+ require "flores/random"
19
18
  require "socket"
20
- require "rspec/stress_it"
19
+ require "flores/rspec"
21
20
 
22
21
  RSpec.configure do |c|
23
- c.extend RSpec::StressIt
22
+ Flores::RSpec.configure(c)
24
23
  end
25
24
 
26
25
  # A factory for encapsulating behavior of a tcp server and client for the
@@ -62,12 +61,12 @@ class TCPIntegrationTestFactory
62
61
  end
63
62
 
64
63
  describe "TCPServer+TCPSocket" do
65
- let(:port) { Randomized.integer(1024..65535) }
66
- let(:text) { Randomized.text(1..2000) }
64
+ let(:port) { Flores::Random.integer(1024..65535) }
65
+ let(:text) { Flores::Random.text(1..2000) }
67
66
  subject { TCPIntegrationTestFactory.new(port) }
68
67
 
69
68
  describe "using stress_it" do
70
- analyze_it "should send data correctly", [:port, :text] do
69
+ stress_it2 "should send data correctly", [:port, :text] do
71
70
  begin
72
71
  subject.setup
73
72
  rescue Errno::EADDRINUSE
@@ -1,3 +1,4 @@
1
+ # encoding: utf-8
1
2
  # This file is part of ruby-flores.
2
3
  # Copyright (C) 2015 Jordan Sissel
3
4
  #
@@ -13,26 +14,27 @@
13
14
  #
14
15
  # You should have received a copy of the GNU Affero General Public License
15
16
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
- #
17
- # encoding: utf-8
18
- require "randomized"
17
+ require "flores/random"
19
18
  require "socket"
20
- require "rspec/stress_it"
19
+ require "flores/rspec"
21
20
 
22
21
  RSpec.configure do |c|
23
- c.extend RSpec::StressIt
22
+ Flores::RSpec.configure(c)
24
23
  end
25
24
 
26
25
  describe TCPServer do
27
26
  subject(:socket) { Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0) }
28
27
  let(:sockaddr) { Socket.sockaddr_in(port, "127.0.0.1") }
29
- after { socket.close }
30
28
 
31
29
  context "on a random port" do
32
- let(:port) { Randomized.integer(-100_000..100_000) }
33
- analyze_it "should bind successfully", [:port] do
34
- socket.bind(sockaddr)
35
- expect(socket.local_address.ip_port).to(be == port)
30
+ let(:port) { Flores::Random.integer(-100_000..100_000) }
31
+ stress_it2 "should bind successfully", [:port] do
32
+ begin
33
+ socket.bind(sockaddr)
34
+ expect(socket.local_address.ip_port).to(be == port)
35
+ ensure
36
+ socket.close
37
+ end
36
38
  end
37
39
  end
38
40
  end
@@ -1,3 +1,4 @@
1
+ # encoding: utf-8
1
2
  # This file is part of ruby-flores.
2
3
  # Copyright (C) 2015 Jordan Sissel
3
4
  #
@@ -13,17 +14,17 @@
13
14
  #
14
15
  # You should have received a copy of the GNU Affero General Public License
15
16
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
- #
17
- # encoding: utf-8
18
- require "randomized"
17
+ require "flores/random"
19
18
  require "socket"
20
- require "rspec/stress_it"
19
+ require "flores/rspec"
21
20
 
22
21
  RSpec.configure do |c|
23
- c.extend RSpec::StressIt
22
+ Flores::RSpec.configure(c)
24
23
  end
25
24
 
26
25
  describe TCPServer do
26
+ analyze
27
+
27
28
  subject(:socket) { Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0) }
28
29
  let(:sockaddr) { Socket.sockaddr_in(port, "127.0.0.1") }
29
30
  let(:ignore_eaddrinuse) do
@@ -41,14 +42,14 @@ describe TCPServer do
41
42
  end
42
43
 
43
44
  context "on privileged ports" do
44
- let(:port) { Randomized.integer(1..1023) }
45
+ let(:port) { Flores::Random.integer(1..1023) }
45
46
  stress_it "should raise Errno::EACCESS" do
46
47
  expect { socket.bind(sockaddr) }.to(raise_error(Errno::EACCES))
47
48
  end
48
49
  end
49
50
 
50
51
  context "on unprivileged ports" do
51
- let(:port) { Randomized.integer(1025..65535) }
52
+ let(:port) { Flores::Random.integer(1025..65535) }
52
53
  stress_it "should bind on a port" do
53
54
  # EADDRINUSE is expected since we are picking ports at random
54
55
  # Let's ignore this specific exception
data/flores.gemspec CHANGED
@@ -2,7 +2,7 @@ Gem::Specification.new do |spec|
2
2
  files = %x(git ls-files).split("\n")
3
3
 
4
4
  spec.name = "flores"
5
- spec.version = "0.0.2"
5
+ spec.version = "0.0.3"
6
6
  spec.summary = "Fuzz, randomize, and stress your tests"
7
7
  spec.description = <<-DESCRIPTION
8
8
  Add fuzzing, randomization, and stress to your tests.
@@ -0,0 +1,23 @@
1
+ # encoding: utf-8
2
+ # This file is part of ruby-flores.
3
+ # Copyright (C) 2015 Jordan Sissel
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU Affero General Public License as
7
+ # published by the Free Software Foundation, either version 3 of the
8
+ # License, or (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU Affero General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Affero General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ # :nodoc:
18
+ module Flores # rubocop:disable Style/ClassAndModuleChildren
19
+ module RSpec # rubocop:disable Style/ClassAndModuleChildren
20
+ module Formatters # rubocop:disable Style/ClassAndModuleChildren
21
+ end
22
+ end
23
+ end
@@ -1,3 +1,4 @@
1
+ # encoding: utf-8
1
2
  # This file is part of ruby-flores.
2
3
  # Copyright (C) 2015 Jordan Sissel
3
4
  #
@@ -13,11 +14,11 @@
13
14
  #
14
15
  # You should have received a copy of the GNU Affero General Public License
15
16
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
- #
17
- # encoding: utf-8
17
+
18
+ require "flores/namespace"
18
19
 
19
20
  # A collection of methods intended for use in randomized testing.
20
- module Randomized
21
+ module Flores::Random
21
22
  # A selection of UTF-8 characters
22
23
  #
23
24
  # I'd love to generate this, but I don't yet know enough about how unicode
@@ -43,7 +44,7 @@ module Randomized
43
44
  # * Negative lengths are not permitted and will raise an ArgumentError
44
45
  #
45
46
  # @param length [Fixnum or Range] the length of text to generate
46
- # @return [String] the
47
+ # @return [String] the generated text
47
48
  def self.text(length)
48
49
  return text_range(length) if length.is_a?(Range)
49
50
 
@@ -51,6 +52,10 @@ module Randomized
51
52
  length.times.collect { character }.join
52
53
  end # def text
53
54
 
55
+ # Generate text with random characters of a length within the given range.
56
+ #
57
+ # @param range [Range] the range of length to generate, inclusive
58
+ # @return [String] the generated text
54
59
  def self.text_range(range)
55
60
  raise ArgumentError, "Requires ascending range, you gave #{range}." if range.end < range.begin
56
61
  raise ArgumentError, "A negative range values are not permitted, I received range #{range}" if range.begin < 0
@@ -77,9 +82,9 @@ module Randomized
77
82
  # @param range [Range]
78
83
  def self.number(range)
79
84
  raise ArgumentError, "Range not given, got #{range.class}: #{range.inspect}" if !range.is_a?(Range)
80
- # Range#size returns the number of elements in the range, not the length of the range.
81
- # This makes #size return (abs(range.begin - range.end) + 1), and we want the length, so subtract 1.
82
- rand * (range.size - 1) + range.begin
85
+ # Ruby 1.9.3 and below do not have Enumerable#size, so we have to compute the size of the range
86
+ # ourselves.
87
+ rand * (range.end - range.begin) + range.begin
83
88
  end # def number
84
89
 
85
90
  # Run a block a random number of times.
@@ -1,3 +1,4 @@
1
+ # encoding: utf-8
1
2
  # This file is part of ruby-flores.
2
3
  # Copyright (C) 2015 Jordan Sissel
3
4
  #
@@ -13,9 +14,8 @@
13
14
  #
14
15
  # You should have received a copy of the GNU Affero General Public License
15
16
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
- #
17
- # encoding: utf-8
18
- require "rspec/core"
17
+ require "flores/namespace"
18
+ require "flores/rspec"
19
19
 
20
20
  # RSpec helpers for stress testing examples
21
21
  #
@@ -26,83 +26,53 @@ require "rspec/core"
26
26
  # end
27
27
  #
28
28
  # TODO(sissel): Show an example of stress_it and analyze_it
29
- module RSpec::StressIt
30
- DEFAULT_ITERATIONS = 1..5000
31
-
32
- # Wraps `it` and runs the block many times. Each run has will clear the `let` cache.
29
+ module Flores::RSpec::Analyze
30
+ # Save state after each example so it can be used in analysis after specs are completed.
33
31
  #
34
- # The intent of this is to allow randomized testing for fuzzing and stress testing
35
- # of APIs to help find edge cases and weird behavior.
32
+ # If you use this, you'll want to set your RSpec formatter to
33
+ # Flores::RSpec::Formatter::Analyze
36
34
  #
37
- # The default number of iterations is randomly selected between 1 and 1000 inclusive
38
- def stress_it(name, options = {}, &block)
39
- stress__iterations = Randomized.iterations(options.delete(:stress_iterations) || DEFAULT_ITERATIONS)
40
- it(name, options) do
41
- # Run the block of an example many times
42
- stress__iterations.each do
43
- # Run the block within 'it' scope
44
- instance_eval(&block)
45
-
46
- # clear the internal rspec `let` cache this lets us run a test
47
- # repeatedly with fresh `let` evaluations.
48
- # Reference: https://github.com/rspec/rspec-core/blob/5fc29a15b9af9dc1c9815e278caca869c4769767/lib/rspec/core/memoized_helpers.rb#L124-L127
49
- __memoized.clear
50
- end
51
- end # it ...
52
- end # def stress_it
53
-
54
- # Generate a random number of copies of a given example.
55
- # The idea is to take 1 `it` and run it N times to help tease out failures.
56
- # Of course, the teasing requires you have randomized `let` usage, for example:
35
+ # Let's show an example that fails sometimes.
57
36
  #
58
- # let(:number) { Randomized.number(0..200) }
59
- # it "should be less than 100" do
60
- # expect(number).to(be < 100)
37
+ # describe "Addition of two numbers" do
38
+ # context "positive numbers" do
39
+ # analyze_results
40
+ # let(:a) { Flores::Random.number(1..1000) }
41
+ #
42
+ # # Here we make negative numbers possible to cause failure in our test.
43
+ # let(:b) { Flores::Random.number(-200..1000) }
44
+ # subject { a + b }
45
+ #
46
+ # stress_it "should be positive" do
47
+ # expect(subject).to(be > 0)
48
+ # end
49
+ # end
61
50
  # end
62
- def stress_it2(name, options = {}, &block)
63
- stress__iterations = Randomized.iterations(options.delete(:stress_iterations) || DEFAULT_ITERATIONS)
64
- stress__iterations.each do |i|
65
- it(name + " [#{i}]", *args) do
66
- instance_eval(&block)
67
- end # it ...
68
- end # .times
69
- end
70
-
71
- # Perform analysis on failure scenarios of a given example
72
- #
73
- # This will run the given example a random number of times and aggregate the
74
- # results. If any failures occur, the spec will fail and a report will be
75
- # given on that test.
76
51
  #
77
- # Example spec:
78
- #
79
- # let(:number) { Randomized.number(0..200) }
80
- # fuzz "should be less than 100" do
81
- # expect(number).to(be < 100)
82
- # end
52
+ # And running it:
83
53
  #
84
- # Example report:
85
- def analyze_it(name, variables, &block) # rubocop:disable Metrics/AbcSize
86
- it(name) do
87
- results = Hash.new { |h, k| h[k] = [] }
88
- Randomized.iterations(DEFAULT_ITERATIONS).each do
89
- state = Hash[variables.collect { |l| [l, __send__(l)] }]
90
- begin
91
- instance_eval(&block)
92
- results[:success] << [state, nil]
93
- rescue => e
94
- results[e.class] << [state, e]
95
- rescue Exception => e # rubocop:disable Lint/RescueException
96
- results[e.class] << [state, e]
97
- end
98
-
99
- # Clear `let` memoizations
100
- __memoized.clear
101
- end
102
-
103
- raise StandardError, Analysis.new(results) if results.any? { |k, _| k != :success }
54
+ # % rspec -f Flores::RSpec::Formatter::Analyze
55
+ # Addition of two numbers positive numbers should be positive
56
+ # 98.20% tests successful of 3675 tests
57
+ # Failure analysis:
58
+ # 1.80% -> [66] RSpec::Expectations::ExpectationNotMetError
59
+ # Sample exception for {:a=>126.21705882478048, :b=>-139.54814492675024, :subject=>-13.33108610196976}
60
+ # expected: > 0
61
+ # got: -13.33108610196976
62
+ # Samples causing RSpec::Expectations::ExpectationNotMetError:
63
+ # {:a=>90.67298249206425, :b=>-136.6237821353908, :subject=>-45.95079964332655}
64
+ # {:a=>20.35865155878871, :b=>-39.592417377658876, :subject=>-19.233765818870165}
65
+ # {:a=>158.07905166101787, :b=>-177.5864470909581, :subject=>-19.50739542994023}
66
+ # {:a=>31.80445518715138, :b=>-188.51942190504894, :subject=>-156.71496671789757}
67
+ # {:a=>116.1479954937354, :b=>-146.18477887927958, :subject=>-30.036783385544183}
68
+ def analyze_results
69
+ # TODO(sissel): Would be lovely to figure out how to inject an 'after' for
70
+ # all examples if we are using the Analyze formatter.
71
+ # Then this method could be implied by using the right formatter, or something.
72
+ after do |example|
73
+ example.metadata[:values] = __memoized.clone
104
74
  end
105
- end # def analyze_it
75
+ end
106
76
 
107
77
  # A formatter to show analysis of an `analyze_it` example.
108
78
  class Analysis < StandardError
@@ -170,4 +140,4 @@ module RSpec::StressIt
170
140
  ]
171
141
  end # def error_sample_states
172
142
  end # class Analysis
173
- end # module RSpec::StressIt
143
+ end # Flores::RSpec::Analyze
@@ -0,0 +1,61 @@
1
+ # encoding: utf-8
2
+ # This file is part of ruby-flores.
3
+ # Copyright (C) 2015 Jordan Sissel
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU Affero General Public License as
7
+ # published by the Free Software Foundation, either version 3 of the
8
+ # License, or (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU Affero General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Affero General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ require "flores/namespace"
18
+ require "rspec/core/formatters/base_text_formatter"
19
+
20
+ Flores::RSpec::Formatters::Analyze = Class.new(RSpec::Core::Formatters::BaseTextFormatter) do
21
+ RSpec::Core::Formatters.register self, :dump_failures, :dump_summary
22
+
23
+ def dump_summary(event)
24
+ # The event is an RSpec::Core::Notifications::SummaryNotification
25
+ # Let's mimic the BaseTextFormatter but without the failing test report
26
+ output.puts
27
+ output.puts "Finished in #{event.formatted_duration}"
28
+ output.puts "#{event.colorized_totals_line}"
29
+ end
30
+
31
+ def dump_failures(event)
32
+ output.puts
33
+ group = event.examples.each_with_object(Hash.new { |h, k| h[k] = [] }) do |e, m|
34
+ m[e.metadata[:full_description]] << e
35
+ m
36
+ end
37
+ group.each { |description, examples| dump_example_summary(description, examples) }
38
+ end
39
+
40
+ def dump_example_summary(description, examples)
41
+ output.puts description
42
+ analysis = Flores::RSpec::Analyze::Analysis.new(group_by_result(examples))
43
+ output.puts(analysis.to_s.gsub(/^/, " "))
44
+ end
45
+
46
+ def group_by_result(examples) # rubocop:disable Metrics/AbcSize
47
+ examples.each_with_object(Hash.new { |h, k| h[k] = [] }) do |example, results|
48
+ if example.metadata[:execution_result].status == :passed
49
+ results[:success] << [example.metadata[:values], nil]
50
+ else
51
+ exception = example.metadata[:execution_result].exception
52
+ results[exception.class] << [example.metadata[:values], exception]
53
+ end
54
+ results
55
+ end
56
+ end
57
+
58
+ def method_missing(m, *args)
59
+ p m => args
60
+ end
61
+ end
@@ -0,0 +1,106 @@
1
+ # encoding: utf-8
2
+ # This file is part of ruby-flores.
3
+ # Copyright (C) 2015 Jordan Sissel
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU Affero General Public License as
7
+ # published by the Free Software Foundation, either version 3 of the
8
+ # License, or (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU Affero General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Affero General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ require "flores/namespace"
18
+ require "flores/rspec"
19
+ require "flores/random"
20
+
21
+ # This module adds helpers useful in doing stress testing within rspec.
22
+ #
23
+ # The number of iterations in a stress test is random.
24
+ #
25
+ # By way of example, let's have a silly test for adding two positive numbers and expecting
26
+ # the result to not be negative:
27
+ #
28
+ # describe "Addition" do
29
+ # context "of two positive numbers" do
30
+ # let(:a) { Flores::Random.number(1..10000) }
31
+ # let(:b) { Flores::Random.number(1..10000) }
32
+ # subject { a + b }
33
+ #
34
+ # # Note the use of 'stress_it' here!
35
+ # stress_it "should be greater than zero" do
36
+ # expect(subject).to(be > 0)
37
+ # end
38
+ # end
39
+ # end
40
+ #
41
+ # Running this:
42
+ #
43
+ # % rspec
44
+ # <lots of dots>
45
+ #
46
+ # Finished in 0.45412 seconds (files took 0.32963 seconds to load)
47
+ # 4795 examples, 0 failures
48
+ #
49
+ # In this way, instead of testing 1 fixed case or 1 randomized case, we test
50
+ # *many* cases in one rspec run.
51
+ module Flores::RSpec::Stress
52
+ # Wraps `it` and runs the block many times. Each run has will clear the `let` cache.
53
+ #
54
+ # The implementation of this is roughly that the given block will be run N times within an `it`:
55
+ #
56
+ # stress_it_internal "my test" do
57
+ # expect(...)
58
+ # end
59
+ #
60
+ # is roughly equivalent to
61
+ #
62
+ # it "my test" do
63
+ # 1000.times do
64
+ # expect(...)
65
+ # __memoized.clear
66
+ # end
67
+ # end
68
+ #
69
+ # The intent of this is to allow randomized testing for fuzzing and stress testing
70
+ # of APIs to help find edge cases and weird behavior.
71
+ #
72
+ # The default number of iterations is randomly selected between 1 and 1000 inclusive
73
+ def stress_it_internal(name, options = {}, &block)
74
+ stress__iterations = Flores::Random.iterations(options.delete(:stress_iterations) || Flores::RSpec::DEFAULT_ITERATIONS)
75
+ it(name, options) do
76
+ # Run the block of an example many times
77
+ stress__iterations.each do
78
+ # Run the block within 'it' scope
79
+ instance_eval(&block)
80
+
81
+ # clear the internal rspec `let` cache this lets us run a test
82
+ # repeatedly with fresh `let` evaluations.
83
+ # Reference: https://github.com/rspec/rspec-core/blob/5fc29a15b9af9dc1c9815e278caca869c4769767/lib/rspec/core/memoized_helpers.rb#L124-L127
84
+ __memoized.clear
85
+ end
86
+ end # it ...
87
+ end # def stress_it_internal
88
+
89
+ # Generate a random number of copies of a given example.
90
+ # The idea is to take 1 `it` and run it N times to help tease out failures.
91
+ # Of course, the teasing requires you have randomized `let` usage, for example:
92
+ #
93
+ # let(:number) { Flores::Random.number(0..200) }
94
+ # it "should be less than 100" do
95
+ # expect(number).to(be < 100)
96
+ # end
97
+ #
98
+ # This creates N (random) copies of your spec example. Using `stress_it` is
99
+ # preferred instead of `stress_it_internal` because this method will cause
100
+ # before, after, and around clauses to be invoked correctly.
101
+ def stress_it(name, *args, &block)
102
+ Flores::Random.iterations(Flores::RSpec.iterations).each do
103
+ it(name, *args, &block)
104
+ end # each
105
+ end # def stress_it
106
+ end # Flores::RSpec::Stress
@@ -0,0 +1,45 @@
1
+ # encoding: utf-8
2
+ # This file is part of ruby-flores.
3
+ # Copyright (C) 2015 Jordan Sissel
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU Affero General Public License as
7
+ # published by the Free Software Foundation, either version 3 of the
8
+ # License, or (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU Affero General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Affero General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ # :nodoc:
18
+ require "flores/namespace"
19
+
20
+ # The root of the rspec helpers the Flores library provides
21
+ module Flores::RSpec
22
+ DEFAULT_ITERATIONS = 1..1000
23
+
24
+ # Sets up rspec with the Flores RSpec helpers. Usage looks like this:
25
+ #
26
+ # RSpec.configure do |config|
27
+ # Flores::RSpec.configure(config)
28
+ # end
29
+ def self.configure(rspec_configuration)
30
+ require "flores/rspec/stress"
31
+ require "flores/rspec/analyze"
32
+ rspec_configuration.extend(Flores::RSpec::Stress)
33
+ rspec_configuration.extend(Flores::RSpec::Analyze)
34
+ end # def self.configure
35
+
36
+ def self.iterations
37
+ return @iterations if @iterations
38
+ if ENV["ITERATIONS"]
39
+ @iterations = 0..ENV["ITERATIONS"].to_i
40
+ else
41
+ @iterations = DEFAULT_ITERATIONS
42
+ end
43
+ @iterations
44
+ end
45
+ end # def Flores::RSpec
@@ -1,3 +1,4 @@
1
+ # encoding: utf-8
1
2
  # This file is part of ruby-flores.
2
3
  # Copyright (C) 2015 Jordan Sissel
3
4
  #
@@ -13,25 +14,20 @@
13
14
  #
14
15
  # You should have received a copy of the GNU Affero General Public License
15
16
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
- #
17
- # encoding: utf-8
18
- require "randomized"
19
- require "rspec/stress_it"
17
+ require "spec_init"
20
18
 
21
- RSpec.configure do |c|
22
- c.extend RSpec::StressIt
23
- end
24
-
25
- shared_examples_for String do |variables|
26
- analyze_it "should be a String", variables do
19
+ shared_examples_for String do
20
+ stress_it "should be a String" do
27
21
  expect(subject).to(be_a(String))
28
22
  end
29
- analyze_it "have valid encoding", variables do
23
+ stress_it "have valid encoding" do
30
24
  expect(subject).to(be_valid_encoding)
31
25
  end
32
26
  end
33
27
 
34
- describe Randomized do
28
+ describe Flores::Random do
29
+ analyze_results
30
+
35
31
  describe "#text" do
36
32
  context "with no arguments" do
37
33
  stress_it "should raise ArgumentError" do
@@ -43,30 +39,30 @@ describe Randomized do
43
39
  subject { described_class.text(length) }
44
40
 
45
41
  context "that is positive" do
46
- let(:length) { rand(1..1000) }
42
+ let(:length) { Flores::Random.integer(1..1000) }
47
43
  it_behaves_like String, [:length]
48
- analyze_it "has correct length", [:length] do
44
+ stress_it "has correct length" do
49
45
  expect(subject.length).to(eq(length))
50
46
  end
51
47
  end
52
48
 
53
49
  context "that is negative" do
54
- let(:length) { -1 * rand(1..1000) }
55
- analyze_it "should raise ArgumentError", [:length] do
50
+ let(:length) { -1 * Flores::Random.integer(1..1000) }
51
+ stress_it "should raise ArgumentError" do
56
52
  expect { subject }.to(raise_error(ArgumentError))
57
53
  end
58
54
  end
59
55
  end
60
56
 
61
57
  context "with 1 range argument" do
62
- let(:start) { rand(1..1000) }
63
- let(:length) { rand(1..1000) }
58
+ let(:start) { Flores::Random.integer(2..1000) }
59
+ let(:length) { Flores::Random.integer(1..1000) }
64
60
  subject { described_class.text(range) }
65
61
 
66
62
  context "that is ascending" do
67
63
  let(:range) { start..(start + length) }
68
64
  it_behaves_like String, [:range]
69
- analyze_it "should give a string within that length range", [:range] do
65
+ stress_it "should give a string within that length range" do
70
66
  expect(subject).to(be_a(String))
71
67
  expect(range).to(include(subject.length))
72
68
  end
@@ -74,7 +70,7 @@ describe Randomized do
74
70
 
75
71
  context "that is descending" do
76
72
  let(:range) { start..(start - length) }
77
- analyze_it "should raise ArgumentError", [:range] do
73
+ stress_it "should raise ArgumentError" do
78
74
  expect { subject }.to(raise_error(ArgumentError))
79
75
  end
80
76
  end
@@ -84,49 +80,53 @@ describe Randomized do
84
80
  describe "#character" do
85
81
  subject { described_class.character }
86
82
  it_behaves_like String, [:subject]
87
- analyze_it "has length == 1", [:subject] do
83
+ stress_it "has length == 1" do
88
84
  expect(subject.length).to(be == 1)
89
85
  end
90
86
  end
91
87
 
92
88
  shared_examples_for Numeric do |type|
93
- let(:start) { Randomized.integer(-100_000..100_000) }
94
- let(:length) { Randomized.integer(1..100_000) }
89
+ let(:start) { Flores::Random.integer(-100_000..100_000) }
90
+ let(:length) { Flores::Random.integer(1..100_000) }
95
91
  let(:range) { start..(start + length) }
96
92
 
97
- analyze_it "should be a #{type}", [:range] do
93
+ stress_it "should be a #{type}" do
98
94
  expect(subject).to(be_a(type))
99
95
  end
100
96
 
101
- analyze_it "should be within the bounds of the given range", [:range] do
97
+ stress_it "should be within the bounds of the given range" do
102
98
  expect(range).to(include(subject))
103
99
  end
104
100
  end
105
101
 
106
102
  describe "#integer" do
107
103
  it_behaves_like Numeric, Fixnum do
108
- subject { Randomized.integer(range) }
104
+ subject { Flores::Random.integer(range) }
109
105
  end
110
106
  end
111
107
 
112
108
  describe "#number" do
113
109
  it_behaves_like Numeric, Float do
114
- subject { Randomized.number(range) }
110
+ subject { Flores::Random.number(range) }
115
111
  end
116
112
  end
117
113
 
118
114
  describe "#iterations" do
119
- let(:start) { Randomized.integer(1..100_000) }
120
- let(:length) { Randomized.integer(1..100_000) }
115
+ let(:start) { Flores::Random.integer(1..1000) }
116
+ let(:length) { Flores::Random.integer(1..1000) }
121
117
  let(:range) { start..(start + length) }
122
- subject { Randomized.iterations(range) }
118
+ subject { Flores::Random.iterations(range) }
123
119
 
124
- analyze_it "should return an Enumerable", [:range] do
120
+ stress_it "should return an Enumerable" do
125
121
  expect(subject).to(be_a(Enumerable))
126
122
  end
127
123
 
128
- analyze_it "should have a size within the expected range", [:range] do
129
- expect(range).to(include(subject.size))
124
+ stress_it "should have a size within the expected range" do
125
+ # Ruby 2.0 added Enumerable#size, so we can't use it here.
126
+ # Meaning `123.times.size` doesn't work. So for this test,
127
+ # we use small ranges because Enumerable#count actually
128
+ # counts (via iteration) and is slow on large numbers.
129
+ expect(range).to(include(subject.count))
130
130
  end
131
131
  end
132
132
  end
@@ -0,0 +1,87 @@
1
+ # encoding: utf-8
2
+ # This file is part of ruby-flores.
3
+ # Copyright (C) 2015 Jordan Sissel
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU Affero General Public License as
7
+ # published by the Free Software Foundation, either version 3 of the
8
+ # License, or (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU Affero General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Affero General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ require "spec_init"
18
+
19
+ Counter = Class.new do
20
+ attr_reader :value
21
+ def initialize
22
+ @value = 0
23
+ end
24
+
25
+ def incr
26
+ @value += 1
27
+ end
28
+
29
+ def decr
30
+ @value -= 1
31
+ end
32
+ end
33
+
34
+ describe Flores::RSpec::Stress do
35
+ subject { Counter.new }
36
+ before do
37
+ expect(subject.value).to(be == 0)
38
+ subject.incr
39
+ expect(subject.value).to(be == 1)
40
+ end
41
+
42
+ after do
43
+ expect(subject.value).to(be == 1)
44
+ subject.decr
45
+ expect(subject.value).to(be == 0)
46
+ end
47
+
48
+ stress_it "should call all before and after hooks" do
49
+ expect(subject.value).to(be == 1)
50
+ end
51
+
52
+ describe "level 1" do
53
+ before do
54
+ expect(subject.value).to(be == 1)
55
+ subject.incr
56
+ expect(subject.value).to(be == 2)
57
+ end
58
+
59
+ after do
60
+ expect(subject.value).to(be == 2)
61
+ subject.decr
62
+ expect(subject.value).to(be == 1)
63
+ end
64
+
65
+ stress_it "should call all before and after hooks" do
66
+ expect(subject.value).to(be == 2)
67
+ end
68
+
69
+ describe "level 2" do
70
+ before do
71
+ expect(subject.value).to(be == 2)
72
+ subject.incr
73
+ expect(subject.value).to(be == 3)
74
+ end
75
+
76
+ after do
77
+ expect(subject.value).to(be == 3)
78
+ subject.decr
79
+ expect(subject.value).to(be == 2)
80
+ end
81
+
82
+ stress_it "should call all before and after hooks" do
83
+ expect(subject.value).to(be == 3)
84
+ end
85
+ end
86
+ end
87
+ end
data/spec/spec_init.rb ADDED
@@ -0,0 +1,23 @@
1
+ # encoding: utf-8
2
+ # This file is part of ruby-flores.
3
+ # Copyright (C) 2015 Jordan Sissel
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU Affero General Public License as
7
+ # published by the Free Software Foundation, either version 3 of the
8
+ # License, or (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU Affero General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU Affero General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ require "flores/random"
18
+ require "flores/rspec"
19
+
20
+ RSpec.configure do |config|
21
+ Kernel.srand config.seed
22
+ Flores::RSpec.configure(config)
23
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flores
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Sissel
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-02-21 00:00:00.000000000 Z
11
+ date: 2015-02-24 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |2
14
14
  Add fuzzing, randomization, and stress to your tests.
@@ -23,42 +23,49 @@ executables: []
23
23
  extensions: []
24
24
  extra_rdoc_files: []
25
25
  files:
26
- - ".rubocop.yml"
26
+ - .rubocop.yml
27
27
  - Gemfile
28
28
  - Gemfile.lock
29
29
  - LICENSE.txt
30
30
  - Makefile
31
+ - README.md
31
32
  - examples/analyze_number.rb
32
33
  - examples/socket_acceptance_spec.rb
33
34
  - examples/socket_analyze_spec.rb
34
35
  - examples/socket_stress_spec.rb
35
36
  - flores.gemspec
36
- - lib/randomized.rb
37
- - lib/rspec/stress_it.rb
38
- - spec/randomized_spec.rb
39
- homepage:
37
+ - lib/flores/namespace.rb
38
+ - lib/flores/random.rb
39
+ - lib/flores/rspec.rb
40
+ - lib/flores/rspec/analyze.rb
41
+ - lib/flores/rspec/formatters/analyze.rb
42
+ - lib/flores/rspec/stress.rb
43
+ - spec/flores/random_spec.rb
44
+ - spec/flores/rspec/stress_spec.rb
45
+ - spec/spec_init.rb
46
+ homepage:
40
47
  licenses:
41
48
  - AGPL 3.0 - http://www.gnu.org/licenses/agpl-3.0.html
42
49
  metadata: {}
43
- post_install_message:
50
+ post_install_message:
44
51
  rdoc_options: []
45
52
  require_paths:
46
53
  - lib
47
54
  - lib
48
55
  required_ruby_version: !ruby/object:Gem::Requirement
49
56
  requirements:
50
- - - ">="
57
+ - - '>='
51
58
  - !ruby/object:Gem::Version
52
59
  version: '0'
53
60
  required_rubygems_version: !ruby/object:Gem::Requirement
54
61
  requirements:
55
- - - ">="
62
+ - - '>='
56
63
  - !ruby/object:Gem::Version
57
64
  version: '0'
58
65
  requirements: []
59
- rubyforge_project:
60
- rubygems_version: 2.4.3
61
- signing_key:
66
+ rubyforge_project:
67
+ rubygems_version: 2.4.5
68
+ signing_key:
62
69
  specification_version: 4
63
70
  summary: Fuzz, randomize, and stress your tests
64
71
  test_files: []