brynary-testjour 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +6 -0
- data/MIT-LICENSE.txt +19 -0
- data/README.rdoc +47 -0
- data/Rakefile +39 -0
- data/bin/testjour +8 -0
- data/lib/testjour/bonjour.rb +56 -0
- data/lib/testjour/cli.rb +78 -0
- data/lib/testjour/colorer.rb +8 -0
- data/lib/testjour/commands/base_command.rb +53 -0
- data/lib/testjour/commands/help.rb +20 -0
- data/lib/testjour/commands/list.rb +57 -0
- data/lib/testjour/commands/local_run.rb +83 -0
- data/lib/testjour/commands/run.rb +165 -0
- data/lib/testjour/commands/slave_run.rb +88 -0
- data/lib/testjour/commands/slave_start.rb +65 -0
- data/lib/testjour/commands/slave_stop.rb +25 -0
- data/lib/testjour/commands/slave_warm.rb +31 -0
- data/lib/testjour/commands/version.rb +18 -0
- data/lib/testjour/commands/warm.rb +73 -0
- data/lib/testjour/commands.rb +10 -0
- data/lib/testjour/cucumber_extensions/drb_formatter.rb +30 -0
- data/lib/testjour/cucumber_extensions/queueing_executor.rb +120 -0
- data/lib/testjour/mysql.rb +82 -0
- data/lib/testjour/pid_file.rb +43 -0
- data/lib/testjour/progressbar.rb +124 -0
- data/lib/testjour/queue_server.rb +66 -0
- data/lib/testjour/rsync.rb +29 -0
- data/lib/testjour/slave_server.rb +98 -0
- data/lib/testjour.rb +68 -0
- data/vendor/authprogs +231 -0
- metadata +102 -0
@@ -0,0 +1,88 @@
|
|
1
|
+
require "drb"
|
2
|
+
require "uri"
|
3
|
+
|
4
|
+
require "testjour/commands/base_command"
|
5
|
+
require "testjour/rsync"
|
6
|
+
require "testjour/queue_server"
|
7
|
+
require "testjour/cucumber_extensions/drb_formatter"
|
8
|
+
require "testjour/mysql"
|
9
|
+
|
10
|
+
module Testjour
|
11
|
+
module CLI
|
12
|
+
|
13
|
+
class SlaveRun < BaseCommand
|
14
|
+
def self.command
|
15
|
+
"slave:run"
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(parser, args)
|
19
|
+
Testjour.logger.debug "Runner command #{self.class}..."
|
20
|
+
super
|
21
|
+
@queue = @non_options.first
|
22
|
+
end
|
23
|
+
|
24
|
+
def run
|
25
|
+
retryable :tries => 2, :on => RsyncFailed do
|
26
|
+
Testjour::Rsync.copy_to_current_directory_from(@queue)
|
27
|
+
end
|
28
|
+
|
29
|
+
ARGV.clear # Don't pass along args to RSpec
|
30
|
+
Testjour.load_cucumber
|
31
|
+
|
32
|
+
ENV["RAILS_ENV"] = "test"
|
33
|
+
require File.expand_path("config/environment")
|
34
|
+
|
35
|
+
Testjour::MysqlDatabaseSetup.with_new_database do
|
36
|
+
Cucumber::CLI.executor.formatters = Testjour::DRbFormatter.new(queue_server)
|
37
|
+
require_files
|
38
|
+
|
39
|
+
begin
|
40
|
+
loop do
|
41
|
+
begin
|
42
|
+
run_file(queue_server.take_work)
|
43
|
+
rescue Testjour::QueueServer::NoWorkUnitsAvailableError
|
44
|
+
# If no work, ignore and keep looping
|
45
|
+
end
|
46
|
+
end
|
47
|
+
rescue DRb::DRbConnError
|
48
|
+
Testjour.logger.debug "DRb connection error. (This is normal.) Exiting runner."
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def require_files
|
54
|
+
cli = Cucumber::CLI.new
|
55
|
+
cli.parse_options!(@non_options)
|
56
|
+
cli.send(:require_files)
|
57
|
+
end
|
58
|
+
|
59
|
+
def run_file(file)
|
60
|
+
Testjour.logger.debug "Running feature file: #{file}"
|
61
|
+
features = feature_parser.parse_feature(File.expand_path(file))
|
62
|
+
Cucumber::CLI.executor.visit_features(features)
|
63
|
+
end
|
64
|
+
|
65
|
+
def queue_server
|
66
|
+
@queue_server ||= begin
|
67
|
+
DRb.start_service
|
68
|
+
DRbObject.new(nil, drb_uri)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def drb_uri
|
73
|
+
uri = URI.parse(@queue)
|
74
|
+
uri.scheme = "druby"
|
75
|
+
uri.path = ""
|
76
|
+
uri.user = nil
|
77
|
+
uri.to_s
|
78
|
+
end
|
79
|
+
|
80
|
+
def feature_parser
|
81
|
+
@feature_parser ||= Cucumber::TreetopParser::FeatureParser.new
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
Parser.register_command SlaveRun
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require "drb"
|
2
|
+
|
3
|
+
require "testjour/commands/base_command"
|
4
|
+
require "daemons/daemonize"
|
5
|
+
require "testjour/pid_file"
|
6
|
+
require "testjour/slave_server"
|
7
|
+
require "testjour/bonjour"
|
8
|
+
|
9
|
+
module Testjour
|
10
|
+
module CLI
|
11
|
+
|
12
|
+
class SlaveStart < BaseCommand
|
13
|
+
class StopServer < Exception
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.command
|
17
|
+
"slave:start"
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(*args)
|
21
|
+
Testjour.logger.debug "Runner command #{self.class}..."
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
def run
|
26
|
+
verify_not_a_git_repo
|
27
|
+
|
28
|
+
original_working_directory = File.expand_path(".")
|
29
|
+
|
30
|
+
pid_file = PidFile.new("./testjour_slave.pid")
|
31
|
+
pid_file.verify_doesnt_exist
|
32
|
+
at_exit { pid_file.remove }
|
33
|
+
register_signal_handler
|
34
|
+
|
35
|
+
logfile = File.expand_path("./testjour.log")
|
36
|
+
Daemonize.daemonize(logfile)
|
37
|
+
|
38
|
+
Dir.chdir(original_working_directory)
|
39
|
+
pid_file.write
|
40
|
+
|
41
|
+
Testjour.setup_logger
|
42
|
+
Testjour::Bonjour.serve(Testjour::SlaveServer.start)
|
43
|
+
DRb.thread.join
|
44
|
+
rescue StopServer
|
45
|
+
exit 0
|
46
|
+
end
|
47
|
+
|
48
|
+
def verify_not_a_git_repo
|
49
|
+
return unless File.exists?("./.git")
|
50
|
+
puts "!!! This directory looks like a git repository. This probably isn't where you want to start a testjour slave"
|
51
|
+
exit 1
|
52
|
+
end
|
53
|
+
|
54
|
+
def register_signal_handler
|
55
|
+
trap("TERM") do
|
56
|
+
Testjour.logger.info "TERM signal received."
|
57
|
+
raise StopServer.new
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
Parser.register_command SlaveStart
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require "testjour/commands/base_command"
|
2
|
+
require "testjour/pid_file"
|
3
|
+
|
4
|
+
module Testjour
|
5
|
+
module CLI
|
6
|
+
|
7
|
+
class SlaveStop < BaseCommand
|
8
|
+
def self.command
|
9
|
+
"slave:stop"
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(*args)
|
13
|
+
Testjour.logger.debug "Runner command #{self.class}..."
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
pid_file = PidFile.new("./testjour_slave.pid")
|
19
|
+
pid_file.send_signal("TERM")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
Parser.register_command SlaveStop
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require "drb"
|
2
|
+
require "uri"
|
3
|
+
|
4
|
+
require "testjour/commands/base_command"
|
5
|
+
require "testjour/rsync"
|
6
|
+
|
7
|
+
module Testjour
|
8
|
+
module CLI
|
9
|
+
|
10
|
+
class SlaveWarm < BaseCommand
|
11
|
+
def self.command
|
12
|
+
"slave:warm"
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(parser, args)
|
16
|
+
Testjour.logger.debug "Runner command #{self.class}..."
|
17
|
+
super
|
18
|
+
@queue = @non_options.first
|
19
|
+
end
|
20
|
+
|
21
|
+
def run
|
22
|
+
retryable :tries => 2, :on => RsyncFailed do
|
23
|
+
Testjour::Rsync.copy_to_current_directory_from(@queue)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
Parser.register_command SlaveWarm
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "testjour/commands/base_command"
|
2
|
+
|
3
|
+
module Testjour
|
4
|
+
module CLI
|
5
|
+
|
6
|
+
class VersionCommand < BaseCommand
|
7
|
+
def self.command
|
8
|
+
"version"
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
puts "Testjour #{Testjour::VERSION}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
Parser.register_command VersionCommand
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require "drb"
|
2
|
+
|
3
|
+
require "testjour/commands/base_command"
|
4
|
+
require "testjour/bonjour"
|
5
|
+
|
6
|
+
module Testjour
|
7
|
+
module CLI
|
8
|
+
|
9
|
+
class Warm < BaseCommand
|
10
|
+
def self.command
|
11
|
+
"warm"
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(*args)
|
15
|
+
Testjour.logger.debug "Runner command #{self.class}..."
|
16
|
+
|
17
|
+
super
|
18
|
+
@found_server = 0
|
19
|
+
require "testjour/colorer"
|
20
|
+
end
|
21
|
+
|
22
|
+
def run
|
23
|
+
if available_servers.any?
|
24
|
+
available_servers.each do |server|
|
25
|
+
request_warm_from(server)
|
26
|
+
end
|
27
|
+
|
28
|
+
print_results
|
29
|
+
else
|
30
|
+
puts
|
31
|
+
puts Testjour::Colorer.failed("Don't see any available test servers. Try again later.")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def available_servers
|
36
|
+
@available_servers ||= Testjour::Bonjour.list
|
37
|
+
end
|
38
|
+
|
39
|
+
def request_warm_from(server)
|
40
|
+
slave_server = DRbObject.new(nil, server.uri)
|
41
|
+
result = slave_server.warm(testjour_uri)
|
42
|
+
|
43
|
+
if result
|
44
|
+
Testjour.logger.info "Requesting warm from available server: #{server.uri}. Accepted."
|
45
|
+
@found_server += 1
|
46
|
+
else
|
47
|
+
Testjour.logger.info "Requesting warm from available server: #{server.uri}. Rejected."
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def print_results
|
52
|
+
if @found_server > 0
|
53
|
+
puts
|
54
|
+
puts "#{@found_server} slave accepted the warm request."
|
55
|
+
else
|
56
|
+
puts
|
57
|
+
puts Testjour::Colorer.failed("Found available servers, but none accepted the warm request. Try again later.")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def testjour_uri
|
62
|
+
DRb.start_service
|
63
|
+
uri = URI.parse(DRb.uri)
|
64
|
+
uri.path = File.expand_path(".")
|
65
|
+
uri.scheme = "testjour"
|
66
|
+
uri.user = `whoami`.strip
|
67
|
+
uri.to_s
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
Parser.register_command Warm
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require "testjour/commands/help"
|
2
|
+
require "testjour/commands/version"
|
3
|
+
require "testjour/commands/run"
|
4
|
+
require "testjour/commands/list"
|
5
|
+
require "testjour/commands/slave_start"
|
6
|
+
require "testjour/commands/slave_stop"
|
7
|
+
require "testjour/commands/slave_run"
|
8
|
+
require "testjour/commands/slave_warm"
|
9
|
+
require "testjour/commands/warm"
|
10
|
+
require "testjour/commands/local_run"
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Testjour
|
2
|
+
|
3
|
+
class DRbFormatter
|
4
|
+
|
5
|
+
def initialize(queue_server)
|
6
|
+
@queue_server = queue_server
|
7
|
+
end
|
8
|
+
|
9
|
+
def step_passed(step, regexp, args)
|
10
|
+
@queue_server.write_result DRb.uri, "."
|
11
|
+
end
|
12
|
+
|
13
|
+
def step_failed(step, regexp, args)
|
14
|
+
@queue_server.write_result DRb.uri, "F", step.error.message, step.error.backtrace
|
15
|
+
end
|
16
|
+
|
17
|
+
def step_pending(step, regexp, args)
|
18
|
+
@queue_server.write_result DRb.uri, "P"
|
19
|
+
end
|
20
|
+
|
21
|
+
def step_skipped(step, regexp, args)
|
22
|
+
@queue_server.write_result DRb.uri, "_"
|
23
|
+
end
|
24
|
+
|
25
|
+
def method_missing(*args, &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require "testjour/colorer"
|
2
|
+
require "testjour/progressbar"
|
3
|
+
|
4
|
+
module Testjour
|
5
|
+
|
6
|
+
class QueueingExecutor < ::Cucumber::Tree::TopDownVisitor
|
7
|
+
attr_reader :step_count
|
8
|
+
attr_accessor :formatter
|
9
|
+
|
10
|
+
class << self
|
11
|
+
attr_accessor :queue
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(queue_server, step_mother)
|
15
|
+
@queue_server = queue_server
|
16
|
+
@step_count = 0
|
17
|
+
@passed = 0
|
18
|
+
@skipped = 0
|
19
|
+
@pending = 0
|
20
|
+
@result_uris = []
|
21
|
+
@errors = []
|
22
|
+
end
|
23
|
+
|
24
|
+
def wait_for_results
|
25
|
+
progress_bar = ProgressBar.new("0 slaves", step_count)
|
26
|
+
|
27
|
+
step_count.times do
|
28
|
+
log_result(*@queue_server.take_result)
|
29
|
+
|
30
|
+
if failed?
|
31
|
+
progress_bar.colorer = Testjour::Colorer.method(:failed).to_proc
|
32
|
+
progress_bar.title = "#{@result_uris.size} slaves, #{@errors.size} failures"
|
33
|
+
else
|
34
|
+
progress_bar.colorer = Testjour::Colorer.method(:passed).to_proc
|
35
|
+
progress_bar.title = "#{@result_uris.size} slaves"
|
36
|
+
end
|
37
|
+
|
38
|
+
progress_bar.inc
|
39
|
+
end
|
40
|
+
|
41
|
+
progress_bar.finish
|
42
|
+
|
43
|
+
print_summary
|
44
|
+
print_errors
|
45
|
+
end
|
46
|
+
|
47
|
+
def failed?
|
48
|
+
@errors.any?
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
def log_result(uri, dot, message, backtrace)
|
53
|
+
@result_uris << uri
|
54
|
+
@result_uris.uniq!
|
55
|
+
|
56
|
+
case dot
|
57
|
+
when "."
|
58
|
+
@passed += 1
|
59
|
+
when "F"
|
60
|
+
@errors << [message, backtrace]
|
61
|
+
when "P"
|
62
|
+
@pending += 1
|
63
|
+
when "_"
|
64
|
+
@skipped += 1
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def print_summary
|
69
|
+
puts
|
70
|
+
puts
|
71
|
+
puts Colorer.passed("#{@passed} steps passed") unless @passed.zero?
|
72
|
+
puts Colorer.failed("#{@errors.size} steps failed") unless @errors.empty?
|
73
|
+
puts Colorer.skipped("#{@skipped} steps skipped") unless @skipped.zero?
|
74
|
+
puts Colorer.pending("#{@pending} steps pending") unless @pending.zero?
|
75
|
+
puts
|
76
|
+
end
|
77
|
+
|
78
|
+
def print_errors
|
79
|
+
@errors.each_with_index do |error, i|
|
80
|
+
message, backtrace = error
|
81
|
+
|
82
|
+
puts
|
83
|
+
puts Colorer.failed("#{i+1})")
|
84
|
+
puts Colorer.failed(message)
|
85
|
+
puts Colorer.failed(backtrace)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def visit_feature(feature)
|
90
|
+
super
|
91
|
+
@queue_server.write_work(feature.file)
|
92
|
+
end
|
93
|
+
|
94
|
+
def visit_row_scenario(scenario)
|
95
|
+
visit_scenario(scenario)
|
96
|
+
end
|
97
|
+
|
98
|
+
def visit_regular_scenario(scenario)
|
99
|
+
visit_scenario(scenario)
|
100
|
+
end
|
101
|
+
|
102
|
+
def visit_row_step(step)
|
103
|
+
visit_step(step)
|
104
|
+
end
|
105
|
+
|
106
|
+
def visit_regular_step(step)
|
107
|
+
visit_step(step)
|
108
|
+
end
|
109
|
+
|
110
|
+
def visit_step(step)
|
111
|
+
@step_count += 1
|
112
|
+
end
|
113
|
+
|
114
|
+
def method_missing(*args)
|
115
|
+
# Do nothing
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Testjour
|
2
|
+
|
3
|
+
# Stolen from deep-test
|
4
|
+
|
5
|
+
class MysqlDatabaseSetup
|
6
|
+
class << self
|
7
|
+
#
|
8
|
+
# ActiveRecord configuration to use when connecting to
|
9
|
+
# MySQL to create databases, drop database, and grant
|
10
|
+
# privileges. By default, connects to information_schema
|
11
|
+
# on localhost as root with no password.
|
12
|
+
#
|
13
|
+
attr_accessor :admin_configuration
|
14
|
+
end
|
15
|
+
|
16
|
+
self.admin_configuration = {
|
17
|
+
:adapter => "mysql",
|
18
|
+
:host => "localhost",
|
19
|
+
:username => "root",
|
20
|
+
:database => "information_schema"
|
21
|
+
}
|
22
|
+
|
23
|
+
def self.with_new_database
|
24
|
+
mysql = self.new
|
25
|
+
mysql.create_database
|
26
|
+
|
27
|
+
at_exit do
|
28
|
+
mysql.drop_database
|
29
|
+
end
|
30
|
+
|
31
|
+
mysql.connect
|
32
|
+
mysql.load_schema
|
33
|
+
|
34
|
+
yield
|
35
|
+
end
|
36
|
+
|
37
|
+
def grant_privileges(connection)
|
38
|
+
sql = %{grant all on #{runner_database_name}.*
|
39
|
+
to %s@'localhost' identified by %s;} % [
|
40
|
+
connection.quote(self.class.admin_configuration[:username]),
|
41
|
+
connection.quote(self.class.admin_configuration[:password] || "")
|
42
|
+
]
|
43
|
+
connection.execute sql
|
44
|
+
end
|
45
|
+
|
46
|
+
def create_database
|
47
|
+
admin_connection do |connection|
|
48
|
+
connection.recreate_database runner_database_name
|
49
|
+
grant_privileges(connection)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def drop_database
|
54
|
+
admin_connection do |connection|
|
55
|
+
connection.drop_database runner_database_name
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def connect
|
60
|
+
ActiveRecord::Base.establish_connection(self.class.admin_configuration.merge(:database => runner_database_name))
|
61
|
+
end
|
62
|
+
|
63
|
+
def load_schema
|
64
|
+
# silence_stream(STDOUT) do
|
65
|
+
load File.join(RAILS_ROOT, "db", "schema.rb")
|
66
|
+
# end
|
67
|
+
end
|
68
|
+
|
69
|
+
def admin_connection
|
70
|
+
conn = ActiveRecord::Base.mysql_connection(self.class.admin_configuration)
|
71
|
+
yield conn
|
72
|
+
ensure
|
73
|
+
conn.disconnect! if conn
|
74
|
+
end
|
75
|
+
|
76
|
+
def runner_database_name
|
77
|
+
@runner_database_name ||= "testjour_runner_#{rand(1_000)}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Testjour
|
2
|
+
|
3
|
+
class PidFile
|
4
|
+
|
5
|
+
def initialize(path)
|
6
|
+
@path = File.expand_path(path)
|
7
|
+
end
|
8
|
+
|
9
|
+
def verify_doesnt_exist
|
10
|
+
if File.exist?(@path)
|
11
|
+
puts "!!! PID file #{@path} already exists. testjour could be running already."
|
12
|
+
puts "!!! Exiting with error. You must stop testjour and clear the .pid before I'll attempt a start."
|
13
|
+
exit 1
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def send_signal(signal)
|
18
|
+
pid = open(@path).read.to_i
|
19
|
+
print "Sending #{signal} to Testjour at PID #{pid}..."
|
20
|
+
begin
|
21
|
+
Process.kill(signal, pid)
|
22
|
+
rescue Errno::ESRCH
|
23
|
+
puts "Process does not exist. Not running."
|
24
|
+
end
|
25
|
+
|
26
|
+
puts "Done."
|
27
|
+
end
|
28
|
+
|
29
|
+
def write
|
30
|
+
open(@path, "w") { |f| f.write(Process.pid) }
|
31
|
+
open(@path, "w") do |f|
|
32
|
+
f.write(Process.pid)
|
33
|
+
File.chmod(0644, @path)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def remove
|
38
|
+
File.unlink(@path) if @path and File.exists?(@path)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|