JonathanTron-specjour 0.2.5.1
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/.dev +3 -0
- data/.document +5 -0
- data/.gitignore +23 -0
- data/History.markdown +55 -0
- data/JonathanTron-specjour.gemspec +102 -0
- data/MIT_LICENSE +20 -0
- data/README.markdown +110 -0
- data/Rakefile +51 -0
- data/VERSION +1 -0
- data/bin/specjour +51 -0
- data/lib/specjour.rb +43 -0
- data/lib/specjour/connection.rb +85 -0
- data/lib/specjour/cpu.rb +19 -0
- data/lib/specjour/cucumber.rb +14 -0
- data/lib/specjour/cucumber/dispatcher.rb +18 -0
- data/lib/specjour/cucumber/distributed_formatter.rb +84 -0
- data/lib/specjour/cucumber/final_report.rb +79 -0
- data/lib/specjour/cucumber/printer.rb +9 -0
- data/lib/specjour/db_scrub.rb +47 -0
- data/lib/specjour/dispatcher.rb +119 -0
- data/lib/specjour/manager.rb +101 -0
- data/lib/specjour/printer.rb +102 -0
- data/lib/specjour/protocol.rb +14 -0
- data/lib/specjour/rspec.rb +9 -0
- data/lib/specjour/rspec/distributed_formatter.rb +83 -0
- data/lib/specjour/rspec/final_report.rb +65 -0
- data/lib/specjour/rspec/marshalable_rspec_failure.rb +35 -0
- data/lib/specjour/rsync_daemon.rb +108 -0
- data/lib/specjour/socket_helpers.rb +11 -0
- data/lib/specjour/tasks/dispatch.rake +21 -0
- data/lib/specjour/tasks/specjour.rb +1 -0
- data/lib/specjour/worker.rb +86 -0
- data/rails/init.rb +6 -0
- data/spec/cpu_spec.rb +28 -0
- data/spec/lib/specjour/worker_spec.rb +14 -0
- data/spec/manager_spec.rb +20 -0
- data/spec/rsync_daemon_spec.rb +88 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/specjour_spec.rb +5 -0
- data/specjour.gemspec +101 -0
- metadata +175 -0
@@ -0,0 +1,101 @@
|
|
1
|
+
module Specjour
|
2
|
+
class Manager
|
3
|
+
require 'dnssd'
|
4
|
+
include DRbUndumped
|
5
|
+
include SocketHelpers
|
6
|
+
|
7
|
+
attr_accessor :project_name, :specs_to_run
|
8
|
+
attr_reader :worker_size, :batch_size, :dispatcher_uri, :registered_projects, :bonjour_service, :worker_pids
|
9
|
+
|
10
|
+
def initialize(options = {})
|
11
|
+
@worker_size = options[:worker_size]
|
12
|
+
@batch_size = options[:batch_size]
|
13
|
+
@registered_projects = options[:registered_projects]
|
14
|
+
@worker_pids = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def available_for?(project_name)
|
18
|
+
registered_projects ? registered_projects.include?(project_name) : true
|
19
|
+
end
|
20
|
+
|
21
|
+
def bundle_install
|
22
|
+
Dir.chdir(project_path) do
|
23
|
+
unless system('bundle check > /dev/null')
|
24
|
+
system("bundle install --relock > /dev/null")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def dispatcher_uri=(uri)
|
30
|
+
uri.host = ip_from_hostname(uri.host)
|
31
|
+
@dispatcher_uri = uri
|
32
|
+
end
|
33
|
+
|
34
|
+
def kill_worker_processes
|
35
|
+
Process.kill('TERM', *worker_pids) rescue nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def project_path
|
39
|
+
File.join("/tmp", project_name)
|
40
|
+
end
|
41
|
+
|
42
|
+
def dispatch
|
43
|
+
suspend_bonjour do
|
44
|
+
sync
|
45
|
+
bundle_install
|
46
|
+
dispatch_workers
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def dispatch_workers
|
51
|
+
GC.copy_on_write_friendly = true if GC.respond_to?(:copy_on_write_friendly=)
|
52
|
+
(1..worker_size).each do |index|
|
53
|
+
worker_pids << fork do
|
54
|
+
exec("specjour --batch-size #{batch_size} #{'--log' if Specjour.log?} --do-work #{project_path},#{dispatcher_uri},#{index}")
|
55
|
+
Kernel.exit!
|
56
|
+
end
|
57
|
+
end
|
58
|
+
at_exit { kill_worker_processes }
|
59
|
+
Process.waitall
|
60
|
+
end
|
61
|
+
|
62
|
+
def start
|
63
|
+
drb_start
|
64
|
+
puts "Workers ready: #{worker_size}"
|
65
|
+
bonjour_announce
|
66
|
+
Signal.trap('INT') { puts; puts "Shutting down manager..."; exit }
|
67
|
+
DRb.thread.join
|
68
|
+
end
|
69
|
+
|
70
|
+
def drb_start
|
71
|
+
DRb.start_service nil, self
|
72
|
+
puts "Manager started at #{drb_uri}"
|
73
|
+
at_exit { DRb.stop_service }
|
74
|
+
end
|
75
|
+
|
76
|
+
def sync
|
77
|
+
cmd "rsync -aL --delete --port=8989 #{dispatcher_uri.host}::#{project_name} #{project_path}"
|
78
|
+
end
|
79
|
+
|
80
|
+
protected
|
81
|
+
|
82
|
+
def bonjour_announce
|
83
|
+
@bonjour_service = DNSSD.register! "specjour_manager_#{object_id}", "_#{drb_uri.scheme}._tcp", nil, drb_uri.port
|
84
|
+
end
|
85
|
+
|
86
|
+
def cmd(command)
|
87
|
+
puts command
|
88
|
+
system command
|
89
|
+
end
|
90
|
+
|
91
|
+
def drb_uri
|
92
|
+
@drb_uri ||= URI.parse(DRb.uri)
|
93
|
+
end
|
94
|
+
|
95
|
+
def suspend_bonjour(&block)
|
96
|
+
bonjour_service.stop
|
97
|
+
block.call
|
98
|
+
bonjour_announce
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Specjour
|
2
|
+
require 'specjour/rspec'
|
3
|
+
|
4
|
+
class Printer < GServer
|
5
|
+
include Protocol
|
6
|
+
RANDOM_PORT = 0
|
7
|
+
|
8
|
+
def self.start(specs_to_run)
|
9
|
+
new(specs_to_run).start
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_accessor :worker_size, :specs_to_run, :completed_workers, :disconnections
|
13
|
+
|
14
|
+
def initialize(specs_to_run)
|
15
|
+
super(
|
16
|
+
port = RANDOM_PORT,
|
17
|
+
host = "0.0.0.0",
|
18
|
+
max_connections = 100,
|
19
|
+
stdlog = $stderr,
|
20
|
+
audit = true,
|
21
|
+
debug = true
|
22
|
+
)
|
23
|
+
@completed_workers = 0
|
24
|
+
@disconnections = 0
|
25
|
+
self.specs_to_run = specs_to_run
|
26
|
+
end
|
27
|
+
|
28
|
+
def serve(client)
|
29
|
+
client = Connection.wrap client
|
30
|
+
client.each(TERMINATOR) do |data|
|
31
|
+
process load_object(data), client
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def ready(client)
|
36
|
+
synchronize do
|
37
|
+
client.print specs_to_run.shift
|
38
|
+
client.flush
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def done(client)
|
43
|
+
self.completed_workers += 1
|
44
|
+
end
|
45
|
+
|
46
|
+
def exit_status
|
47
|
+
report.exit_status
|
48
|
+
end
|
49
|
+
|
50
|
+
def worker_summary=(client, summary)
|
51
|
+
report.add(summary)
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
def disconnecting(client_port)
|
57
|
+
self.disconnections += 1
|
58
|
+
if disconnections == worker_size
|
59
|
+
shutdown
|
60
|
+
stop unless stopped?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def log(msg)
|
65
|
+
# noop
|
66
|
+
end
|
67
|
+
|
68
|
+
def error(exception)
|
69
|
+
Specjour.logger.debug exception.inspect
|
70
|
+
end
|
71
|
+
|
72
|
+
def process(message, client)
|
73
|
+
if message.is_a?(String)
|
74
|
+
$stdout.print message
|
75
|
+
$stdout.flush
|
76
|
+
elsif message.is_a?(Array)
|
77
|
+
send(message.first, client, *message[1..-1])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def report
|
82
|
+
@report ||= Rspec::FinalReport.new
|
83
|
+
end
|
84
|
+
|
85
|
+
def stopping
|
86
|
+
report.summarize
|
87
|
+
if disconnections != completed_workers && !Specjour::Dispatcher.interrupted?
|
88
|
+
puts abandoned_worker_message
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def synchronize(&block)
|
93
|
+
@connectionsMutex.synchronize &block
|
94
|
+
end
|
95
|
+
|
96
|
+
def abandoned_worker_message
|
97
|
+
data = "* ERROR: NOT ALL WORKERS COMPLETED PROPERLY *"
|
98
|
+
filler = "*" * data.size
|
99
|
+
[filler, data, filler].join "\n"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Specjour
|
2
|
+
module Protocol
|
3
|
+
TERMINATOR = "|ruojceps|"
|
4
|
+
TERMINATOR_REGEXP = /#{TERMINATOR}$/
|
5
|
+
|
6
|
+
def dump_object(data)
|
7
|
+
Marshal.dump(data) << TERMINATOR
|
8
|
+
end
|
9
|
+
|
10
|
+
def load_object(data)
|
11
|
+
Marshal.load(data.sub(TERMINATOR_REGEXP, ''))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Specjour::Rspec
|
2
|
+
class DistributedFormatter < Spec::Runner::Formatter::BaseTextFormatter
|
3
|
+
require 'specjour/rspec/marshalable_rspec_failure'
|
4
|
+
|
5
|
+
class << self
|
6
|
+
attr_accessor :batch_size
|
7
|
+
end
|
8
|
+
@batch_size = 1
|
9
|
+
|
10
|
+
attr_reader :failing_messages, :passing_messages, :pending_messages, :output
|
11
|
+
attr_reader :duration, :example_count, :failure_count, :pending_count, :pending_examples, :failing_examples
|
12
|
+
|
13
|
+
def initialize(options, output)
|
14
|
+
@options = options
|
15
|
+
@output = output
|
16
|
+
@failing_messages = []
|
17
|
+
@passing_messages = []
|
18
|
+
@pending_messages = []
|
19
|
+
@pending_examples = []
|
20
|
+
@failing_examples = []
|
21
|
+
end
|
22
|
+
|
23
|
+
def example_failed(example, counter, failure)
|
24
|
+
failing_messages << colorize_failure('F', failure)
|
25
|
+
batch_print(failing_messages)
|
26
|
+
end
|
27
|
+
|
28
|
+
def example_passed(example)
|
29
|
+
passing_messages << green('.')
|
30
|
+
batch_print(passing_messages)
|
31
|
+
end
|
32
|
+
|
33
|
+
def example_pending(example, message, deprecated_pending_location=nil)
|
34
|
+
super
|
35
|
+
pending_messages << yellow('*')
|
36
|
+
batch_print(pending_messages)
|
37
|
+
end
|
38
|
+
|
39
|
+
def dump_summary(duration, example_count, failure_count, pending_count)
|
40
|
+
@duration = duration
|
41
|
+
@example_count = example_count
|
42
|
+
@failure_count = failure_count
|
43
|
+
@pending_count = pending_count
|
44
|
+
output.send_message(:worker_summary=, to_hash)
|
45
|
+
end
|
46
|
+
|
47
|
+
def dump_pending
|
48
|
+
#noop
|
49
|
+
end
|
50
|
+
|
51
|
+
def dump_failure(counter, failure)
|
52
|
+
failing_examples << failure
|
53
|
+
end
|
54
|
+
|
55
|
+
def start_dump
|
56
|
+
print_and_flush failing_messages
|
57
|
+
print_and_flush passing_messages
|
58
|
+
print_and_flush pending_messages
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_hash
|
62
|
+
h = {}
|
63
|
+
[:duration, :example_count, :failure_count, :pending_count, :pending_examples, :failing_examples].each do |key|
|
64
|
+
h[key] = send(key)
|
65
|
+
end
|
66
|
+
h
|
67
|
+
end
|
68
|
+
|
69
|
+
protected
|
70
|
+
|
71
|
+
def batch_print(messages)
|
72
|
+
if messages.size == self.class.batch_size
|
73
|
+
print_and_flush(messages)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def print_and_flush(messages)
|
78
|
+
output.print messages.to_s
|
79
|
+
output.flush
|
80
|
+
messages.replace []
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Specjour
|
2
|
+
module Rspec
|
3
|
+
class FinalReport
|
4
|
+
attr_reader :duration, :example_count, :failure_count, :pending_count, :pending_examples, :failing_examples
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@duration = 0.0
|
8
|
+
@example_count = 0
|
9
|
+
@failure_count = 0
|
10
|
+
@pending_count = 0
|
11
|
+
@pending_examples = []
|
12
|
+
@failing_examples = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def add(stats)
|
16
|
+
stats.each do |key, value|
|
17
|
+
if key == :duration
|
18
|
+
@duration = value.to_f if duration < value.to_f
|
19
|
+
else
|
20
|
+
increment(key, value)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def exit_status
|
26
|
+
failing_examples.empty?
|
27
|
+
end
|
28
|
+
|
29
|
+
def increment(key, value)
|
30
|
+
current = instance_variable_get("@#{key}")
|
31
|
+
instance_variable_set("@#{key}", current + value)
|
32
|
+
end
|
33
|
+
|
34
|
+
def formatter_options
|
35
|
+
@formatter_options ||= OpenStruct.new(
|
36
|
+
:colour => true,
|
37
|
+
:autospec => false,
|
38
|
+
:dry_run => false
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
def formatter
|
43
|
+
@formatter ||= begin
|
44
|
+
f = Spec::Runner::Formatter::BaseTextFormatter.new(formatter_options, $stdout)
|
45
|
+
f.instance_variable_set(:@pending_examples, pending_examples)
|
46
|
+
f
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def summarize
|
51
|
+
if example_count > 0
|
52
|
+
formatter.dump_pending
|
53
|
+
dump_failures
|
54
|
+
formatter.dump_summary(duration, example_count, failure_count, pending_count)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def dump_failures
|
59
|
+
failing_examples.each_with_index do |failure, index|
|
60
|
+
formatter.dump_failure index + 1, failure
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Specjour
|
2
|
+
module Rspec
|
3
|
+
class ::Spec::Runner::Reporter::Failure
|
4
|
+
|
5
|
+
def initialize(group_description, example_description, exception)
|
6
|
+
@example_name = "#{group_description} #{example_description}"
|
7
|
+
@exception = MarshalableException.new(exception)
|
8
|
+
@pending_fixed = exception.is_a?(Spec::Example::PendingExampleFixedError)
|
9
|
+
@exception_not_met = exception.is_a?(Spec::Expectations::ExpectationNotMetError)
|
10
|
+
end
|
11
|
+
|
12
|
+
def pending_fixed?
|
13
|
+
@pending_fixed
|
14
|
+
end
|
15
|
+
|
16
|
+
def expectation_not_met?
|
17
|
+
@exception_not_met
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class MarshalableException
|
23
|
+
attr_accessor :message, :backtrace, :class_name
|
24
|
+
|
25
|
+
def initialize(exception)
|
26
|
+
self.class_name = exception.class.name
|
27
|
+
self.message = exception.message
|
28
|
+
self.backtrace = exception.backtrace
|
29
|
+
end
|
30
|
+
|
31
|
+
def class
|
32
|
+
@class ||= OpenStruct.new :name => class_name
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module Specjour
|
2
|
+
class RsyncDaemon
|
3
|
+
require 'fileutils'
|
4
|
+
include SocketHelpers
|
5
|
+
|
6
|
+
# Corresponds to the version of specjour that changed the configuration
|
7
|
+
# file.
|
8
|
+
CONFIG_VERSION = "0.2.3".freeze
|
9
|
+
CONFIG_FILE_NAME = "rsyncd.conf"
|
10
|
+
PID_FILE_NAME = "rsyncd.pid"
|
11
|
+
|
12
|
+
attr_reader :project_path, :project_name
|
13
|
+
|
14
|
+
def initialize(project_path, project_name)
|
15
|
+
@project_path = project_path
|
16
|
+
@project_name = project_name
|
17
|
+
end
|
18
|
+
|
19
|
+
def config_directory
|
20
|
+
@config_directory ||= File.join(project_path, ".specjour")
|
21
|
+
end
|
22
|
+
|
23
|
+
def config_file
|
24
|
+
@config_file ||= File.join(config_directory, CONFIG_FILE_NAME)
|
25
|
+
end
|
26
|
+
|
27
|
+
def pid
|
28
|
+
if File.exists?(pid_file)
|
29
|
+
File.read(pid_file).strip.to_i
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def pid_file
|
34
|
+
File.join(config_directory, PID_FILE_NAME)
|
35
|
+
end
|
36
|
+
|
37
|
+
def start
|
38
|
+
write_config
|
39
|
+
Dir.chdir(project_path) do
|
40
|
+
Kernel.system *command
|
41
|
+
end
|
42
|
+
Kernel.at_exit { stop }
|
43
|
+
end
|
44
|
+
|
45
|
+
def stop
|
46
|
+
if pid
|
47
|
+
Process.kill("TERM", pid)
|
48
|
+
FileUtils.rm(pid_file)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
|
54
|
+
def command
|
55
|
+
["rsync", "--daemon", "--config=#{config_file}", "--port=8989"]
|
56
|
+
end
|
57
|
+
|
58
|
+
def check_config_version
|
59
|
+
File.read(config_file) =~ /\A# (\d+.\d+.\d+)/
|
60
|
+
if out_of_date? Regexp.last_match(1)
|
61
|
+
$stderr.puts <<-WARN
|
62
|
+
|
63
|
+
Specjour has made changes to the way #{CONFIG_FILE_NAME} is generated.
|
64
|
+
Back up '#{config_file}'
|
65
|
+
and re-run the dispatcher to generate the new config file.
|
66
|
+
|
67
|
+
WARN
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def out_of_date?(version)
|
72
|
+
CONFIG_VERSION != version
|
73
|
+
end
|
74
|
+
|
75
|
+
def write_config
|
76
|
+
if File.exists? config_file
|
77
|
+
check_config_version
|
78
|
+
else
|
79
|
+
FileUtils.mkdir_p config_directory
|
80
|
+
File.open(config_file, 'w') do |f|
|
81
|
+
f.write config
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def config
|
87
|
+
<<-CONFIG
|
88
|
+
# #{CONFIG_VERSION}
|
89
|
+
# Rsync daemon config for #{project_name}
|
90
|
+
#
|
91
|
+
# Serve this project with the following command:
|
92
|
+
# $ #{(command | ['--no-detach']).join(' ')}
|
93
|
+
#
|
94
|
+
# Rsync with the following command:
|
95
|
+
# $ rsync -a --port=8989 #{hostname}::#{project_name} /tmp/#{project_name}
|
96
|
+
#
|
97
|
+
use chroot = no
|
98
|
+
timeout = 20
|
99
|
+
read only = yes
|
100
|
+
pid file = ./.specjour/#{PID_FILE_NAME}
|
101
|
+
|
102
|
+
[#{project_name}]
|
103
|
+
path = .
|
104
|
+
exclude = .git* .specjour doc tmp/* log script
|
105
|
+
CONFIG
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|