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.
@@ -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,9 @@
1
+ module Specjour
2
+ module Rspec
3
+ require 'spec'
4
+ require 'spec/runner/formatter/base_text_formatter'
5
+
6
+ require 'specjour/rspec/distributed_formatter'
7
+ require 'specjour/rspec/final_report'
8
+ end
9
+ 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