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