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.
- 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
|