specjour 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,129 @@
1
+ module Specjour
2
+ class Loader
3
+ include Protocol
4
+ include Fork
5
+
6
+ attr_reader :test_paths, :printer_uri, :project_path, :task, :worker_size, :worker_pids, :quiet
7
+
8
+ def initialize(options = {})
9
+ @options = options
10
+ @printer_uri = options[:printer_uri]
11
+ @test_paths = options[:test_paths]
12
+ @worker_size = options[:worker_size]
13
+ @task = options[:task]
14
+ @quiet = options[:quiet]
15
+ @project_path = options[:project_path]
16
+ @worker_pids = []
17
+ Dir.chdir project_path
18
+ Specjour.load_custom_hooks
19
+ end
20
+
21
+ def start
22
+ load_app
23
+ Configuration.after_load.call
24
+ (1..worker_size).each do |index|
25
+ worker_pids << fork do
26
+ w = Worker.new(
27
+ :number => index,
28
+ :printer_uri => printer_uri,
29
+ :quiet => quiet
30
+ ).send(task)
31
+ end
32
+ end
33
+ Process.waitall
34
+ ensure
35
+ kill_worker_processes
36
+ end
37
+
38
+ def spec_files
39
+ @spec_files ||= file_collector(spec_paths) do |path|
40
+ if path == project_path
41
+ Dir["#{path}/spec/**/*_spec.rb"]
42
+ else
43
+ Dir["#{path}/**/*_spec.rb"]
44
+ end
45
+ end
46
+ end
47
+
48
+ def feature_files
49
+ @feature_files ||= file_collector(feature_paths) do |path|
50
+ if path == project_path
51
+ Dir["#{path}/features/**/*.feature"]
52
+ else
53
+ Dir["#{path}/**/*.feature"]
54
+ end
55
+ end
56
+ end
57
+
58
+ protected
59
+
60
+ def spec_paths
61
+ @spec_paths ||= test_paths.select {|p| p =~ /spec.*$/}
62
+ end
63
+
64
+ def feature_paths
65
+ @feature_paths ||= test_paths.select {|p| p =~ /features.*$/}
66
+ end
67
+
68
+ def file_collector(paths, &globber)
69
+ if spec_paths.empty? && feature_paths.empty?
70
+ globber[project_path]
71
+ else
72
+ paths.map do |path|
73
+ path = File.expand_path(path, project_path)
74
+ if File.directory?(path)
75
+ globber[path]
76
+ else
77
+ path
78
+ end
79
+ end.flatten.uniq
80
+ end
81
+ end
82
+
83
+ def load_app
84
+ share_examples if spec_files.any?
85
+ share_features if feature_files.any?
86
+ end
87
+
88
+ def share_examples
89
+ RSpec::Preloader.load spec_files
90
+ connection.send_message :tests=, filtered_examples
91
+ ensure
92
+ ::RSpec.reset
93
+ end
94
+
95
+ def share_features
96
+ Cucumber::Preloader.load
97
+ connection.send_message :tests=, feature_files
98
+ end
99
+
100
+ def filtered_examples
101
+ ::RSpec.world.example_groups.map do |g|
102
+ g.descendants.map do |gs|
103
+ gs.examples
104
+ end.flatten.map do |e|
105
+ "#{e.file_path}:#{e.metadata[:line_number]}"
106
+ end
107
+ end.flatten.uniq
108
+ end
109
+
110
+ def kill_worker_processes
111
+ if Specjour.interrupted?
112
+ Process.kill('INT', *worker_pids) rescue Errno::ESRCH
113
+ else
114
+ Process.kill('TERM', *worker_pids) rescue Errno::ESRCH
115
+ end
116
+ end
117
+
118
+ def connection
119
+ @connection ||= begin
120
+ at_exit { connection.disconnect }
121
+ printer_connection
122
+ end
123
+ end
124
+
125
+ def printer_connection
126
+ Connection.new URI.parse(printer_uri)
127
+ end
128
+ end
129
+ end
@@ -1,20 +1,18 @@
1
1
  module Specjour
