fuzzbert 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.md +204 -0
- data/bin/fuzzbert +9 -0
- data/lib/fuzzbert.rb +36 -0
- data/lib/fuzzbert/autorun.rb +65 -0
- data/lib/fuzzbert/autorun.rb~ +33 -0
- data/lib/fuzzbert/container.rb +21 -0
- data/lib/fuzzbert/dsl.rb +9 -0
- data/lib/fuzzbert/error_handler.rb +19 -0
- data/lib/fuzzbert/executor.rb +153 -0
- data/lib/fuzzbert/generation.rb +8 -0
- data/lib/fuzzbert/generator.rb +18 -0
- data/lib/fuzzbert/generators.rb +70 -0
- data/lib/fuzzbert/mutator.rb +19 -0
- data/lib/fuzzbert/rake_task.rb +81 -0
- data/lib/fuzzbert/template.rb +154 -0
- data/lib/fuzzbert/test.rb +13 -0
- data/lib/fuzzbert/test_suite.rb +25 -0
- data/lib/fuzzbert/version.rb +4 -0
- data/spec/autorun_spec.rb +39 -0
- data/spec/dsl_spec.rb +88 -0
- data/spec/executor_spec.rb +113 -0
- data/spec/generator_spec.rb +25 -0
- data/spec/mutator_spec.rb +32 -0
- data/spec/template_spec.rb +84 -0
- data/spec/test_spec.rb +20 -0
- metadata +80 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Martin Boßlet
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,204 @@
|
|
1
|
+
# FuzzBert [![Build Status](https://secure.travis-ci.org/krypt/FuzzBert.png?branch=master)](http://travis-ci.org/krypt/FuzzBert)
|
2
|
+
|
3
|
+
A random testing/fuzzer framework for Ruby.
|
4
|
+
|
5
|
+
Random Testing (or "fuzzing") is not really new, it has been around for quite
|
6
|
+
some time. Yet it still hasn't found widespread adoption in everyday coding
|
7
|
+
practices, much too often it is only used for the purpose of finding exploits
|
8
|
+
for existing applications or libraries. FuzzBert wants to improve this situation.
|
9
|
+
It's a simple fuzzing framework with an RSpec-like DSL that will allow you to
|
10
|
+
integrate random tests in your project with minimal effort.
|
11
|
+
|
12
|
+
For further information on random testing, here are two excellent starting points:
|
13
|
+
|
14
|
+
* [Udacity CS258](http://www.udacity.com/overview/Course/cs258/)
|
15
|
+
* [Babysitting an Army of Monkeys](http://fuzzinginfo.files.wordpress.com/2012/05/cmiller-csw-2010.pdf)
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
gem install fuzzbert
|
20
|
+
fuzzbert --help
|
21
|
+
|
22
|
+
## Defining a random test
|
23
|
+
|
24
|
+
FuzzBert defines an RSpec-like DSL that can be used to define different fuzzing
|
25
|
+
scenarios. The DSL uses three words: `fuzz`, `deploy` and `data`. `fuzz` can be
|
26
|
+
thought of as defining a new scenario, such as "fuzz this command line tool",
|
27
|
+
"fuzz this particular URL of my web app" or "fuzz this library method taking
|
28
|
+
external input".
|
29
|
+
|
30
|
+
Within a `fuzz` block, there must be one occurence of `deploy` and one or several
|
31
|
+
occurences of `data`. The `deploy` block is the spot where we deliver the random
|
32
|
+
payload that has been generated. It is agnostic about the actual target in order to
|
33
|
+
leave you free to fuzz whatever you require in your particular case. The `data`
|
34
|
+
blocks define the shape of the random data being generated. There can be more than
|
35
|
+
one such block because it is often beneficial to not only shoot completely random
|
36
|
+
data at the target - you often want to deliver more structured data as well, trying
|
37
|
+
to find the edge cases deeper within your code. Good random test suites make use
|
38
|
+
of both - totally random data as well as structured data - in order to cover as
|
39
|
+
much "code surface" as possible.
|
40
|
+
|
41
|
+
Here is a quick example that fuzzes `JSON.parse`:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
require 'json'
|
45
|
+
require 'fuzzbert'
|
46
|
+
|
47
|
+
fuzz "JSON.parse" do
|
48
|
+
|
49
|
+
deploy do |data|
|
50
|
+
begin
|
51
|
+
JSON.parse data
|
52
|
+
rescue StandardError
|
53
|
+
#fine, we just want to capture crashes
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
data "completely random" do
|
58
|
+
FuzzBert::Generators.random
|
59
|
+
end
|
60
|
+
|
61
|
+
data "enclosing curly braces" do
|
62
|
+
c = FuzzBert::Container.new
|
63
|
+
c << FuzzBert::Generators.fixed("{")
|
64
|
+
c << FuzzBert::Generators.random
|
65
|
+
c << FuzzBert::Generators.fixed("}")
|
66
|
+
c.generator
|
67
|
+
end
|
68
|
+
|
69
|
+
data "my custom generator" do
|
70
|
+
prng = Random.new
|
71
|
+
lambda do
|
72
|
+
buf = '{ user: { '
|
73
|
+
buf << prng.bytes(100)
|
74
|
+
buf << ' } }'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
```
|
80
|
+
|
81
|
+
The `deploy` block takes the generated data as a parameter. The block itself is
|
82
|
+
responsible of deploying the payload. An execution is considered successful if
|
83
|
+
the `deploy` block passes with no uncaught error being raised. If an error slips
|
84
|
+
through or if the Ruby process crashes altogether, the execution is of course
|
85
|
+
considered as a failure.
|
86
|
+
|
87
|
+
`data` blocks must return a lambda or proc that takes no argument. You can either
|
88
|
+
choose completely custom lambdas of your own or use those predefined for you in
|
89
|
+
`FuzzBert::Generators`.
|
90
|
+
|
91
|
+
## Templates
|
92
|
+
|
93
|
+
Using the approach described so far is most useful for binary protocols, but as
|
94
|
+
soon as largely String-based data is needed it can soon become tedious. What you
|
95
|
+
actually want in these situations is some sort of template mechanism that comes
|
96
|
+
with mostly fixed data and only replaces a few selected parts with randomly
|
97
|
+
generated data. This, too, is possible with FuzzBert, it comes with a minimal
|
98
|
+
templating language:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
require 'fuzzbert'
|
102
|
+
|
103
|
+
fuzz "My Web App" do
|
104
|
+
|
105
|
+
deploy do |data|
|
106
|
+
# Send the data to your web app with httpclient or similar.
|
107
|
+
# You define the "error conditions": if a response to some
|
108
|
+
# data is not as expected, you could simply raise an error
|
109
|
+
# here.
|
110
|
+
end
|
111
|
+
|
112
|
+
data "JSON generated from a template" do
|
113
|
+
t = FuzzBert::Template.new '{ user: { id: ${id}, name: "${name}" } }'
|
114
|
+
t.set(:id) { FuzzBert::Generators.cycle(1..10000) }
|
115
|
+
t.set(:name) { FuzzBert::Generators.random }
|
116
|
+
t.generator
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
```
|
121
|
+
|
122
|
+
## Mutators
|
123
|
+
|
124
|
+
Mutation is the principle used in "Babysitting an Army of Monkeys". The basis for
|
125
|
+
the mutation tests is a valid sample of input that is then modified in exactly one
|
126
|
+
position in each test instance. You can apply this principle as follows:
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
require 'fuzzbert'
|
130
|
+
|
131
|
+
fuzz "Web App" do
|
132
|
+
deploy do |data|
|
133
|
+
#send JSON data via HTTP
|
134
|
+
end
|
135
|
+
|
136
|
+
data "mutated data" do
|
137
|
+
m = FuzzBert::Mutator.new '{ user: { id: 42, name: "FuzzBert" }'
|
138
|
+
m.generator
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
144
|
+
## How the tests are performed
|
145
|
+
|
146
|
+
Each `fuzz` block defines a `TestSuite`. These are executed in a round-robin manner.
|
147
|
+
Each individual `TestSuite` will then apply the `deploy` block with a sample of
|
148
|
+
data generated successively by each one of the `data` blocks. Once all `data` blocks
|
149
|
+
were used, the next `TestSuite` will be executed and so on... By default, a FuzzBert
|
150
|
+
fuzzing session runs forever, until the process is either killed or by manually hitting
|
151
|
+
`CTRL+C` for example. This was a deliberate design choice since random testing suites
|
152
|
+
need to be run for quite some time to be effective. It's something you want to run over
|
153
|
+
the weekend rather than for a couple of minutes. Still, it can make sense to explicitly
|
154
|
+
limit the number of runs, for example when integrating FuzzBert with a CI server or
|
155
|
+
with Travis. You can do so by passing the `--limit` parameter to the `fuzzbert`
|
156
|
+
executable.
|
157
|
+
|
158
|
+
Every single execution of `deploy` is run in a separate process. The main reason for
|
159
|
+
this is that we typically want to detect hard crashes when a C extension or even Ruby
|
160
|
+
itself encounters an input it can't handle. Besides being able to cope with these cases,
|
161
|
+
running in separate processes proves beneficial otherwise as well: by default, FuzzBert
|
162
|
+
runs the tests in four separate processes at once, therefore utilizing your CPU's cores
|
163
|
+
effectively. You can tweak that setting with `--pool-size` to set this number to 1
|
164
|
+
(for completely sequential runs) or to the exact number of cores your CPU offers.
|
165
|
+
|
166
|
+
## What happens if we encounter a bug?
|
167
|
+
|
168
|
+
If a test should end up failing (either the process crashed completely or caused an
|
169
|
+
uncaught error), FuzzBert will output the failing test on your terminal and tell you
|
170
|
+
where it stored the data that caused this. This conveniently allows you to run FuzzBert
|
171
|
+
over the weekend and when you return on Monday, the troubleshooters will sit there all
|
172
|
+
lined up for you to go through and filter. By using the `--console` command line switch
|
173
|
+
you can tell FuzzBert to not explicitly store the data, but echoing the data that
|
174
|
+
caused the crash to the terminal instead.
|
175
|
+
|
176
|
+
## Rake integration
|
177
|
+
|
178
|
+
You may integrate Rake tasks for FuzzBert similar to how you would include a task for
|
179
|
+
Rspec:
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
require 'rake'
|
183
|
+
require 'fuzzbert/rake_task'
|
184
|
+
|
185
|
+
FuzzBert::RakeTask.new(:fuzz) do |spec|
|
186
|
+
spec.fuzzbert_opts = ['--limit 10000000', '--console']
|
187
|
+
spec.pattern = 'fuzz/**/fuzz_*.rb'
|
188
|
+
end
|
189
|
+
```
|
190
|
+
|
191
|
+
## Supported versions
|
192
|
+
|
193
|
+
FuzzBert has been confirmed to run on CRuby 1.9.3 and Rubinius 2.0.0dev. Since
|
194
|
+
it heavily relies on forking, it does not run on JRuby so far, but support is planned and
|
195
|
+
on its way.
|
196
|
+
|
197
|
+
You may also use FuzzBert for fuzzing arbitrary applications or libraries that aren't
|
198
|
+
connected to Ruby at all - have a look in the examples that ship with FuzzBert.
|
199
|
+
|
200
|
+
## License
|
201
|
+
|
202
|
+
Copyright (c) 2012 Martin Boßlet. Distributed under the MIT License. See LICENSE for
|
203
|
+
further details.
|
204
|
+
|
data/bin/fuzzbert
ADDED
data/lib/fuzzbert.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
= Info
|
4
|
+
|
5
|
+
FuzzBert - Random Testing / Fuzzing in Ruby
|
6
|
+
|
7
|
+
Copyright (C) 2012
|
8
|
+
Martin Bosslet <martin.bosslet@googlemail.com>
|
9
|
+
All rights reserved.
|
10
|
+
|
11
|
+
= License
|
12
|
+
|
13
|
+
See the file 'LICENSE' for further details.
|
14
|
+
|
15
|
+
=end
|
16
|
+
|
17
|
+
module FuzzBert
|
18
|
+
|
19
|
+
PRNG = Random.new
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
require_relative 'fuzzbert/version'
|
24
|
+
require_relative 'fuzzbert/generation'
|
25
|
+
require_relative 'fuzzbert/generators'
|
26
|
+
require_relative 'fuzzbert/generator'
|
27
|
+
require_relative 'fuzzbert/mutator'
|
28
|
+
require_relative 'fuzzbert/template'
|
29
|
+
require_relative 'fuzzbert/container'
|
30
|
+
require_relative 'fuzzbert/test'
|
31
|
+
require_relative 'fuzzbert/error_handler'
|
32
|
+
require_relative 'fuzzbert/test_suite'
|
33
|
+
require_relative 'fuzzbert/executor'
|
34
|
+
require_relative 'fuzzbert/autorun'
|
35
|
+
require_relative 'fuzzbert/dsl'
|
36
|
+
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module FuzzBert::AutoRun
|
4
|
+
|
5
|
+
TEST_CASES = []
|
6
|
+
|
7
|
+
module_function
|
8
|
+
|
9
|
+
def register(suite)
|
10
|
+
TEST_CASES << suite
|
11
|
+
end
|
12
|
+
|
13
|
+
def autorun
|
14
|
+
options, files = process_args(ARGV)
|
15
|
+
load_files(files)
|
16
|
+
run(options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def run(options=nil)
|
20
|
+
executor = options ? FuzzBert::Executor.new(TEST_CASES, options) : FuzzBert::Executor.new(TEST_CASES)
|
21
|
+
executor.run
|
22
|
+
end
|
23
|
+
|
24
|
+
private; module_function
|
25
|
+
|
26
|
+
def load_files(files)
|
27
|
+
files.each do |pattern|
|
28
|
+
Dir.glob(pattern).each { |f| load File.expand_path(f) }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def process_args(args = [])
|
33
|
+
options = {}
|
34
|
+
orig_args = args.dup
|
35
|
+
|
36
|
+
OptionParser.new do |opts|
|
37
|
+
opts.banner = 'FuzzBert options:'
|
38
|
+
opts.version = FuzzBert::VERSION
|
39
|
+
|
40
|
+
opts.on '-h', '--help', 'Display this help.' do
|
41
|
+
puts opts
|
42
|
+
exit
|
43
|
+
end
|
44
|
+
|
45
|
+
opts.on '--pool-size SIZE', Integer, "Sets the number of concurrently running processes to SIZE" do |n|
|
46
|
+
options[:pool_size] = n.to_i
|
47
|
+
end
|
48
|
+
|
49
|
+
opts.on '--limit LIMIT', Integer, "Instead of running permanently, fuzzing will be stopped after running LIMIT of instances" do |n|
|
50
|
+
p n
|
51
|
+
options[:limit] = n.to_i
|
52
|
+
end
|
53
|
+
|
54
|
+
opts.on '--console', "Output the failing cases including data on the console instead of saving them in a file" do
|
55
|
+
options[:handler] = FuzzBert::Handler::Console.new
|
56
|
+
end
|
57
|
+
|
58
|
+
opts.parse! args
|
59
|
+
end
|
60
|
+
|
61
|
+
[options, args]
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
|
2
|
+
module FuzzBert::AutoRun
|
3
|
+
|
4
|
+
TEST_CASES = []
|
5
|
+
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def register(suite)
|
9
|
+
TEST_CASES << suite
|
10
|
+
end
|
11
|
+
|
12
|
+
def autorun
|
13
|
+
at_exit do
|
14
|
+
next if $! # don't run if there was an exception
|
15
|
+
|
16
|
+
load_files(ARGV)
|
17
|
+
run
|
18
|
+
|
19
|
+
end unless @@installed_at_exit
|
20
|
+
@@installed_at_exit = true
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
FuzzBert::Executor.new(TEST_CASES).run
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def load_files(args)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
class FuzzBert::Container
|
3
|
+
include FuzzBert::Generation
|
4
|
+
|
5
|
+
def initialize(generators=[])
|
6
|
+
@generators = generators
|
7
|
+
end
|
8
|
+
|
9
|
+
def <<(generator)
|
10
|
+
@generators << generator
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_data
|
14
|
+
"".tap do |buf|
|
15
|
+
@generators.each { |gen| buf << gen.call }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
|
data/lib/fuzzbert/dsl.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
|
2
|
+
module FuzzBert::Handler
|
3
|
+
class FileOutput
|
4
|
+
def handle(id, data, pid, status)
|
5
|
+
prefix = status.termsig ? "crash" : "bug"
|
6
|
+
filename = "#{prefix}#{pid}"
|
7
|
+
File.open(filename, "wb") { |f| f.print(data) }
|
8
|
+
puts "#{id} failed. Data was saved as #{filename}."
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class Console
|
13
|
+
def handle(id, data, pid, status)
|
14
|
+
puts "#{id} failed. Data: #{data.inspect}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
|
@@ -0,0 +1,153 @@
|
|
1
|
+
class FuzzBert::Executor
|
2
|
+
|
3
|
+
attr_reader :pool_size, :limit, :handler
|
4
|
+
|
5
|
+
DEFAULT_POOL_SIZE = 4
|
6
|
+
DEFAULT_LIMIT = -1
|
7
|
+
DEFAULT_HANDLER = FuzzBert::Handler::FileOutput
|
8
|
+
|
9
|
+
def initialize(suites, args = {
|
10
|
+
pool_size: DEFAULT_POOL_SIZE,
|
11
|
+
limit: DEFAULT_LIMIT,
|
12
|
+
handler: DEFAULT_HANDLER.new
|
13
|
+
})
|
14
|
+
@pool_size = args[:pool_size] || DEFAULT_POOL_SIZE
|
15
|
+
@limit = args[:limit] || DEFAULT_LIMIT
|
16
|
+
@handler = args[:handler] || DEFAULT_HANDLER.new
|
17
|
+
@data_cache = {}
|
18
|
+
@n = 0
|
19
|
+
@exiting = false
|
20
|
+
@producer = DataProducer.new(suites)
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
trap_child_exit
|
25
|
+
trap_interrupt
|
26
|
+
|
27
|
+
@pool_size.times { run_instance(*@producer.next) }
|
28
|
+
@running = true
|
29
|
+
@limit == -1 ? sleep : conditional_sleep
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def run_instance(description, test, generator)
|
35
|
+
data = generator.to_data
|
36
|
+
pid = fork do
|
37
|
+
begin
|
38
|
+
test.run(data)
|
39
|
+
rescue StandardError
|
40
|
+
abort
|
41
|
+
end
|
42
|
+
end
|
43
|
+
id = "#{description}/#{generator.description}"
|
44
|
+
@data_cache[pid] = [id, data]
|
45
|
+
end
|
46
|
+
|
47
|
+
def trap_child_exit
|
48
|
+
trap(:CHLD) do
|
49
|
+
begin
|
50
|
+
while exitval = Process.wait2(-1, Process::WNOHANG)
|
51
|
+
pid = exitval[0]
|
52
|
+
status = exitval[1]
|
53
|
+
data_ary = @data_cache.delete(pid)
|
54
|
+
unless status.success?
|
55
|
+
handle(data_ary[0], data_ary[1], pid, status) unless interrupted(status)
|
56
|
+
end
|
57
|
+
@n += 1
|
58
|
+
if @limit == -1 || @n < @limit
|
59
|
+
run_instance(*@producer.next)
|
60
|
+
else
|
61
|
+
@running = false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
rescue Errno::ECHILD
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def trap_interrupt
|
70
|
+
trap(:INT) do
|
71
|
+
exit! (1) if @exiting
|
72
|
+
@exiting = true
|
73
|
+
graceful_exit
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def graceful_exit
|
78
|
+
puts "\nExiting...Interrupt again to exit immediately"
|
79
|
+
begin
|
80
|
+
while Process.wait; end
|
81
|
+
rescue Errno::ECHILD
|
82
|
+
end
|
83
|
+
exit 0
|
84
|
+
end
|
85
|
+
|
86
|
+
def handle(id, data, pid, status)
|
87
|
+
@handler.handle(id, data, pid, status)
|
88
|
+
end
|
89
|
+
|
90
|
+
def interrupted(status)
|
91
|
+
return false if status.exited?
|
92
|
+
return true if status.termsig == nil || status.termsig == 2
|
93
|
+
end
|
94
|
+
|
95
|
+
def conditional_sleep
|
96
|
+
sleep 0.1 until @running == false
|
97
|
+
end
|
98
|
+
|
99
|
+
class DataProducer
|
100
|
+
def initialize(suites)
|
101
|
+
@ring = Ring.new(suites)
|
102
|
+
update
|
103
|
+
end
|
104
|
+
|
105
|
+
def update
|
106
|
+
@suite = @ring.next
|
107
|
+
@gen_iter = ProcessSafeEnumerator.new(@suite.generators)
|
108
|
+
end
|
109
|
+
|
110
|
+
def next
|
111
|
+
gen = nil
|
112
|
+
until gen
|
113
|
+
begin
|
114
|
+
gen = @gen_iter.next
|
115
|
+
rescue StopIteration
|
116
|
+
update
|
117
|
+
end
|
118
|
+
end
|
119
|
+
[@suite.description, @suite.test, gen]
|
120
|
+
end
|
121
|
+
|
122
|
+
class Ring
|
123
|
+
def initialize(objs)
|
124
|
+
@i = 0
|
125
|
+
objs = [objs] unless objs.respond_to?(:each)
|
126
|
+
@objs = objs.to_a
|
127
|
+
end
|
128
|
+
|
129
|
+
def next
|
130
|
+
obj = @objs[@i]
|
131
|
+
@i = (@i + 1) % @objs.size
|
132
|
+
obj
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
#needed because the Fiber used for normal Enumerators has race conditions
|
137
|
+
class ProcessSafeEnumerator
|
138
|
+
def initialize(ary)
|
139
|
+
@i = 0
|
140
|
+
@ary = ary.to_a
|
141
|
+
end
|
142
|
+
|
143
|
+
def next
|
144
|
+
obj = @ary[@i]
|
145
|
+
raise StopIteration unless obj
|
146
|
+
@i += 1
|
147
|
+
obj
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|