flores 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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: []