2
2
  class Manager
3
3
  require 'dnssd'
4
- require 'specjour/rspec'
5
- require 'specjour/cucumber'
6
4
 
7
5
  include DRbUndumped
8
6
  include SocketHelper
7
+ include Fork
9
8
 
10
- attr_accessor :project_name, :preload_spec, :preload_feature, :worker_task, :pid
11
- attr_reader :worker_size, :dispatcher_uri, :registered_projects, :worker_pids, :options
9
+ attr_accessor :test_paths, :project_name, :worker_task, :pid
10
+ attr_reader :worker_size, :dispatcher_uri, :registered_projects, :loader_pid, :options, :rsync_port
12
11
 
13
12
  def self.start_quietly(options)
14
13
  manager = new options.merge(:quiet => true)
15
14
  manager.drb_uri
16
- manager.pid = QuietFork.fork { manager.start }
17
- sleep 0.2
15
+ manager.pid = Fork.fork_quietly { manager.start }
18
16
  manager
19
17
  end
20
18
 
@@ -23,8 +21,8 @@ module Specjour
23
21
  @worker_size = options[:worker_size]
24
22
  @worker_task = options[:worker_task]
25
23
  @registered_projects = options[:registered_projects]
26
- @worker_pids = []
27
- at_exit { kill_worker_processes }
24
+ @rsync_port = options[:rsync_port]
25
+ at_exit { kill_loader_process }
28
26
  end
29
27
 
30
28
  def available_for?(project_name)
@@ -39,8 +37,10 @@ module Specjour
39
37
  def dispatch
40
38
  suspend_bonjour do
41
39
  sync
42
- execute_before_fork
43
- dispatch_workers
40
+ with_clean_env do
41
+ execute_before_fork
42
+ dispatch_loader
43
+ end
44
44
  end
45
45
  end
46
46
 
@@ -57,14 +57,21 @@ module Specjour
57
57
  end
58
58
  end
59
59
 
60
- def dispatch_workers
61
- worker_pids.clear
62
- (1..worker_size).each do |index|
63
- worker_pids << fork do
64
- exec "specjour work #{worker_options(index)}"
65
- end
60
+ def dispatch_loader
61
+ @loader_pid = fork do
62
+ exec_cmd = "load --printer-uri #{dispatcher_uri} --workers #{worker_size} --task #{worker_task} --project-path #{project_path}"
63
+ exec_cmd << " --test-paths #{test_paths.join(" ")}" if test_paths.any?
64
+ exec_cmd << " --log" if Specjour.log?
65
+ exec_cmd << " --quiet" if quiet?
66
+ exec_ruby = "Specjour::CLI.start(#{exec_cmd.split(' ').inspect})"
67
+ load_path = ''
68
+ $LOAD_PATH.each {|p| load_path << "-I#{p} " if p =~ /specjour/}
69
+ exec_cmd = "ruby #{load_path} -rspecjour -e '#{exec_ruby}'"
70
+ exec exec_cmd
66
71
  end
67
72
  Process.waitall
73
+ ensure
74
+ kill_loader_process
68
75
  end
69
76
 
70
77
  def in_project(&block)
@@ -73,13 +80,14 @@ module Specjour
73
80
 
74
81
  def interrupted=(bool)
75
82
  Specjour.interrupted = bool
83
+ kill_loader_process
76
84
  end
77
85
 
78
- def kill_worker_processes
86
+ def kill_loader_process
79
87
  if Specjour.interrupted?
80
- Process.kill('INT', *worker_pids) rescue Errno::ESRCH
88
+ Process.kill('INT', loader_pid) rescue Errno::ESRCH
81
89
  else
