brynary-testjour 0.1.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.
@@ -0,0 +1,124 @@
1
+ #
2
+ # Ruby/ProgressBar - a text progress bar library
3
+ #
4
+ # Copyright (C) 2001 Satoru Takabayashi <satoru@namazu.org>
5
+ # All rights reserved.
6
+ # This is free software with ABSOLUTELY NO WARRANTY.
7
+ #
8
+ # You can redistribute it and/or modify it under the terms
9
+ # of Ruby's licence.
10
+ #
11
+
12
+ class ProgressBar
13
+ VERSION = "0.3"
14
+
15
+ attr_accessor :colorer
16
+ attr_writer :title
17
+
18
+ def initialize (title, total, out = STDERR)
19
+ @title = title
20
+ @total = total
21
+ @out = out
22
+ @current = 0
23
+ @previous = 0
24
+ @is_finished = false
25
+ @start_time = Time.now
26
+ show_progress
27
+ end
28
+
29
+ def inspect
30
+ "(ProgressBar: #{@current}/#{@total})"
31
+ end
32
+
33
+ def format_time (t)
34
+ t = t.to_i
35
+ sec = t % 60
36
+ min = (t / 60) % 60
37
+ hour = t / 3600
38
+ sprintf("%02d:%02d:%02d", hour, min, sec);
39
+ end
40
+
41
+ # ETA stands for Estimated Time of Arrival.
42
+ def eta
43
+ if @current == 0
44
+ "ETA: --:--:--"
45
+ else
46
+ elapsed = Time.now - @start_time
47
+ eta = elapsed * @total / @current - elapsed;
48
+ sprintf("ETA: %s", format_time(eta))
49
+ end
50
+ end
51
+
52
+ def elapsed
53
+ elapsed = Time.now - @start_time
54
+ sprintf("Time: %s", format_time(elapsed))
55
+ end
56
+
57
+ def time
58
+ if @is_finished then elapsed else eta end
59
+ end
60
+
61
+ def eol
62
+ if @is_finished then "\n" else "\r" end
63
+ end
64
+
65
+ def bar(percentage)
66
+ @bar = "=" * 41
67
+ len = percentage * (@bar.length + 1) / 100
68
+ sprintf("[%.*s%s%*s]", len, @bar, ">", [@bar.size - len, 0].max, "")
69
+ end
70
+
71
+ def show (percentage)
72
+ output = sprintf("%-25s %3d%% %s %s%s",
73
+ @title[0,25],
74
+ percentage,
75
+ bar(percentage),
76
+ time,
77
+ eol
78
+ )
79
+
80
+ unless @colorer.nil?
81
+ output = colorer.call(output)
82
+ end
83
+
84
+ @out.print(output)
85
+ end
86
+
87
+ def show_progress
88
+ if @total.zero?
89
+ cur_percentage = 100
90
+ prev_percentage = 0
91
+ else
92
+ cur_percentage = (@current * 100 / @total).to_i
93
+ prev_percentage = (@previous * 100 / @total).to_i
94
+ end
95
+
96
+ if cur_percentage > prev_percentage || @is_finished
97
+ show(cur_percentage)
98
+ end
99
+ end
100
+
101
+ public
102
+ def finish
103
+ @current = @total
104
+ @is_finished = true
105
+ show_progress
106
+ end
107
+
108
+ def set (count)
109
+ if count < 0 || count > @total
110
+ raise "invalid count: #{count} (total: #{total})"
111
+ end
112
+ @current = count
113
+ show_progress
114
+ @previous = @current
115
+ end
116
+
117
+ def inc (step = 1)
118
+ @current += step
119
+ @current = @total if @current > @total
120
+ show_progress
121
+ @previous = @current
122
+ end
123
+ end
124
+
@@ -0,0 +1,66 @@
1
+ require "thread"
2
+ require "drb"
3
+ require "timeout"
4
+
5
+ module Testjour
6
+
7
+ class QueueServer
8
+ TIMEOUT_IN_SECONDS = 60
9
+
10
+ def self.with_server
11
+ server = new
12
+ DRb.start_service(nil, server)
13
+ yield server
14
+ end
15
+
16
+ def self.stop
17
+ DRb.stop_service
18
+ end
19
+
20
+ def initialize
21
+ reset
22
+ end
23
+
24
+ def reset
25
+ @work_queue = Queue.new
26
+ @result_queue = Queue.new
27
+ end
28
+
29
+ def done_with_work
30
+ @done_with_work = true
31
+ end
32
+
33
+ def take_result
34
+ Timeout.timeout(TIMEOUT_IN_SECONDS, ResultOverdueError) do
35
+ @result_queue.pop
36
+ end
37
+ end
38
+
39
+ def take_work
40
+ raise NoWorkUnitsRemainingError if @done_with_work
41
+
42
+ @work_queue.pop(true)
43
+ rescue Object => ex
44
+ if ex.message == "queue empty"
45
+ raise NoWorkUnitsAvailableError
46
+ else
47
+ raise
48
+ end
49
+ end
50
+
51
+ def write_result(uri, dot, message = nil, backtrace = [])
52
+ @result_queue.push [uri, dot, message.to_s, backtrace.join("\n")]
53
+ nil
54
+ end
55
+
56
+ def write_work(work_unit)
57
+ @work_queue.push work_unit
58
+ nil
59
+ end
60
+
61
+ class NoWorkUnitsAvailableError < StandardError; end
62
+ class NoWorkUnitsRemainingError < StandardError; end
63
+ class ResultOverdueError < StandardError; end
64
+ end
65
+
66
+ end
@@ -0,0 +1,29 @@
1
+ require "uri"
2
+
3
+ module Testjour
4
+
5
+ class RsyncFailed < StandardError
6
+ end
7
+
8
+ class Rsync
9
+
10
+ def self.copy_to_current_directory_from(source_uri)
11
+ destination_dir = File.expand_path(".")
12
+ uri = URI.parse(source_uri)
13
+
14
+ command = "rsync -az --delete --exclude=.git --exclude=*.log --exclude=*.pid #{uri.user}@#{uri.host}:#{uri.path}/ #{destination_dir}"
15
+
16
+ Testjour.logger.info "Rsyncing: #{command}"
17
+ start_time = Time.now
18
+ successful = system command
19
+
20
+ if successful
21
+ time = Time.now - start_time
22
+ Testjour.logger.debug("Rsync finished in %.2fs" % time)
23
+ else
24
+ raise RsyncFailed.new
25
+ end
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,98 @@
1
+ require "thread"
2
+ require "drb"
3
+ require "uri"
4
+ require "timeout"
5
+ require "systemu"
6
+
7
+ module Testjour
8
+
9
+ class SlaveServer
10
+
11
+ def self.start
12
+ server = self.new
13
+ DRb.start_service(nil, server)
14
+ uri = URI.parse(DRb.uri)
15
+ return uri.port.to_i
16
+ end
17
+
18
+ def self.stop
19
+ DRb.stop_service
20
+ end
21
+
22
+ def status
23
+ running? ? "busy" : "available"
24
+ end
25
+
26
+ def warm(queue_server_url)
27
+ if running?
28
+ Testjour.logger.info "Not running because pid exists: #{@pid}"
29
+ return false
30
+ end
31
+
32
+ pid_queue = Queue.new
33
+ @pid = nil
34
+
35
+ Thread.new do
36
+ Thread.current.abort_on_exception = true
37
+ cmd = command_to_warm_for(queue_server_url)
38
+ Testjour.logger.debug "Starting warm with command: #{cmd}"
39
+ status, stdout, stderr = systemu(cmd) { |pid| pid_queue << pid }
40
+ Testjour.logger.warn stderr if stderr.strip.size > 0
41
+ end
42
+
43
+ @pid = pid_queue.pop
44
+
45
+ Testjour.logger.info "Warming from server #{queue_server_url} on PID #{@pid}"
46
+
47
+ return @pid
48
+ end
49
+
50
+ def run(queue_server_url, cucumber_options)
51
+ if running?
52
+ Testjour.logger.info "Not running because pid exists: #{@pid}"
53
+ return false
54
+ end
55
+
56
+ pid_queue = Queue.new
57
+ @pid = nil
58
+
59
+ Thread.new do
60
+ Thread.current.abort_on_exception = true
61
+ cmd = command_to_run_for(queue_server_url, cucumber_options)
62
+ Testjour.logger.debug "Starting runner with command: #{cmd}"
63
+ status, stdout, stderr = systemu(cmd) { |pid| pid_queue << pid }
64
+ Testjour.logger.warn stderr if stderr.strip.size > 0
65
+ end
66
+
67
+ @pid = pid_queue.pop
68
+
69
+ Testjour.logger.info "Running tests from queue #{queue_server_url} on PID #{@pid}"
70
+
71
+ return @pid
72
+ end
73
+
74
+ protected
75
+
76
+ def command_to_run_for(master_server_uri, cucumber_options)
77
+ "#{testjour_bin_path} slave:run #{master_server_uri} -- #{cucumber_options.join(' ')}".strip
78
+ end
79
+
80
+ def command_to_warm_for(master_server_uri)
81
+ "#{testjour_bin_path} slave:warm #{master_server_uri}".strip
82
+ end
83
+
84
+ def testjour_bin_path
85
+ File.expand_path(File.dirname(__FILE__) + "/../../bin/testjour")
86
+ end
87
+
88
+ def running?
89
+ return false unless @pid
90
+ Process::kill 0, @pid.to_s.to_i
91
+ true
92
+ rescue Errno::ESRCH, Errno::EPERM
93
+ false
94
+ end
95
+
96
+ end
97
+
98
+ end
data/lib/testjour.rb ADDED
@@ -0,0 +1,68 @@
1
+ require "rubygems"
2
+ require "English"
3
+ require "logger"
4
+
5
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__))) unless $LOAD_PATH.include?(File.expand_path(File.dirname(__FILE__)))
6
+
7
+ module Kernel
8
+ # Options:
9
+ # * :tries - Number of retries to perform. Defaults to 1.
10
+ # * :on - The Exception on which a retry will be performed. Defaults to Exception, which retries on any Exception.
11
+ #
12
+ # Example
13
+ # =======
14
+ # retryable(:tries => 1, :on => OpenURI::HTTPError) do
15
+ # # your code here
16
+ # end
17
+ #
18
+ def retryable(options = {}, &block)
19
+ opts = { :tries => 1, :on => Exception }.merge(options)
20
+
21
+ retry_exception, retries = opts[:on], opts[:tries]
22
+
23
+ begin
24
+ return yield
25
+ rescue retry_exception
26
+ retry if (retries -= 1) > 0
27
+ end
28
+
29
+ yield
30
+ end
31
+ end
32
+
33
+ module Testjour
34
+ VERSION = '0.1.0'
35
+
36
+ class << self
37
+ attr_accessor :step_mother
38
+ attr_accessor :executor
39
+ end
40
+
41
+ def self.load_cucumber
42
+ $LOAD_PATH.unshift(File.expand_path("./vendor/plugins/cucumber/lib"))
43
+
44
+ require "cucumber"
45
+ require "cucumber/formatters/ansicolor"
46
+ require "cucumber/treetop_parser/feature_en"
47
+ Cucumber.load_language("en")
48
+
49
+ # Expose this because we need it
50
+ class << Cucumber::CLI
51
+ attr_reader :executor
52
+ attr_reader :step_mother
53
+ end
54
+ end
55
+
56
+ def self.logger
57
+ return @logger if @logger
58
+ setup_logger
59
+ @logger
60
+ end
61
+
62
+ def self.setup_logger
63
+ @logger = Logger.new("testjour.log")
64
+ @logger.formatter = proc { |severity, time, progname, msg| "#{time.strftime("%b %d %H:%M:%S")} [#{$PID}]: #{msg}\n" }
65
+ @logger.level = Logger::DEBUG
66
+ end
67
+
68
+ end
data/vendor/authprogs ADDED
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/perl
2
+ #
3
+ # authprogs, Copyright 2003, Brian Hatch.
4
+ #
5
+ # Released under the GPL. See the file
6
+ # COPYING for more information.
7
+ #
8
+ # This program is intended to be called from an authorized_keys
9
+ # file, i.e. triggered by use of specific SSH identities.
10
+ #
11
+ # It will check the original command (saved in $SSH_ORIGINAL_COMMAND
12
+ # environment variable by sshd) and see if it is on the 'approved'
13
+ # list.
14
+ #
15
+ # Allowed commands are stored in ~/.ssh/authprogs.conf
16
+ # The format of this file is as follows:
17
+ #
18
+ # [ ALL ]
19
+ # command0 arg arg arg
20
+ #
21
+ # [ ip.ad.dr.01 ip.ad.dr.02 ]
22
+ # command1 arg arg arg
23
+ #
24
+ # [ ip.ad.dr.03 ]
25
+ # command2 arg arg arg
26
+ # command3 arg arg
27
+ #
28
+ # There is no regexp or shell metacharacter support. If
29
+ # you want to allow 'ls /dir1' and 'ls /dir2' you need to
30
+ # explicitly create those two rules. Putting "ls /dir[12]"
31
+ # in the authprogs.conf file will *not* work.
32
+ #
33
+ # NOTE: Some versions of Bash do not export the (already exported)
34
+ # SSH_CLIENT environment variable. You can get around this by adding
35
+ # export SSH_CLIENT=${SSH_CLIENT}
36
+ # or something similar in your ~/.bashrc, /etc/profile, etc.
37
+ # http://mail.gnu.org/archive/html/bug-bash/2002-01/msg00096.html
38
+ #
39
+ # Changes:
40
+ # 2003-10-27: fixed exit status, noted by Brad Fritz.
41
+ # 2003-10-27: added blank SSH_ORIGINAL_COMMAND debug log message
42
+
43
+
44
+ use strict;
45
+ use subs qw(bail log);
46
+ use POSIX qw(strftime);
47
+ use File::Basename;
48
+ use FileHandle;
49
+
50
+ # DEBUGLEVEL values:
51
+ # 0 - log nothing
52
+ # 1 - log errors
53
+ # 2 - log failed commands
54
+ # 3 - log successful commands
55
+ # 4 - log debugging info
56
+ my $DEBUGLEVEL = 4;
57
+
58
+ # Salt to taste. /dev/null might be a likely
59
+ # place if you don't want any logging.
60
+ my $LOGFILE = "$ENV{HOME}/.ssh/authprogs.log";
61
+
62
+ # Configfile - location of the host/commands allowed.
63
+ my $CONFIGFILE = "$ENV{HOME}/.ssh/authprogs.conf";
64
+
65
+ # $CLIENT_COMMAND is the string the client sends us.
66
+ #
67
+ # Unfortunately, the actual spacing is lost. IE
68
+ # ("some string" and "some" "string" are not differentiable.)
69
+ my ($CLIENT_COMMAND) = $ENV{SSH_ORIGINAL_COMMAND};
70
+
71
+ # strip quotes - we'll explain later on.
72
+ $CLIENT_COMMAND =~ s/['"]//g;
73
+
74
+ # Set CLIENT_IP to just the ip addr, sans port numbers.
75
+ my ($CLIENT_IP) = $ENV{SSH_CLIENT} =~ /^(\S+)/;
76
+
77
+
78
+ # Open log in append mode. Note that the use of '>>'
79
+ # means you better be doing it somewhere that is only
80
+ # writeable by you, lest you have a symlink/etc attack.
81
+ # Since we default to ~/.ssh, this should not be a problem.
82
+
83
+ if ( $DEBUGLEVEL ) {
84
+ open LOG, ">>$LOGFILE" or bail "Can't open $LOGFILE\n";
85
+ LOG->autoflush(1);
86
+ }
87
+
88
+ if ( ! $ENV{SSH_ORIGINAL_COMMAND} ) {
89
+ log(4, "SSH_ORIGINAL_COMMAND not set - either the client ".
90
+ "didn't send one, or your shell is removing it from ".
91
+ "the environment.");
92
+ }
93
+
94
+
95
+ # Ok, let's scan the authprogs.conf file
96
+ open CONFIGFILE, $CONFIGFILE or bail "Config '$CONFIGFILE' not readable!";
97
+
98
+ # Note: we do not verify that the configuration file is owned by
99
+ # this user. Some might argue that we should. (A quick stat
100
+ # compared to $< would do the trick.) However some installations
101
+ # relax the requirement that the .ssh dir is owned by the user
102
+ # s.t. it can be owned by root and only modifyable in that way to
103
+ # keep even the user from making changes. We should trust the
104
+ # administrator's SSH setup (StrictModes) and not bother checking
105
+ # the ownership/perms of configfile.
106
+
107
+ my $VALID_COMMAND=0; # flag: is this command appopriate for this host?
108
+
109
+ READ_CONF: while (<CONFIGFILE>) {
110
+ chomp;
111
+
112
+ # Skip blanks and comments.
113
+ if ( /^\s*#/ ) { next }
114
+ if ( /^\s*$/ ) { next }
115
+
116
+ # Are we the beginning of a new set of
117
+ # clients?
118
+ if ( /^\[/ ) {
119
+
120
+ # Snag the IP address(es) in question.
121
+
122
+ /^ \[ ( [^\]]+ ) \] /x;
123
+ $_ = $1;
124
+
125
+ if ( /^\s*ALL\s*$/ ) { # If wildcard selected
126
+ $_ = $CLIENT_IP;
127
+ }
128
+
129
+ my @clients = split;
130
+
131
+ log 4, "Found new clients line for @clients\n";
132
+
133
+ # This would be a great place to add
134
+ # ip <=> name mapping so we can have it work
135
+ # on hostnames rather than just IP addresses.
136
+ # If so, better make sure that forward and
137
+ # reverse lookups match -- an attacker in
138
+ # control of his network can easily set a PTR
139
+ # record, so don't rely on it alone.
140
+
141
+ unless ( grep /^$CLIENT_IP$/, @clients ) {
142
+
143
+ log 4, "Client IP does not match this list.\n";
144
+
145
+ $VALID_COMMAND=0;
146
+
147
+ # Nope, not relevant - go to next
148
+ # host definition list.
149
+ while (<CONFIGFILE>) {
150
+ last if /^\[/;
151
+ }
152
+
153
+ # Never found another host definition. Bail.
154
+ redo READ_CONF;
155
+ }
156
+ $VALID_COMMAND=1;
157
+ log 4, "Client matches this list.\n";
158
+
159
+ next;
160
+ }
161
+
162
+ # We must be a potential command
163
+ if ( ! $VALID_COMMAND ) {
164
+ bail "Parsing error at line $. of $CONFIGFILE\n";
165
+ }
166
+
167
+
168
+ my $allowed_command = $_;
169
+ $allowed_command =~ s/\s+$//; # strip trailing slashes
170
+ $allowed_command =~ s/^\s+//; # strip leading slashes
171
+
172
+ # We've now got the command as we'd run it through 'system'.
173
+ #
174
+ # Problem: SSH sticks the command in $SSH_ORIGINAL_COMMAND
175
+ # but doesn't retain the argument breaks.
176
+ #
177
+ # Solution: Let's guess by stripping double and single quotes
178
+ # from both the client and the config file. If those
179
+ # versions match, we'll assume the client was right.
180
+
181
+ my $allowed_command_sans_quotes = $allowed_command;
182
+ $allowed_command_sans_quotes =~ s/["']//g;
183
+
184
+ log 4, "Comparing allowed command and client's command:\n";
185
+ log 4, " Allowed: $allowed_command_sans_quotes\n";
186
+ log 4, " Client: $CLIENT_COMMAND\n";
187
+
188
+ if ( $allowed_command_sans_quotes eq $CLIENT_COMMAND ) {
189
+ log 3, "Running [$allowed_command] from $ENV{SSH_CLIENT}\n";
190
+
191
+ # System is a bad thing to use on untrusted input.
192
+ # But $allowed_command comes from the user on the SSH
193
+ # server, from his authprogs.conf file. So we can trust
194
+ # it as much as we trust him, since it's running as that
195
+ # user.
196
+
197
+ system $allowed_command;
198
+ exit $? >> 8;
199
+ }
200
+
201
+ }
202
+
203
+ # The remote end wants to run something they're not allowed to run.
204
+ # Log it, and chastize them.
205
+
206
+ log 2, "Denying request '$ENV{SSH_ORIGINAL_COMMAND}' from $ENV{SSH_CLIENT}\n";
207
+ print STDERR "You're not allowed to run '$ENV{SSH_ORIGINAL_COMMAND}'\n";
208
+ exit 1;
209
+
210
+
211
+ sub bail {
212
+ # print log message (w/ guarenteed newline)
213
+ if (@_) {
214
+ $_ = join '', @_;
215
+ chomp $_;
216
+ log 1, "$_\n";
217
+ }
218
+
219
+ close LOG if $DEBUGLEVEL;
220
+ exit 1
221
+ }
222
+
223
+ sub log {
224
+ my ($level,@list) = @_;
225
+ return if $DEBUGLEVEL < $level;
226
+
227
+ my $timestamp = strftime "%Y/%m/%d %H:%M:%S", localtime;
228
+ my $progname = basename $0;
229
+ grep { s/^/$timestamp $progname\[$$\]: / } @list;
230
+ print LOG @list;
231
+ }
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: brynary-testjour
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Bryan Helmkamp
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-12-09 00:00:00 -08:00
13
+ default_executable: testjour
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: systemu
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.2.0
23
+ version:
24
+ - !ruby/object:Gem::Dependency
25
+ name: dnssd
26
+ version_requirement:
27
+ version_requirements: !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: 0.6.0
32
+ version:
33
+ description: Distributed test running with autodiscovery via Bonjour (for Cucumber first)
34
+ email: bryan@brynary.com
35
+ executables:
36
+ - testjour
37
+ extensions: []
38
+
39
+ extra_rdoc_files: []
40
+
41
+ files:
42
+ - History.txt
43
+ - MIT-LICENSE.txt
44
+ - README.rdoc
45
+ - Rakefile
46
+ - bin/testjour
47
+ - lib/testjour
48
+ - lib/testjour/bonjour.rb
49
+ - lib/testjour/cli.rb
50
+ - lib/testjour/colorer.rb
51
+ - lib/testjour/commands
52
+ - lib/testjour/commands/base_command.rb
53
+ - lib/testjour/commands/help.rb
54
+ - lib/testjour/commands/list.rb
55
+ - lib/testjour/commands/local_run.rb
56
+ - lib/testjour/commands/run.rb
57
+ - lib/testjour/commands/slave_run.rb
58
+ - lib/testjour/commands/slave_start.rb
59
+ - lib/testjour/commands/slave_stop.rb
60
+ - lib/testjour/commands/slave_warm.rb
61
+ - lib/testjour/commands/version.rb
62
+ - lib/testjour/commands/warm.rb
63
+ - lib/testjour/commands.rb
64
+ - lib/testjour/cucumber_extensions
65
+ - lib/testjour/cucumber_extensions/drb_formatter.rb
66
+ - lib/testjour/cucumber_extensions/queueing_executor.rb
67
+ - lib/testjour/mysql.rb
68
+ - lib/testjour/pid_file.rb
69
+ - lib/testjour/progressbar.rb
70
+ - lib/testjour/queue_server.rb
71
+ - lib/testjour/rsync.rb
72
+ - lib/testjour/slave_server.rb
73
+ - lib/testjour.rb
74
+ - vendor/authprogs
75
+ has_rdoc: false
76
+ homepage: http://github.com/brynary/testjour
77
+ post_install_message:
78
+ rdoc_options: []
79
+
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: "0"
87
+ version:
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: "0"
93
+ version:
94
+ requirements: []
95
+
96
+ rubyforge_project:
97
+ rubygems_version: 1.2.0
98
+ signing_key:
99
+ specification_version: 2
100
+ summary: Distributed test running with autodiscovery via Bonjour (for Cucumber first)
101
+ test_files: []
102
+