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