82
- Process.kill('TERM', *worker_pids) rescue Errno::ESRCH
90
+ Process.kill('TERM', loader_pid) rescue Errno::ESRCH
83
91
  end
84
92
  end
85
93
 
@@ -93,9 +101,8 @@ module Specjour
93
101
 
94
102
  def start
95
103
  drb_start
96
- puts "Workers ready: #{worker_size}."
97
- puts "Listening for #{registered_projects.join(', ')}"
98
104
  bonjour_announce
105
+ at_exit { stop_bonjour }
99
106
  DRb.thread.join
100
107
  end
101
108
 
@@ -104,16 +111,20 @@ module Specjour
104
111
  end
105
112
 
106
113
  def sync
107
- unless cmd "rsync -aL --delete --port=8989 #{dispatcher_uri.host}::#{project_name} #{project_path}"
108
- raise Error, "Rsync Failed."
109
- end
114
+ cmd "rsync -aL --delete --ignore-errors --port=#{rsync_port} #{dispatcher_uri.host}::#{project_name} #{project_path}"
115
+ puts "rsync complete"
110
116
  end
111
117
 
112
118
  protected
113
119
 
114
120
  def bonjour_announce
121
+ projects = registered_projects.join(", ")
122
+ puts "Workers ready: #{worker_size}"
123
+ puts "Listening for #{projects}"
115
124
  unless quiet?
116
- bonjour_service.register "specjour_manager_#{object_id}", "_#{drb_uri.scheme}._tcp", nil, drb_uri.port
125
+ text = DNSSD::TextRecord.new
126
+ text['version'] = Specjour::VERSION
127
+ bonjour_service.register "specjour_manager_#{projects}_#{Process.pid}", "_#{drb_uri.scheme}._tcp", domain=nil, drb_uri.port, host=nil, text
117
128
  end
118
129
  end
119
130
 
@@ -134,7 +145,7 @@ module Specjour
134
145
  end
135
146
 
136
147
  def stop_bonjour
137
- bonjour_service.stop
148
+ bonjour_service.stop if bonjour_service && !bonjour_service.stopped?
138
149
  @bonjour_service = nil
139
150
  end
140
151
 
@@ -144,13 +155,18 @@ module Specjour
144
155
  bonjour_announce
145
156
  end
146
157
 
147
- def worker_options(index)
148
- exec_options = "--project-path #{project_path} --printer-uri #{dispatcher_uri} --number #{index} --task #{worker_task}"
149
- exec_options << " --preload-spec #{preload_spec}" if preload_spec
150
- exec_options << " --preload-feature #{preload_feature}" if preload_feature
151
- exec_options << " --log" if Specjour.log?
152
- exec_options << " --quiet" if quiet?
153
- exec_options
158
+ def with_clean_env
159
+ if defined?(Bundler)
160
+ Bundler.with_clean_env do
161
+ if ENV['RUBYOPT']
162
+ opts = ENV['RUBYOPT'].split(" ").delete_if {|opt| opt =~ /bundler/}
163
+ ENV['RUBYOPT'] = opts.join(" ")
164
+ end
165
+ yield
166
+ end
167
+ else
168
+ yield
169
+ end
154
170
  end
155
171
  end
156
172
  end
@@ -1,52 +1,78 @@
1
1
  module Specjour
2
- require 'specjour/rspec'
3
- require 'specjour/cucumber'
4
2
 
5
- class Printer < GServer
3
+ class Printer
6
4
  include Protocol
7
5
  RANDOM_PORT = 0
8
6
 
9
- def self.start(specs_to_run)
10
- new(specs_to_run).start
11
- end
12
-
13
- attr_accessor :worker_size, :specs_to_run, :completed_workers, :disconnections, :profiler
7
+ attr_reader :port, :clients
8
+ attr_accessor :tests_to_run, :example_size, :examples_complete, :profiler
14
9
 
