fuzzbert 1.0.0
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.
- 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 [](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
|
+
|