flatware 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE.txt +20 -0
- data/README.rdoc +79 -0
- data/bin/flatware +6 -0
- data/lib/flatware.rb +50 -0
- data/lib/flatware/cli.rb +71 -0
- data/lib/flatware/cucumber.rb +38 -0
- data/lib/flatware/cucumber/formatter.rb +151 -0
- data/lib/flatware/cucumber/runtime.rb +36 -0
- data/lib/flatware/dispatcher.rb +38 -0
- data/lib/flatware/fireable.rb +22 -0
- data/lib/flatware/result.rb +24 -0
- data/lib/flatware/scenario_result.rb +24 -0
- data/lib/flatware/sink.rb +136 -0
- data/lib/flatware/step_result.rb +44 -0
- data/lib/flatware/summary.rb +61 -0
- data/lib/flatware/worker.rb +55 -0
- metadata +160 -0
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Brian Dunn
|
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.rdoc
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
= Flatware
|
2
|
+
|
3
|
+
Flatware is a a distributed cucumber runner.
|
4
|
+
|
5
|
+
== Requirements
|
6
|
+
|
7
|
+
* ZeroMQ ~> 2.1
|
8
|
+
|
9
|
+
== Installation
|
10
|
+
|
11
|
+
Add this to your Gemfile:
|
12
|
+
|
13
|
+
gem 'flatware'
|
14
|
+
|
15
|
+
and `bundle install`.
|
16
|
+
|
17
|
+
== Usage
|
18
|
+
|
19
|
+
To run your entire suite with the default cucumber options, just:
|
20
|
+
|
21
|
+
flatware
|
22
|
+
|
23
|
+
If you'd like to limit the number of forked workers, you can pass the 'w' flag:
|
24
|
+
|
25
|
+
flatware -w 3
|
26
|
+
|
27
|
+
You can also pass most cucumber options to flatware. For example, to run only features that are not tagged 'javascript', you can:
|
28
|
+
|
29
|
+
flatware cucumber -t ~@javascript
|
30
|
+
|
31
|
+
|
32
|
+
== Planned Features
|
33
|
+
|
34
|
+
* Reliable enough to use as part of your Continuous Integration system
|
35
|
+
* Always accounts for every feature you ask it to run
|
36
|
+
* Use heuristics to run your slowest tests first
|
37
|
+
* speak Cucumber's DRB protocol; if you know how to use Spork you know how to use Flatware
|
38
|
+
|
39
|
+
== Design Goals
|
40
|
+
|
41
|
+
=== Maintainable
|
42
|
+
|
43
|
+
* Fully test at an integration level. Don't be afraid to change the code. If you break it you'll know.
|
44
|
+
* Couple as loosely as possible, and only to the most stable/public bits of Cucumber.
|
45
|
+
|
46
|
+
=== Minimal
|
47
|
+
|
48
|
+
* Projects define their own preperation scripts
|
49
|
+
* Only distribute to local cores (for now)
|
50
|
+
* Only handle cucumber
|
51
|
+
|
52
|
+
=== Robust
|
53
|
+
|
54
|
+
* Depend on a dedicated messaging library
|
55
|
+
* Be acountable for completed work; provide progress report regardless of completing the suite.
|
56
|
+
|
57
|
+
== Tinkering
|
58
|
+
|
59
|
+
Flatware is tested with aruba. In order to get a demo cucumber project you can add the `@no-clobber` tag to `features/flatware.feature` and run the test with `cucumber features/flatware.feature`. Now you should have a `./tmp/aruba` directory. CD there and you can run the `worker` and `dispatcher` binaries, like `../../bin/worker` and `../../bin/dispatcher`
|
60
|
+
|
61
|
+
== Resources
|
62
|
+
|
63
|
+
The excellent ZeroMQ guide: http://zguide.zeromq.org/page:all
|
64
|
+
|
65
|
+
== Contributing to Flatware
|
66
|
+
|
67
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
68
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
69
|
+
* Fork the project
|
70
|
+
* Start a feature/bugfix branch
|
71
|
+
* Commit and push until you are happy with your contribution
|
72
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
73
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
74
|
+
|
75
|
+
== Copyright
|
76
|
+
|
77
|
+
Copyright (c) 2011-2012 Brian Dunn. See LICENSE.txt for
|
78
|
+
further details.
|
79
|
+
|
data/bin/flatware
ADDED
data/lib/flatware.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'zmq'
|
2
|
+
|
3
|
+
module Flatware
|
4
|
+
autoload :CLI, 'flatware/cli'
|
5
|
+
autoload :Cucumber, 'flatware/cucumber'
|
6
|
+
autoload :Dispatcher, 'flatware/dispatcher'
|
7
|
+
autoload :Fireable, 'flatware/fireable'
|
8
|
+
autoload :Result, 'flatware/result'
|
9
|
+
autoload :ScenarioResult, 'flatware/scenario_result'
|
10
|
+
autoload :Sink, 'flatware/sink'
|
11
|
+
autoload :StepResult, 'flatware/step_result'
|
12
|
+
autoload :Summary, 'flatware/summary'
|
13
|
+
autoload :Worker, 'flatware/worker'
|
14
|
+
|
15
|
+
Job = Struct.new :id, :args
|
16
|
+
|
17
|
+
extend self
|
18
|
+
def socket(*args)
|
19
|
+
context.socket(*args).tap do |socket|
|
20
|
+
sockets.push socket
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def close
|
25
|
+
sockets.each &:close
|
26
|
+
context.close
|
27
|
+
@context = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def log(*message)
|
31
|
+
if verbose?
|
32
|
+
$stderr.print "#{$$} "
|
33
|
+
$stderr.puts *message
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_writer :verbose
|
38
|
+
def verbose?
|
39
|
+
!!@verbose
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def context
|
44
|
+
@context ||= ZMQ::Context.new
|
45
|
+
end
|
46
|
+
|
47
|
+
def sockets
|
48
|
+
@sockets ||= []
|
49
|
+
end
|
50
|
+
end
|
data/lib/flatware/cli.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'thor'
|
2
|
+
module Flatware
|
3
|
+
class CLI < Thor
|
4
|
+
|
5
|
+
def self.processors
|
6
|
+
@processors ||= `hostinfo`.match(/^(?<processors>\d+) processors are logically available\.$/)[:processors].to_i
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.worker_option
|
10
|
+
method_option :workers, aliases: "-w", type: :numeric, default: processors, desc: "Number of concurent processes to run"
|
11
|
+
end
|
12
|
+
|
13
|
+
class_option :log, aliases: "-l", type: :boolean, desc: "Print debug messages to $stderr"
|
14
|
+
|
15
|
+
default_task :default
|
16
|
+
worker_option
|
17
|
+
desc "default", "parallelizes cucumber with default arguments"
|
18
|
+
def default
|
19
|
+
cucumber
|
20
|
+
end
|
21
|
+
|
22
|
+
worker_option
|
23
|
+
desc "cucumber [CUCUMBER_ARGS]", "parallelizes cucumber with custom arguments"
|
24
|
+
def cucumber(*)
|
25
|
+
Flatware.verbose = options[:log]
|
26
|
+
Worker.spawn workers
|
27
|
+
jobs = Cucumber.extract_jobs_from_args args
|
28
|
+
fork do
|
29
|
+
log "dispatch"
|
30
|
+
$0 = 'flatware dispatcher'
|
31
|
+
Dispatcher.start jobs
|
32
|
+
end
|
33
|
+
log "bossman"
|
34
|
+
$0 = 'flatware sink'
|
35
|
+
Sink.start_server jobs
|
36
|
+
Process.waitall
|
37
|
+
end
|
38
|
+
|
39
|
+
worker_option
|
40
|
+
desc "fan [COMMAND]", "executes the given job on all of the workers"
|
41
|
+
def fan(*command)
|
42
|
+
Flatware.verbose = options[:log]
|
43
|
+
|
44
|
+
command = command.join(" ")
|
45
|
+
puts "Running '#{command}' on #{workers} workers"
|
46
|
+
|
47
|
+
workers.times do |i|
|
48
|
+
fork do
|
49
|
+
exec({"TEST_ENV_NUMBER" => i.to_s}, command)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
Process.waitall
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
desc "clear", "kills all flatware processes"
|
57
|
+
def clear
|
58
|
+
`ps -c -opid,command | grep flatware | cut -f 1 -d ' ' | xargs kill -6`
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def log(*args)
|
64
|
+
Flatware.log(*args)
|
65
|
+
end
|
66
|
+
|
67
|
+
def workers
|
68
|
+
options[:workers]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'cucumber'
|
2
|
+
require_relative 'cucumber/runtime'
|
3
|
+
module Flatware
|
4
|
+
module Cucumber
|
5
|
+
autoload :Formatter, 'flatware/cucumber/formatter'
|
6
|
+
autoload :ProgressString, 'flatware/cucumber/formatter'
|
7
|
+
|
8
|
+
FORMATS = {
|
9
|
+
:passed => '.',
|
10
|
+
:failed => 'F',
|
11
|
+
:undefined => 'U',
|
12
|
+
:pending => 'P',
|
13
|
+
:skipped => '-'
|
14
|
+
}
|
15
|
+
|
16
|
+
STATUSES = FORMATS.keys
|
17
|
+
|
18
|
+
extend self
|
19
|
+
|
20
|
+
attr_reader :jobs
|
21
|
+
|
22
|
+
def extract_jobs_from_args(args=[], out_stream=$stdout, error_stream=$stderr)
|
23
|
+
raw_args = args.dup
|
24
|
+
config = ::Cucumber::Cli::Configuration.new(out_stream, error_stream)
|
25
|
+
config.parse! args
|
26
|
+
options = raw_args - args
|
27
|
+
@jobs = config.feature_files.map { |file| Job.new file, options }
|
28
|
+
end
|
29
|
+
|
30
|
+
def run(feature_files=[], options=[])
|
31
|
+
runtime.run feature_files, options
|
32
|
+
end
|
33
|
+
|
34
|
+
def runtime
|
35
|
+
@runtime ||= Runtime.new
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
require 'cucumber/formatter/console'
|
2
|
+
module Flatware
|
3
|
+
module Cucumber
|
4
|
+
class Formatter
|
5
|
+
def initialize(step_mother, *)
|
6
|
+
@step_mother = step_mother
|
7
|
+
end
|
8
|
+
|
9
|
+
def scenario_name(keyword, name, file_colon_line, source_indent)
|
10
|
+
@current_scenario = file_colon_line
|
11
|
+
end
|
12
|
+
|
13
|
+
def before_feature_element(feature_element)
|
14
|
+
case feature_element
|
15
|
+
when ::Cucumber::Ast::ScenarioOutline
|
16
|
+
@outline_steps = feature_element
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def after_feature_element(feature_element)
|
21
|
+
@outline_steps = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def after_feature(*)
|
25
|
+
background_steps.each do |step|
|
26
|
+
Sink.push step
|
27
|
+
end unless current_scenario
|
28
|
+
end
|
29
|
+
|
30
|
+
def after_step_result(keyword, step_match, multiline_arg, status, exception, source_indent, background)
|
31
|
+
result = if status_only? background
|
32
|
+
background_steps << Result.background(status, exception)
|
33
|
+
Result.status status
|
34
|
+
else
|
35
|
+
Result.step status, exception, current_scenario
|
36
|
+
end
|
37
|
+
|
38
|
+
Sink.push result
|
39
|
+
end
|
40
|
+
|
41
|
+
def before_outline_table(outline_table)
|
42
|
+
@outline_table = outline_table
|
43
|
+
end
|
44
|
+
|
45
|
+
def after_outline_table(outline_table)
|
46
|
+
@outline_table = nil
|
47
|
+
end
|
48
|
+
|
49
|
+
def before_table_row(table_row)
|
50
|
+
if example_row? table_row
|
51
|
+
@step_collector = StepCollector.new(step_mother)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def after_table_row(table_row)
|
56
|
+
if example_row? table_row
|
57
|
+
step_collector.stop table_row
|
58
|
+
Sink.push Result.new step_collector.progress, step_collector.steps
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def table_cell_value(_, status)
|
63
|
+
Sink.push Result.status status if example_cell? status
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
attr_reader :step_mother, :step_collector, :current_scenario
|
69
|
+
|
70
|
+
def background_steps
|
71
|
+
@background_steps ||= []
|
72
|
+
end
|
73
|
+
|
74
|
+
def status_only?(background)
|
75
|
+
scenario_outline? or (background and not current_scenario)
|
76
|
+
end
|
77
|
+
|
78
|
+
def scenario_outline?
|
79
|
+
!!@outline_steps
|
80
|
+
end
|
81
|
+
|
82
|
+
def example_row?(table_row)
|
83
|
+
outline_table? and not table_header_row? table_row
|
84
|
+
end
|
85
|
+
|
86
|
+
def example_cell?(status)
|
87
|
+
outline_table? and not table_header_cell? status
|
88
|
+
end
|
89
|
+
|
90
|
+
def table_header_cell?(status)
|
91
|
+
status == :skipped_param
|
92
|
+
end
|
93
|
+
|
94
|
+
def outline_table?
|
95
|
+
!!@outline_table
|
96
|
+
end
|
97
|
+
|
98
|
+
def table_header_row?(table_row)
|
99
|
+
table_row.failed?
|
100
|
+
rescue ::Cucumber::Ast::OutlineTable::ExampleRow::InvalidForHeaderRowError
|
101
|
+
true
|
102
|
+
else
|
103
|
+
false
|
104
|
+
end
|
105
|
+
|
106
|
+
class StepCollector
|
107
|
+
attr_reader :step_mother
|
108
|
+
def initialize(step_mother)
|
109
|
+
@step_mother = step_mother
|
110
|
+
snapshot_steps
|
111
|
+
end
|
112
|
+
|
113
|
+
def stop(table_row)
|
114
|
+
@scenario_id = extract_scenario(table_row)
|
115
|
+
@example_row_steps = step_mother.steps - ran_steps
|
116
|
+
end
|
117
|
+
|
118
|
+
def steps
|
119
|
+
example_row_steps or raise('stop collecting first')
|
120
|
+
example_row_steps.map do |step|
|
121
|
+
StepResult.new(step.status, step.exception, scenario_id)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def progress
|
126
|
+
''
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
attr_reader :example_row_steps, :scenario_id, :ran_steps
|
132
|
+
|
133
|
+
def extract_scenario(table_row)
|
134
|
+
table_row.scenario_outline.file_colon_line(table_row.line)
|
135
|
+
end
|
136
|
+
|
137
|
+
def snapshot_steps
|
138
|
+
@ran_steps = step_mother.steps.dup
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
class ProgressString
|
144
|
+
extend ::Cucumber::Formatter::Console
|
145
|
+
def self.format(status)
|
146
|
+
return '' unless status
|
147
|
+
format_string FORMATS[status], status
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'cucumber'
|
2
|
+
|
3
|
+
module Flatware
|
4
|
+
module Cucumber
|
5
|
+
class Runtime < ::Cucumber::Runtime
|
6
|
+
|
7
|
+
attr_accessor :configuration, :loader
|
8
|
+
attr_reader :out, :err
|
9
|
+
attr_reader :visitor
|
10
|
+
|
11
|
+
def initialize(out=StringIO.new, err=out)
|
12
|
+
@out, @err = out, err
|
13
|
+
super(default_configuration)
|
14
|
+
load_step_definitions
|
15
|
+
@results = Results.new(configuration)
|
16
|
+
end
|
17
|
+
|
18
|
+
def default_configuration
|
19
|
+
config = ::Cucumber::Cli::Configuration.new
|
20
|
+
config.parse! []
|
21
|
+
config
|
22
|
+
end
|
23
|
+
|
24
|
+
def run(feature_files=[], options=[])
|
25
|
+
@loader = nil
|
26
|
+
options = Array(feature_files) + %w[--format Flatware::Cucumber::Formatter] + options
|
27
|
+
|
28
|
+
configure(::Cucumber::Cli::Main.new(options, out, err).configuration)
|
29
|
+
|
30
|
+
self.visitor = configuration.build_tree_walker(self)
|
31
|
+
visitor.visit_features(features)
|
32
|
+
results
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Flatware
|
2
|
+
class Dispatcher
|
3
|
+
DISPATCH_PORT = 'ipc://dispatch'
|
4
|
+
|
5
|
+
def self.start(jobs=Cucumber.jobs)
|
6
|
+
new(jobs).dispatch!
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(jobs)
|
10
|
+
@jobs = jobs
|
11
|
+
end
|
12
|
+
|
13
|
+
def dispatch!
|
14
|
+
return Flatware.close if jobs.empty?
|
15
|
+
fireable.until_fired dispatch do |request|
|
16
|
+
if job = jobs.pop
|
17
|
+
dispatch.send Marshal.dump job
|
18
|
+
else
|
19
|
+
dispatch.send 'seppuku'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
attr_reader :jobs
|
27
|
+
|
28
|
+
def fireable
|
29
|
+
@fireable ||= Fireable.new
|
30
|
+
end
|
31
|
+
|
32
|
+
def dispatch
|
33
|
+
@dispatch ||= Flatware.socket(ZMQ::REP).tap do |socket|
|
34
|
+
socket.bind DISPATCH_PORT
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Flatware
|
2
|
+
class Fireable
|
3
|
+
def initialize
|
4
|
+
@die = Flatware.socket(ZMQ::SUB).tap do |die|
|
5
|
+
die.connect 'ipc://die'
|
6
|
+
die.setsockopt ZMQ::SUBSCRIBE, ''
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :die
|
11
|
+
|
12
|
+
def until_fired(sockets=[], &block)
|
13
|
+
while ready = ZMQ.select(Array(sockets) + [die])
|
14
|
+
messages = ready.flatten.compact.map(&:recv)
|
15
|
+
break if messages.include? 'seppuku'
|
16
|
+
messages.each &block
|
17
|
+
end
|
18
|
+
ensure
|
19
|
+
Flatware.close
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Flatware
|
2
|
+
class Result
|
3
|
+
attr_reader :progress, :steps
|
4
|
+
|
5
|
+
def initialize(progress, steps=nil)
|
6
|
+
@progress, @steps = progress, steps || []
|
7
|
+
end
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def step(*args)
|
11
|
+
step = StepResult.new *args
|
12
|
+
new step.progress, [step]
|
13
|
+
end
|
14
|
+
|
15
|
+
def status(status)
|
16
|
+
new Cucumber::ProgressString.format status
|
17
|
+
end
|
18
|
+
|
19
|
+
def background(status, exception)
|
20
|
+
new '', [StepResult.new(status, exception)]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Flatware
|
2
|
+
class ScenarioResult
|
3
|
+
attr_reader :id, :steps
|
4
|
+
|
5
|
+
def initialize(id, steps=[])
|
6
|
+
@id = id
|
7
|
+
@steps = steps
|
8
|
+
end
|
9
|
+
|
10
|
+
def status
|
11
|
+
first(:failed) || first(:undefined) || :passed
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def first(status)
|
17
|
+
statuses.detect {|s| s == status}
|
18
|
+
end
|
19
|
+
|
20
|
+
def statuses
|
21
|
+
steps.map &:status
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'flatware'
|
2
|
+
require 'flatware/cucumber/formatter'
|
3
|
+
module Flatware
|
4
|
+
class Sink
|
5
|
+
class << self
|
6
|
+
def push(message)
|
7
|
+
client.push Marshal.dump message
|
8
|
+
end
|
9
|
+
|
10
|
+
def finished(job)
|
11
|
+
push job
|
12
|
+
end
|
13
|
+
|
14
|
+
def start_server(jobs=Cucumber.jobs, out_stream=$stdout, error_stream=$stderr)
|
15
|
+
Server.new(jobs, out_stream, error_stream).start
|
16
|
+
end
|
17
|
+
|
18
|
+
def client
|
19
|
+
@client ||= Client.new
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Server
|
24
|
+
def initialize(jobs, out, error)
|
25
|
+
@jobs, @out, @error = jobs, out, error
|
26
|
+
end
|
27
|
+
|
28
|
+
def start
|
29
|
+
trap 'INT' do
|
30
|
+
summarize
|
31
|
+
summarize_remaining
|
32
|
+
end
|
33
|
+
|
34
|
+
before_firing { listen }
|
35
|
+
Flatware.close
|
36
|
+
end
|
37
|
+
|
38
|
+
def listen
|
39
|
+
until done?
|
40
|
+
message = socket.recv
|
41
|
+
case (result = Marshal.load message)
|
42
|
+
when Result
|
43
|
+
print result.progress
|
44
|
+
steps.push *result.steps
|
45
|
+
when Job
|
46
|
+
completed_jobs << result
|
47
|
+
log "COMPLETED SCENARIO"
|
48
|
+
else
|
49
|
+
log "i don't know that message, bro."
|
50
|
+
end
|
51
|
+
end
|
52
|
+
summarize
|
53
|
+
rescue ZMQ::Error => e
|
54
|
+
raise unless e.message == "Interrupted system call"
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
attr_reader :out, :jobs
|
60
|
+
|
61
|
+
def print(*args)
|
62
|
+
out.print *args
|
63
|
+
end
|
64
|
+
|
65
|
+
def puts(*args)
|
66
|
+
out.puts *args
|
67
|
+
end
|
68
|
+
|
69
|
+
def summarize
|
70
|
+
Summary.new(steps, out).summarize
|
71
|
+
end
|
72
|
+
|
73
|
+
def summarize_remaining
|
74
|
+
return if remaining_work.empty?
|
75
|
+
puts
|
76
|
+
puts "The following features have not been run:"
|
77
|
+
for job in remaining_work
|
78
|
+
puts job.id
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def log(*args)
|
83
|
+
Flatware.log *args
|
84
|
+
end
|
85
|
+
|
86
|
+
def before_firing(&block)
|
87
|
+
die = Flatware.socket(ZMQ::PUB).tap do |socket|
|
88
|
+
socket.bind 'ipc://die'
|
89
|
+
end
|
90
|
+
block.call
|
91
|
+
die.send 'seppuku'
|
92
|
+
end
|
93
|
+
|
94
|
+
def steps
|
95
|
+
@steps ||= []
|
96
|
+
end
|
97
|
+
|
98
|
+
def completed_jobs
|
99
|
+
@completed_jobs ||= []
|
100
|
+
end
|
101
|
+
|
102
|
+
def done?
|
103
|
+
log remaining_work
|
104
|
+
remaining_work.empty?
|
105
|
+
end
|
106
|
+
|
107
|
+
def remaining_work
|
108
|
+
jobs - completed_jobs
|
109
|
+
end
|
110
|
+
|
111
|
+
def fireable
|
112
|
+
@fireable ||= Fireable.new
|
113
|
+
end
|
114
|
+
|
115
|
+
def socket
|
116
|
+
@socket ||= Flatware.socket(ZMQ::PULL).tap do |socket|
|
117
|
+
socket.bind 'ipc://sink'
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
class Client
|
123
|
+
def push(message)
|
124
|
+
socket.send message
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
def socket
|
130
|
+
@socket ||= Flatware.socket(ZMQ::PUSH).tap do |socket|
|
131
|
+
socket.connect 'ipc://sink'
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Flatware
|
2
|
+
class StepResult
|
3
|
+
attr_reader :status, :exception, :scenario_id
|
4
|
+
|
5
|
+
def initialize(status, exception, scenario_id=nil)
|
6
|
+
@status, @exception, @scenario_id = status, serialized(exception), scenario_id
|
7
|
+
end
|
8
|
+
|
9
|
+
def passed?
|
10
|
+
status == :passed
|
11
|
+
end
|
12
|
+
|
13
|
+
def failed?
|
14
|
+
status == :failed
|
15
|
+
end
|
16
|
+
|
17
|
+
def progress
|
18
|
+
Cucumber::ProgressString.format(status)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def serialized(e)
|
23
|
+
SerializedException.new(e.class, e.message, e.backtrace) if e
|
24
|
+
end
|
25
|
+
|
26
|
+
class SerializedException
|
27
|
+
attr_reader :class, :message, :backtrace
|
28
|
+
def initialize(klass, message, backtrace)
|
29
|
+
@class, @message, @backtrace = serialized(klass), message, backtrace
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
def serialized(klass)
|
34
|
+
SerializedClass.new(klass.to_s)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class SerializedClass
|
39
|
+
attr_reader :name
|
40
|
+
alias to_s name
|
41
|
+
def initialize(name); @name = name end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'cucumber/formatter/console'
|
2
|
+
module Flatware
|
3
|
+
class Summary
|
4
|
+
include ::Cucumber::Formatter::Console
|
5
|
+
attr_reader :io, :steps
|
6
|
+
|
7
|
+
def initialize(steps, io=StringIO.new)
|
8
|
+
@io = io
|
9
|
+
@steps = steps
|
10
|
+
end
|
11
|
+
|
12
|
+
def scenarios
|
13
|
+
@scenarios ||= scenario_steps.group_by(&:scenario_id).map do |scenario, steps|
|
14
|
+
ScenarioResult.new(scenario, steps)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def summarize
|
19
|
+
2.times { io.puts }
|
20
|
+
print_steps :failed
|
21
|
+
print_counts 'scenario', scenarios
|
22
|
+
print_counts 'step', steps
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def scenario_steps
|
28
|
+
steps.select &:scenario_id
|
29
|
+
end
|
30
|
+
|
31
|
+
def print_steps(status)
|
32
|
+
print_elements steps.select(&with_status(status)), status, 'steps'
|
33
|
+
end
|
34
|
+
|
35
|
+
def print_counts(label, collection)
|
36
|
+
io.puts pluralize(label, collection.size) + count_summary(collection)
|
37
|
+
end
|
38
|
+
|
39
|
+
def pluralize(word, number)
|
40
|
+
"#{number} #{number == 1 ? word : word + 's'}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def with_status(status)
|
44
|
+
proc {|r| r.status == status}
|
45
|
+
end
|
46
|
+
|
47
|
+
def count_summary(results)
|
48
|
+
return "" unless results.any?
|
49
|
+
status_counts = Cucumber::STATUSES.map do |status|
|
50
|
+
count = results.select(&with_status(status)).size
|
51
|
+
format_string "#{count} #{status}", status if count > 0
|
52
|
+
end.compact.join ", "
|
53
|
+
|
54
|
+
" (#{status_counts})"
|
55
|
+
end
|
56
|
+
|
57
|
+
def count(status)
|
58
|
+
completed_scenarios.select {|scenario| scenario.status == status}.count
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
module Flatware
|
3
|
+
class Worker
|
4
|
+
|
5
|
+
def self.listen!
|
6
|
+
new.listen
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.spawn(worker_count)
|
10
|
+
worker_count.times do |i|
|
11
|
+
fork do
|
12
|
+
$0 = "flatware worker #{i}"
|
13
|
+
ENV['TEST_ENV_NUMBER'] = i.to_s
|
14
|
+
listen!
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def listen
|
20
|
+
time = Benchmark.realtime do
|
21
|
+
fireable
|
22
|
+
report_for_duty
|
23
|
+
fireable.until_fired task do |work|
|
24
|
+
job = Marshal.load work
|
25
|
+
log 'working!'
|
26
|
+
Cucumber.run job.id, job.args
|
27
|
+
Sink.finished job
|
28
|
+
report_for_duty
|
29
|
+
log 'waiting'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
log time
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def log(*args)
|
38
|
+
Flatware.log *args
|
39
|
+
end
|
40
|
+
|
41
|
+
def fireable
|
42
|
+
@fireable ||= Fireable.new
|
43
|
+
end
|
44
|
+
|
45
|
+
def task
|
46
|
+
@task ||= Flatware.socket(ZMQ::REQ).tap do |task|
|
47
|
+
task.connect Dispatcher::DISPATCH_PORT
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def report_for_duty
|
52
|
+
task.send 'ready'
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
metadata
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: flatware
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Brian Dunn
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-02-24 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: zmq
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: thor
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 0.15.0
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 0.15.0
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: cucumber
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.1.0
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.1.0
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: aruba
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rake
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: rspec
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
description: A distributed cucumber runner
|
111
|
+
email: brian@hashrocket.com
|
112
|
+
executables:
|
113
|
+
- flatware
|
114
|
+
extensions: []
|
115
|
+
extra_rdoc_files:
|
116
|
+
- LICENSE.txt
|
117
|
+
- README.rdoc
|
118
|
+
files:
|
119
|
+
- lib/flatware.rb
|
120
|
+
- lib/flatware/cli.rb
|
121
|
+
- lib/flatware/cucumber.rb
|
122
|
+
- lib/flatware/cucumber/formatter.rb
|
123
|
+
- lib/flatware/cucumber/runtime.rb
|
124
|
+
- lib/flatware/dispatcher.rb
|
125
|
+
- lib/flatware/fireable.rb
|
126
|
+
- lib/flatware/result.rb
|
127
|
+
- lib/flatware/scenario_result.rb
|
128
|
+
- lib/flatware/sink.rb
|
129
|
+
- lib/flatware/step_result.rb
|
130
|
+
- lib/flatware/summary.rb
|
131
|
+
- lib/flatware/worker.rb
|
132
|
+
- LICENSE.txt
|
133
|
+
- README.rdoc
|
134
|
+
- bin/flatware
|
135
|
+
homepage: http://github.com/briandunn/flatware
|
136
|
+
licenses:
|
137
|
+
- MIT
|
138
|
+
post_install_message:
|
139
|
+
rdoc_options: []
|
140
|
+
require_paths:
|
141
|
+
- lib
|
142
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
143
|
+
none: false
|
144
|
+
requirements:
|
145
|
+
- - ! '>='
|
146
|
+
- !ruby/object:Gem::Version
|
147
|
+
version: '0'
|
148
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
149
|
+
none: false
|
150
|
+
requirements:
|
151
|
+
- - ! '>='
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '0'
|
154
|
+
requirements: []
|
155
|
+
rubyforge_project:
|
156
|
+
rubygems_version: 1.8.24
|
157
|
+
signing_key:
|
158
|
+
specification_version: 3
|
159
|
+
summary: A distributed cucumber runner
|
160
|
+
test_files: []
|