15
- def initialize(specs_to_run)
16
- super(
17
- port = RANDOM_PORT,
18
- host = "0.0.0.0",
19
- max_connections = 100,
20
- stdlog = $stderr,
21
- audit = true,
22
- debug = true
23
- )
24
- @completed_workers = 0
25
- @disconnections = 0
10
+ def initialize
11
+ @host = "0.0.0.0"
12
+ @server_socket = TCPServer.new(@host, RANDOM_PORT)
13
+ @port = @server_socket.addr[1]
26
14
  @profiler = {}
27
- self.specs_to_run = run_order(specs_to_run)
15
+ @clients = {}
16
+ @tests_to_run = []
17
+ @example_size = 0
18
+ self.examples_complete = 0
19
+ end
20
+
21
+ def start
22
+ fds = [@server_socket]
23
+ catch(:stop) do
24
+ while true
25
+ reads = select(fds).first
26
+ reads.each do |socket_being_read|
27
+ if socket_being_read == @server_socket
28
+ client_socket = @server_socket.accept
29
+ fds << client_socket
30
+ clients[client_socket] = Connection.wrap(client_socket)
31
+ elsif socket_being_read.eof?
32
+ socket_being_read.close
33
+ fds.delete(socket_being_read)
34
+ clients.delete(socket_being_read)
35
+ disconnecting
36
+ else
37
+ serve(clients[socket_being_read])
38
+ end
39
+ end
40
+ end
41
+ end
42
+ ensure
43
+ stopping
44
+ fds.each {|c| c.close}
45
+ end
46
+
47
+ def exit_status
48
+ reporters.all? {|r| r.exit_status == true}
28
49
  end
29
50
 
51
+ protected
52
+
30
53
  def serve(client)
31
- client = Connection.wrap client
32
- client.each(TERMINATOR) do |data|
33
- process load_object(data), client
54
+ data = load_object(client.gets(TERMINATOR))
55
+ case data
56
+ when String
57
+ $stdout.print data
58
+ $stdout.flush
59
+ when Array
60
+ send data.first, *(data[1..-1].unshift(client))
34
61
  end
35
62
  end
36
63
 
37
64
  def ready(client)
38
- synchronize do
39
- client.print specs_to_run.shift
40
- client.flush
41
- end
65
+ client.print tests_to_run.shift
66
+ client.flush
42
67
  end
43
68
 
44
69
  def done(client)
45
- self.completed_workers += 1
70
+ self.examples_complete += 1
46
71
  end
47
72
 
48
- def exit_status
49
- reporters.all? {|r| r.exit_status == true}
73
+ def tests=(client, tests)
74
+ self.tests_to_run = run_order(tests_to_run | tests)
75
+ self.example_size = tests_to_run.size
50
76
  end
51
77
 
52
78
  def rspec_summary=(client, summary)
@@ -62,39 +88,18 @@ module Specjour
62
88
  self.profiler[test] = time
63
89
  end
64
90
 
65
- protected
66
-
67
- def disconnecting(client_port)
68
- synchronize { self.disconnections += 1 }
69
- if disconnections == worker_size
70
- shutdown
71
- stop unless Specjour.interrupted?
72
- end
73
- end
74
-
75
- def log(msg)
76
- # noop
77
- end
78
-
79
- def error(exception)
80
- Specjour.logger.debug "#{exception.inspect}\n#{exception.backtrace.join("\n")}"
81
- end
82
-
83
- def process(message, client)
84
- if message.is_a?(String)
85
- $stdout.print message
86
- $stdout.flush
87
- elsif message.is_a?(Array)
88
- send(message.first, client, *message[1..-1])
91
+ def disconnecting
92
+ if (examples_complete == example_size || clients.empty?)
93
+ throw(:stop)
89
94
  end
90
95
  end
91
96
 
92
- def run_order(specs_to_run)
97
+ def run_order(tests)
93
98
  if File.exist?('.specjour/performance')
