specjour 0.4.1 → 0.5.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,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