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 ADDED
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2008-09-23
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
data/MIT-LICENSE.txt ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2008 Bryan Helmkamp
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,47 @@
1
+ === Testjour
2
+
3
+ * http://github.com/brynary/testjour
4
+
5
+ === Description
6
+
7
+ Distributed test running with autodiscovery via Bonjour (for Cucumber first)
8
+
9
+ === Synopsis
10
+
11
+ On machines to be used as Testjour slaves:
12
+
13
+ $ mkdir testjour-working-dir
14
+ $ testjour slave:start
15
+
16
+
17
+ On your development machine, verify it can see the testjour slave:
18
+
19
+ $ testjour list
20
+
21
+ Testjour servers:
22
+
23
+ bhelmkamp available bryans-computer.local.:62434
24
+
25
+ Now run your tests:
26
+
27
+ $ testjour run features
28
+
29
+ Note: This only really makes sense if you use more than one slave. Otherwise
30
+ it's slower than just running them locally.
31
+
32
+ === Install
33
+
34
+ To install the latest release (once there is a release):
35
+
36
+ $ sudo gem install testjour
37
+
38
+ For now, just pull down the code from the GitHub repo:
39
+
40
+ $ git clone git://github.com/brynary/testjour.git
41
+ $ cd testjour
42
+ $ rake gem
43
+ $ rake install_gem
44
+
45
+ === Authors
46
+
47
+ - Maintained by Bryan Helmkamp (http://brynary.com/)
data/Rakefile ADDED
@@ -0,0 +1,39 @@
1
+ require 'rubygems'
2
+ require "rake/gempackagetask"
3
+ require "rake/clean"
4
+ require './lib/testjour.rb'
5
+
6
+ spec = Gem::Specification.new do |s|
7
+ s.name = "testjour"
8
+ s.version = Testjour::VERSION
9
+ s.author = "Bryan Helmkamp"
10
+ s.email = "bryan" + "@" + "brynary.com"
11
+ s.homepage = "http://github.com/brynary/testjour"
12
+ s.summary = "Distributed test running with autodiscovery via Bonjour (for Cucumber first)"
13
+ s.description = s.summary
14
+ s.executables = "testjour"
15
+ s.files = %w[History.txt MIT-LICENSE.txt README.rdoc Rakefile] + Dir["bin/*"] + Dir["lib/**/*"] + Dir["vendor/**/*"]
16
+
17
+ s.add_dependency "systemu", ">=1.2.0"
18
+ s.add_dependency "dnssd", ">=0.6.0"
19
+ end
20
+
21
+ Rake::GemPackageTask.new(spec) do |package|
22
+ package.gem_spec = spec
23
+ end
24
+
25
+ desc 'Show information about the gem.'
26
+ task :write_gemspec do
27
+ File.open("testjour.gemspec", 'w') do |f|
28
+ f.write spec.to_ruby
29
+ end
30
+ puts "Generated: testjour.gemspec"
31
+ end
32
+
33
+ CLEAN.include ["pkg", "*.gem", "doc", "ri", "coverage"]
34
+
35
+ desc 'Install the package as a gem.'
36
+ task :install_gem => [:clean, :package] do
37
+ gem = Dir['pkg/*.gem'].first
38
+ sh "sudo gem install --local #{gem}"
39
+ end
data/bin/testjour ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + "/../lib/testjour")
4
+
5
+ require "testjour/cli"
6
+ require "testjour/commands"
7
+
8
+ Testjour::CLI.execute
@@ -0,0 +1,56 @@
1
+ require "dnssd"
2
+ require "set"
3
+
4
+ Thread.abort_on_exception = true
5
+
6
+ module Testjour
7
+ SERVICE = "_testjour._tcp"
8
+
9
+ class Bonjour
10
+
11
+ class Server
12
+ attr_reader :name, :host, :port
13
+
14
+ def initialize(name, host, port)
15
+ @name = name
16
+ @host = host
17
+ @port = port
18
+ end
19
+
20
+ def ==(other)
21
+ other.class == self.class && other.uri == self.uri
22
+ end
23
+
24
+ def uri
25
+ "druby://" + @host.gsub(/\.$/, "") + ":" + @port.to_s
26
+ end
27
+ end
28
+
29
+ def self.list
30
+ hosts = []
31
+
32
+ service = DNSSD.browse(SERVICE) do |reply|
33
+ DNSSD.resolve(reply.name, reply.type, reply.domain) do |rr|
34
+ server = Server.new(reply.name, rr.target, rr.port)
35
+ hosts << server unless hosts.any? { |h| h == server }
36
+ end
37
+ end
38
+
39
+ sleep 3
40
+ service.stop
41
+ return hosts
42
+ end
43
+
44
+ def self.serve(port)
45
+ name = ENV['USER']
46
+
47
+ tr = DNSSD::TextRecord.new
48
+ tr['description'] = "#{name}'s testjour server"
49
+
50
+ DNSSD.register(name, SERVICE, "local", port, tr.encode) do |reply|
51
+ Testjour.logger.info "Broadcasting: Ready to run tests under name '#{name}' on port #{port}..."
52
+ end
53
+ end
54
+ end
55
+
56
+ end
@@ -0,0 +1,78 @@
1
+ module Testjour
2
+ module CLI
3
+
4
+ class NoCommandGiven < StandardError
5
+ def message
6
+ "No command given"
7
+ end
8
+ end
9
+
10
+ class UnknownCommand < StandardError
11
+ def initialize(command_name)
12
+ @command_name = command_name
13
+ end
14
+
15
+ def message
16
+ "Unknown command: #{@command_name.inspect}"
17
+ end
18
+ end
19
+
20
+ def self.execute
21
+ Parser.new.execute
22
+ end
23
+
24
+ class Parser
25
+ class << self
26
+ attr_accessor :commands
27
+
28
+ def register_command(klass)
29
+ @commands << klass
30
+ end
31
+ end
32
+
33
+ self.commands = []
34
+
35
+ def execute
36
+ begin
37
+ raise NoCommandGiven if ARGV.empty?
38
+ raise UnknownCommand.new(command_name) unless command_klass
39
+
40
+ args = ARGV.dup
41
+ args.shift # Remove subcommand name
42
+
43
+ command_klass.new(self, args).run
44
+ rescue NoCommandGiven, UnknownCommand
45
+ $stderr.puts "ERROR: #{$!.message}"
46
+ $stderr.puts usage
47
+ exit 1
48
+ end
49
+ end
50
+
51
+ def command_klass
52
+ self.class.commands.detect do |command_klass|
53
+ command_klass.command == command_name
54
+ end
55
+ end
56
+
57
+ def command_name
58
+ ARGV.first
59
+ end
60
+
61
+ def usage
62
+ message = []
63
+ message << "usage: testjour <SUBCOMMAND> [OPTIONS] [ARGS...]"
64
+ message << "Type 'testjour help <SUBCOMMAND>' for help on a specific subcommand."
65
+ message << "Type 'testjour version' to get this program's version."
66
+ message << ""
67
+ message << "Available subcommands are:"
68
+
69
+ self.class.commands.sort_by { |c| c.command }.each do |command_klass|
70
+ message << " " + command_klass.command
71
+ end
72
+
73
+ message.map { |line| line.chomp }.join("\n")
74
+ end
75
+ end
76
+
77
+ end
78
+ end
@@ -0,0 +1,8 @@
1
+ require "cucumber"
2
+ require "cucumber/formatters/ansicolor"
3
+
4
+ module Testjour
5
+ class Colorer
6
+ extend ::Cucumber::Formatters::ANSIColor
7
+ end
8
+ end
@@ -0,0 +1,53 @@
1
+ require "optparse"
2
+
3
+ module Testjour
4
+ module CLI
5
+
6
+ class BaseCommand
7
+ attr_reader :non_options, :options
8
+
9
+ def self.command
10
+ self.name.downcase
11
+ end
12
+
13
+ def self.options
14
+ {}
15
+ end
16
+
17
+ def self.help
18
+ nil
19
+ end
20
+
21
+ def self.detailed_help
22
+ nil
23
+ end
24
+
25
+ # def self.usage
26
+ # message = []
27
+ #
28
+ # if help.nil?
29
+ # message << command
30
+ # else
31
+ # message << "#{command}: #{help}"
32
+ # end
33
+ # message << detailed_help unless detailed_help.nil?
34
+ # message << ""
35
+ # message << "Valid options:"
36
+ # message
37
+ # @option_parser.summarize(message)
38
+ # end
39
+
40
+ def initialize(parser, args)
41
+ @parser = parser
42
+ @options = {}
43
+ @non_options = option_parser.parse(args)
44
+ end
45
+
46
+ def option_parser
47
+ OptionParser.new
48
+ end
49
+
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,20 @@
1
+ require "testjour/commands/base_command"
2
+
3
+ module Testjour
4
+ module CLI
5
+
6
+ class HelpCommand < BaseCommand
7
+ def self.command
8
+ "help"
9
+ end
10
+
11
+ def run
12
+ puts @parser.usage
13
+ exit 1
14
+ end
15
+ end
16
+
17
+ Parser.register_command HelpCommand
18
+ end
19
+ end
20
+
@@ -0,0 +1,57 @@
1
+ require "drb"
2
+ require "testjour/commands/base_command"
3
+ require "testjour/bonjour"
4
+
5
+ module Testjour
6
+ module CLI
7
+
8
+ class List < BaseCommand
9
+ def self.command
10
+ "list"
11
+ end
12
+
13
+ def initialize(*args)
14
+ super
15
+ Testjour.load_cucumber
16
+ require "testjour/colorer"
17
+ rescue LoadError
18
+ # No cucumber, we can't use color :(
19
+ end
20
+
21
+ def run
22
+ available_servers = Testjour::Bonjour.list
23
+
24
+ if available_servers.any?
25
+ puts
26
+ puts "Testjour servers:"
27
+ puts
28
+
29
+ available_servers.each do |server|
30
+ slave_server = DRbObject.new(nil, server.uri)
31
+ status = colorize_status(slave_server.status)
32
+ puts " %-12s %s %s" % [server.name, status, "#{server.host}:#{server.port}"]
33
+ end
34
+ else
35
+ puts
36
+ puts "No testjour servers found."
37
+ end
38
+ end
39
+
40
+ def colorize_status(status)
41
+ formatted_status = ("%-12s" % status)
42
+ return formatted_status unless defined?(Testjour::Colorer)
43
+
44
+ case formatted_status.strip
45
+ when "available"
46
+ Testjour::Colorer.green(formatted_status)
47
+ else
48
+ Testjour::Colorer.yellow(formatted_status)
49
+ end
50
+ end
51
+
52
+ end
53
+
54
+ Parser.register_command List
55
+ end
56
+ end
57
+
@@ -0,0 +1,83 @@
1
+ require "drb"
2
+ require "uri"
3
+
4
+ require "testjour/commands/base_command"
5
+ require "testjour/queue_server"
6
+ require "testjour/cucumber_extensions/drb_formatter"
7
+ require "testjour/mysql"
8
+
9
+ module Testjour
10
+ module CLI
11
+
12
+ class LocalRun < BaseCommand
13
+ def self.command
14
+ "local:run"
15
+ end
16
+
17
+ def initialize(parser, args)
18
+ Testjour.logger.debug "Runner command #{self.class}..."
19
+ super
20
+ @queue = @non_options.shift
21
+ end
22
+
23
+ def run
24
+ ARGV.clear # Don't pass along args to RSpec
25
+ Testjour.load_cucumber
26
+
27
+ ENV["RAILS_ENV"] = "test"
28
+ require File.expand_path("config/environment")
29
+
30
+ Testjour::MysqlDatabaseSetup.with_new_database do
31
+ Cucumber::CLI.executor.formatters = Testjour::DRbFormatter.new(queue_server)
32
+ require_files
33
+
34
+ begin
35
+ loop do
36
+ begin
37
+ run_file(queue_server.take_work)
38
+ rescue Testjour::QueueServer::NoWorkUnitsAvailableError
39
+ # If no work, ignore and keep looping
40
+ end
41
+ end
42
+ rescue DRb::DRbConnError
43
+ Testjour.logger.debug "DRb connection error. (This is normal.) Exiting runner."
44
+ end
45
+ end
46
+ end
47
+
48
+ def require_files
49
+ cli = Cucumber::CLI.new
50
+ cli.parse_options!(@non_options)
51
+ cli.send(:require_files)
52
+ end
53
+
54
+ def run_file(file)
55
+ Testjour.logger.debug "Running feature file: #{file}"
56
+ features = feature_parser.parse_feature(File.expand_path(file))
57
+ Cucumber::CLI.executor.visit_features(features)
58
+ end
59
+
60
+ def queue_server
61
+ @queue_server ||= begin
62
+ DRb.start_service
63
+ DRbObject.new(nil, drb_uri)
64
+ end
65
+ end
66
+
67
+ def drb_uri
68
+ uri = URI.parse(@queue)
69
+ uri.scheme = "druby"
70
+ uri.path = ""
71
+ uri.user = nil
72
+ uri.to_s
73
+ end
74
+
75
+ def feature_parser
76
+ @feature_parser ||= Cucumber::TreetopParser::FeatureParser.new
77
+ end
78
+
79
+ end
80
+
81
+ Parser.register_command LocalRun
82
+ end
83
+ end
@@ -0,0 +1,165 @@
1
+ require "drb"
2
+
3
+ require "testjour/commands/base_command"
4
+ require "testjour/queue_server"
5
+ require "testjour/bonjour"
6
+
7
+ module Testjour
8
+ module CLI
9
+
10
+ class Run < BaseCommand
11
+ def self.command
12
+ "run"
13
+ end
14
+
15
+ def initialize(*args)
16
+ Testjour.logger.debug "Runner command #{self.class}..."
17
+ Testjour.load_cucumber
18
+
19
+ super
20
+ @found_server = 0
21
+ require "testjour/cucumber_extensions/queueing_executor"
22
+ require "testjour/colorer"
23
+ end
24
+
25
+ def run
26
+ Testjour::QueueServer.with_server do |queue|
27
+ start_local_runners unless servers_specified?
28
+ start_slave_runners unless no_remote?
29
+
30
+ if @found_server.zero?
31
+ puts "No processes to build on. Aborting."
32
+ exit
33
+ end
34
+
35
+ queue_features(queue)
36
+ print_results
37
+ end
38
+ end
39
+
40
+ def disable_cucumber_require
41
+ Cucumber::CLI.class_eval do
42
+ def require_files
43
+ ARGV.clear # Shut up RSpec
44
+ end
45
+ end
46
+ end
47
+
48
+ def queue_features(queue)
49
+ Testjour.logger.debug "Queueing features..."
50
+ disable_cucumber_require
51
+ ARGV.replace(@non_options.clone)
52
+ Cucumber::CLI.executor = Testjour::QueueingExecutor.new(queue, Cucumber::CLI.step_mother)
53
+ Cucumber::CLI.execute
54
+ end
55
+
56
+ def print_results
57
+ puts
58
+ puts "Requesting build from #{@found_server} processes..."
59
+ puts
60
+
61
+ Cucumber::CLI.executor.wait_for_results
62
+ Testjour.logger.debug "DONE"
63
+ end
64
+
65
+ def slave_servers_to_use
66
+ # require "rubygems"; require "ruby-debug"; Debugger.start; debugger
67
+ @slave_servers_to_use ||= available_servers.select do |potential_server|
68
+ !servers_specified? || specified_servers_include?(potential_server)
69
+ end
70
+ end
71
+
72
+ def available_servers
73
+ @available_servers ||= Testjour::Bonjour.list
74
+ end
75
+
76
+ def request_build_from(server)
77
+ slave_server = DRbObject.new(nil, server.uri)
78
+ result = slave_server.run(testjour_uri, @non_options)
79
+
80
+ if result
81
+ Testjour.logger.info "Requesting buld from available server: #{server.uri}. Accepted."
82
+ @found_server += 1
83
+ else
84
+ Testjour.logger.info "Requesting buld from available server: #{server.uri}. Rejected."
85
+ end
86
+ end
87
+
88
+ def start_local_runners
89
+ 2.times do
90
+ start_local_runner
91
+ end
92
+ end
93
+
94
+ def start_slave_runners
95
+ slave_servers_to_use.each do |server|
96
+ request_build_from(server)
97
+ end
98
+ end
99
+
100
+ def start_local_runner
101
+ pid_queue = Queue.new
102
+
103
+ Thread.new do
104
+ Thread.current.abort_on_exception = true
105
+ cmd = command_for_local_run
106
+ Testjour.logger.debug "Starting local:run with command: #{cmd}"
107
+ status, stdout, stderr = systemu(cmd) { |pid| pid_queue << pid }
108
+ Testjour.logger.warn stderr if stderr.strip.size > 0
109
+ end
110
+
111
+ pid = pid_queue.pop
112
+
113
+ @found_server += 1
114
+ Testjour.logger.info "Started local:run on PID #{pid}"
115
+
116
+ pid
117
+ end
118
+
119
+ def command_for_local_run
120
+ "#{testjour_bin_path} local:run #{testjour_uri} -- #{@non_options.join(' ')}".strip
121
+ end
122
+
123
+ def testjour_bin_path
124
+ File.expand_path(File.dirname(__FILE__) + "/../../../bin/testjour")
125
+ end
126
+
127
+ def testjour_uri
128
+ uri = URI.parse(DRb.uri)
129
+ uri.path = File.expand_path(".")
130
+ uri.scheme = "testjour"
131
+ uri.user = `whoami`.strip
132
+ uri.to_s
133
+ end
134
+
135
+ def specified_servers_include?(potential_server)
136
+ @options[:server].any? do |specified_server|
137
+ potential_server.host.include?(specified_server)
138
+ end
139
+ end
140
+
141
+ def no_remote?
142
+ @options[:no_remote]
143
+ end
144
+
145
+ def servers_specified?
146
+ @options[:server] && @options[:server].any?
147
+ end
148
+
149
+ def option_parser
150
+ OptionParser.new do |opts|
151
+ opts.on("--on SERVER", "Specify a pattern to exclude servers to. Disabled local runners") do |server|
152
+ @options[:server] ||= []
153
+ @options[:server] << server
154
+ end
155
+
156
+ opts.on("--no-remote", "Only use local runners") do |server|
157
+ @options[:no_remote] = true
158
+ end
159
+ end
160
+ end
161
+ end
162
+
163
+ Parser.register_command Run
164
+ end
165
+ end