94
- ordered_specs = File.readlines('.specjour/performance').map {|l| l.chop.split(':')[1]}
95
- (specs_to_run - ordered_specs) | (ordered_specs & specs_to_run)
99
+ ordered_tests = File.readlines('.specjour/performance').map {|l| l.chop.split(':', 2)[1]}
100
+ (tests - ordered_tests) | (ordered_tests & tests)
96
101
  else
97
- specs_to_run
102
+ tests
98
103
  end
99
104
  end
100
105
 
@@ -120,29 +125,21 @@ module Specjour
120
125
 
121
126
  def stopping
122
127
  summarize_reports
123
- warn_if_workers_deserted
124
128
  record_performance unless Specjour.interrupted?
129
+ print_missing_tests if tests_to_run.any?
125
130
  end
126
131
 
127
132
  def summarize_reports
128
133
  reporters.each {|r| r.summarize}
129
134
  end
130
135
 
131
- def synchronize(&block)
132
- @connectionsMutex.synchronize &block
136
+ def print_missing_tests
137
+ puts "*" * 60
138
+ puts "Oops! The following tests were not run:"
139
+ puts "*" * 60
140
+ puts tests_to_run
141
+ puts "*" * 60
133
142
  end
134
143
 
135
- def warn_if_workers_deserted
136
- if disconnections != completed_workers && !Specjour.interrupted?
137
- puts
138
- puts workers_deserted_message
139
- end
140
- end
141
-
142
- def workers_deserted_message
143
- data = "* ERROR: NOT ALL WORKERS COMPLETED PROPERLY *"
144
- filler = "*" * data.size
145
- [filler, data, filler].join "\n"
146
- end
147
144
  end
148
145
  end
@@ -31,7 +31,7 @@ module Specjour::RSpec
31
31
  end
32
32
 
33
33
  def close
34
- @examples = []
34
+ examples.clear
35
35
  super
36
36
  end
37
37
 
@@ -42,11 +42,11 @@ module Specjour::RSpec
42
42
  end
43
43
 
44
44
  def pending_examples
45
- ::RSpec.world.find(examples, :execution_result => { :status => 'pending' })
45
+ examples.select {|e| e.execution_result[:status] == 'pending'}
46
46
  end
47
47
 
48
48
  def failed_examples
49
- ::RSpec.world.find(examples, :execution_result => { :status => 'failed' })
49
+ examples.select {|e| e.execution_result[:status] == 'failed'}
50
50
  end
51
51
 
52
52
  def formatter
@@ -11,5 +11,9 @@ module Specjour::RSpec
11
11
  def class
12
12
  @class ||= OpenStruct.new :name => class_name
13
13
  end
14
+
15
+ def pending_fixed?
16
+ false
17
+ end
14
18
  end
15
19
  end
@@ -1,8 +1,13 @@
1
1
  class Specjour::RSpec::Preloader
2
- def self.load(spec_file)
3
- $LOAD_PATH.unshift File.join(Dir.pwd, 'spec')
4
- require spec_file
5
- ensure
6
- $LOAD_PATH.shift
2
+ def self.load(paths=[])
3
+ require './spec/spec_helper'
4
+ load_spec_files paths
5
+ end
6
+
7
+ def self.load_spec_files(paths)
8
+ options = ::RSpec::Core::ConfigurationOptions.new(paths)
9
+ options.parse_options
10
+ options.configure ::RSpec.configuration
11
+ ::RSpec.configuration.load_spec_files
7
12
  end
8
13
  end
@@ -1,12 +1,13 @@
1
1
  module Specjour::RSpec::Runner
2
+ ::RSpec::Core::Runner::AT_EXIT_HOOK_BACKTRACE_LINE.replace "#{__FILE__}:#{__LINE__ + 3}:in `run'"
2
3
  def self.run(spec, output)
3
- reset
4
4
  args = ['--format=Specjour::RSpec::DistributedFormatter', spec]
