brynary-testjour 0.1.0

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