5
- ::RSpec::Core::Runner.run_in_process args, $stderr, output
6
- end
7
-
8
- def self.reset
9
- ::RSpec.world.instance_variable_set(:@example_groups, [])
10
- ::RSpec.configuration.instance_variable_set(:@formatter, nil)
5
+ ::RSpec::Core::Runner.run args, $stderr, output
6
+ ensure
7
+ ::RSpec.configuration.filter_manager = ::RSpec::Core::FilterManager.new
8
+ ::RSpec.world.filtered_examples.clear
9
+ ::RSpec.world.inclusion_filter.clear
10
+ ::RSpec.world.exclusion_filter.clear
11
+ ::RSpec.world.send(:instance_variable_set, :@line_numbers, nil)
11
12
  end
12
13
  end
@@ -11,11 +11,6 @@ module Specjour
11
11
  require 'specjour/rspec/shared_example_group_ext'
12
12
 
13
13
  ::RSpec::Core::Runner.disable_autorun!
14
-
15
- def self.wants_to_quit
16
- if defined?(::RSpec) && ::RSpec.respond_to?(:wants_to_quit=)
17
- ::RSpec.wants_to_quit = true
18
- end
19
- end
14
+ ::RSpec::Core::Runner.class_eval "def self.trap_interrupt;end"
20
15
  end
21
16
  end
@@ -5,15 +5,16 @@ module Specjour
5
5
 
6
6
  # Corresponds to the version of specjour that changed the configuration
7
7
  # file.
8
- CONFIG_VERSION = "0.3.0.rc8".freeze
8
+ CONFIG_VERSION = "0.5.0".freeze
9
9
  CONFIG_FILE_NAME = "rsyncd.conf"
10
10
  PID_FILE_NAME = "rsyncd.pid"
11
11
 
12
- attr_reader :project_path, :project_name
12
+ attr_reader :project_path, :project_name, :port
13
13
 
14
- def initialize(project_path, project_name)
14
+ def initialize(project_path, project_name, port)
15
15
  @project_path = project_path
16
16
  @project_name = project_name
17
+ @port = port
17
18
  end
18
19
 
19
20
  def config_directory
@@ -35,11 +36,12 @@ module Specjour
35
36
  end
36
37
 
37
38
  def start
39
+ Kernel.at_exit { stop }
38
40
  write_config
39
41
  Dir.chdir(project_path) do
40
42
  Kernel.system *command
43
+ sleep 0.1
41
44
  end
42
- Kernel.at_exit { stop }
43
45
  end
44
46
 
45
47
  def stop
@@ -52,7 +54,7 @@ module Specjour
52
54
  protected
53
55
 
54
56
  def command
55
- ["rsync", "--daemon", "--config=#{config_file}", "--port=8989"]
57
+ ["rsync", "--daemon", "--config=#{config_file}", "--port=#{port}"]
56
58
  end
57
59
 
58
60
  def check_config_version
@@ -92,7 +94,7 @@ remove it, and re-run the dispatcher to generate the new config file.
92
94
  # $ #{(command | ['--no-detach']).join(' ')}
93
95
  #
94
96
  # Rsync with the following command:
95
- # $ rsync -a --port=8989 #{hostname}::#{project_name} /tmp/#{project_name}
97
+ # $ rsync -a --port=#{port} #{hostname}::#{project_name} /tmp/#{project_name}
96
98
  #
97
99
  use chroot = no
98
100
  timeout = 20
@@ -101,7 +103,8 @@ pid file = ./.specjour/#{PID_FILE_NAME}
101
103
 
102
104
  [#{project_name}]
103
105
  path = .
104
- exclude = .git* .specjour/rsync* doc tmp/* log
106
+ include = tmp/cache/
107
+ exclude = .git* .specjour/rsync* doc/* tmp/* log/*
105
108
  CONFIG
106
109
  end
107
110
  end