nitra 0.9.4 → 0.9.5

Sign up to get free protection for your applications and to get access to all the features.
data/lib/nitra/runner.rb CHANGED
@@ -1,127 +1,184 @@
1
1
  require 'stringio'
2
2
 
3
3
  class Nitra::Runner
4
- attr_reader :configuration, :server_channel, :runner_id
4
+ attr_reader :configuration, :server_channel, :runner_id, :framework, :workers, :tasks
5
5
 
6
6
  def initialize(configuration, server_channel, runner_id)
7
- @configuration = configuration
8
- @server_channel = server_channel
9
- @runner_id = runner_id
7
+ ENV["RAILS_ENV"] = configuration.environment
8
+
9
+ @workers = {}
10
+ @runner_id = runner_id
11
+ @framework = configuration.framework
12
+ @configuration = configuration
13
+ @server_channel = server_channel
14
+ @tasks = Nitra::Tasks.new(self)
10
15
 
11
16
  configuration.calculate_default_process_count
12
17
  server_channel.raise_epipe_on_write_error = true
13
18
  end
14
19
 
15
20
  def run
16
- ENV["RAILS_ENV"] = configuration.environment
17
-
18
- initialise_database
21
+ tasks.run(:before_runner)
19
22
 
20
23
  load_rails_environment
21
24
 
22
- pipes = start_workers
25
+ tasks.run(:before_worker, configuration.process_count)
26
+
27
+ start_workers
23
28
 
24
29
  trap("SIGTERM") { $aborted = true }
25
30
  trap("SIGINT") { $aborted = true }
26
31
 
27
- hand_out_files_to_workers(pipes)
32
+ hand_out_files_to_workers
33
+
34
+ tasks.run(:after_runner)
28
35
  rescue Errno::EPIPE
29
36
  ensure
30
37
  trap("SIGTERM", "DEFAULT")
31
38
  trap("SIGINT", "DEFAULT")
32
39
  end
33
40
 
34
- protected
35
- def initialise_database
36
- if configuration.load_schema
37
- configuration.process_count.times do |index|
38
- debug "initialising database #{index+1}..."
39
- ENV["TEST_ENV_NUMBER"] = (index + 1).to_s
40
- output = `bundle exec rake db:drop db:create db:schema:load 2>&1`
41
- server_channel.write("command" => "stdout", "process" => "db:schema:load", "text" => output)
42
- end
43
- end
44
-
45
- if configuration.migrate
46
- configuration.process_count.times do |index|
47
- debug "migrating database #{index+1}..."
48
- ENV["TEST_ENV_NUMBER"] = (index + 1).to_s
49
- output = `bundle exec rake db:migrate 2>&1`
50
- server_channel.write("command" => "stdout", "process" => "db:migrate", "text" => output)
51
- end
41
+ def debug(*text)
42
+ if configuration.debug
43
+ server_channel.write("command" => "debug", "text" => "runner #{runner_id}: #{text.join}")
52
44
  end
53
45
  end
54
46
 
47
+ protected
48
+
55
49
  def load_rails_environment
56
- debug "loading rails environment..."
50
+ return unless File.file?('config/application.rb')
51
+ debug "Loading rails environment..."
57
52
 
58
53
  ENV["TEST_ENV_NUMBER"] = "1"
59
54
 
60
55
  output = Nitra::Utils.capture_output do
61
- require 'spec/spec_helper'
56
+ require './config/application'
57
+ Rails.application.require_environment!
58
+ ActiveRecord::Base.connection.disconnect!
62
59
  end
63
60
 
64
61
  server_channel.write("command" => "stdout", "process" => "rails initialisation", "text" => output)
65
-
66
- ActiveRecord::Base.connection.disconnect!
67
62
  end
68
63
 
69
64
  def start_workers
70
- (0...configuration.process_count).collect do |index|
71
- Nitra::Worker.new(runner_id, index, configuration).fork_and_run
65
+ (1..configuration.process_count).collect do |index|
66
+ start_worker(index)
72
67
  end
73
68
  end
74
69
 
75
- def hand_out_files_to_workers(pipes)
76
- while !$aborted && pipes.length > 0
77
- Nitra::Channel.read_select(pipes).each do |worker_channel|
70
+ def start_worker(index)
71
+ pid, pipe = Nitra::Workers::Worker.worker_classes[framework].new(runner_id, index, configuration).fork_and_run
72
+ workers[index] = {:pid => pid, :pipe => pipe}
73
+ end
74
+
75
+ def worker_pipes
76
+ workers.collect {|index, worker_hash| worker_hash[:pipe]}
77
+ end
78
+
79
+ def hand_out_files_to_workers
80
+ while !$aborted && workers.length > 0
81
+ Nitra::Channel.read_select(worker_pipes + [server_channel]).each do |worker_channel|
82
+
83
+ # This is our back-channel that lets us know in case the master is dead.
84
+ kill_workers if worker_channel == server_channel && server_channel.rd.eof?
85
+
78
86
  unless data = worker_channel.read
79
- pipes.delete worker_channel
80
- debug "worker #{worker_channel} unexpectedly died."
87
+ worker_number, worker_hash = workers.find {|number, hash| hash[:pipe] == worker_channel}
88
+ workers.delete worker_number
89
+ debug "Worker #{worker_number} unexpectedly died."
81
90
  next
82
91
  end
83
92
 
84
93
  case data['command']
85
- when "debug", "stdout"
94
+ when "debug", "stdout", "error"
86
95
  server_channel.write(data)
87
96
 
88
97
  when "result"
89
- if m = data['text'].match(/(\d+) examples?, (\d+) failure/)
90
- example_count = m[1].to_i
91
- failure_count = m[2].to_i
92
- end
93
-
94
- stripped_data = data['text'].gsub(/^[.FP*]+$/, '').gsub(/\nFailed examples:.+/m, '').gsub(/^Finished in.+$/, '').gsub(/^\d+ example.+$/, '').gsub(/^No examples found.$/, '').gsub(/^Failures:$/, '')
95
-
96
- server_channel.write(
97
- "command" => "result",
98
- "filename" => data["filename"],
99
- "return_code" => data["return_code"],
100
- "example_count" => example_count,
101
- "failure_count" => failure_count,
102
- "text" => stripped_data)
98
+ handle_result(data)
103
99
 
104
100
  when "ready"
105
- server_channel.write("command" => "next")
106
- next_file = server_channel.read.fetch("filename")
107
-
108
- if next_file
109
- debug "sending #{next_file} to channel #{worker_channel}"
110
- worker_channel.write "command" => "process", "filename" => next_file
111
- else
112
- debug "sending close message to channel #{worker_channel}"
113
- worker_channel.write "command" => "close"
114
- pipes.delete worker_channel
115
- end
101
+ handle_ready(data, worker_channel)
116
102
  end
117
103
  end
118
104
  end
119
105
  end
120
106
 
121
- def debug(*text)
107
+ ##
108
+ # This parses the results we got back from the worker.
109
+ #
110
+ # It needs rewriting when we finally rewrite the workers to use custom formatters.
111
+ #
112
+ # Also, it's probably buggy as hell...
113
+ #
114
+ def handle_result(data)
115
+ #defaults - theoretically anything can end up here so we just want to pass on useful data
116
+ result_text = ""
117
+ example_count = 0
118
+ failure_count = 0
119
+ return_code = data["return_code"].to_i
120
+
121
+ # Rspec result
122
+ if m = data['text'].match(/(\d+) examples?, (\d+) failure/)
123
+ example_count = m[1].to_i
124
+ failure_count = m[2].to_i
125
+
126
+ # Cucumber result
127
+ elsif m = data['text'].match(/(\d+) scenarios?.+$/)
128
+ example_count = m[1].to_i
129
+ if m = data['text'].match(/\d+ scenarios? \(.*(\d+) [failed|undefined].*\)/)
130
+ failure_count = m[1].to_i
131
+ else
132
+ failure_count = 0
133
+ end
134
+ end
135
+
136
+ result_text = data['text'] if failure_count > 0 || return_code != 0
137
+
122
138
  server_channel.write(
123
- "command" => "debug",
124
- "text" => "runner #{runner_id}: #{text.join}"
125
- ) if configuration.debug
139
+ "command" => "result",
140
+ "filename" => data["filename"],
141
+ "return_code" => return_code,
142
+ "example_count" => example_count,
143
+ "failure_count" => failure_count,
144
+ "text" => result_text)
145
+ end
146
+
147
+ def handle_ready(data, worker_channel)
148
+ worker_number = data["worker_number"]
149
+ server_channel.write("command" => "next", "framework" => data["framework"])
150
+ data = server_channel.read
151
+
152
+ case data["command"]
153
+ when "framework"
154
+ close_worker(worker_number, worker_channel)
155
+
156
+ @framework = data["framework"]
157
+ debug "Restarting #{worker_number} with framework #{framework}"
158
+ start_worker(worker_number)
159
+
160
+ when "file"
161
+ debug "Sending #{data["filename"]} to #{worker_number}"
162
+ worker_channel.write "command" => "process", "filename" => data["filename"]
163
+
164
+ when "drain"
165
+ close_worker(worker_number, worker_channel)
166
+ end
167
+ end
168
+
169
+ def close_worker(worker_number, worker_channel)
170
+ debug "Sending close message to #{worker_number}"
171
+ worker_channel.write "command" => "close"
172
+ workers.delete worker_number
173
+ end
174
+
175
+ ##
176
+ # Kill the workers.
177
+ #
178
+ def kill_workers
179
+ worker_pids = workers.collect{|index, hash| hash[:pid]}
180
+ worker_pids.each {|pid| Process.kill('USR1', pid) rescue Errno::ESRCH}
181
+ Process.waitall
182
+ exit
126
183
  end
127
184
  end
data/lib/nitra/slave.rb CHANGED
@@ -6,17 +6,25 @@ module Nitra::Slave
6
6
  @configuration = configuration
7
7
  end
8
8
 
9
- def connect(runner_id_base)
10
- runner_id = "#{runner_id_base}A"
11
-
9
+ ##
10
+ # Starts the slave runners.
11
+ #
12
+ # We do this in two steps, starts them all and then sends them their configurations.
13
+ # This extra complexity speeds up the initial startup when working with many slaves.
14
+ #
15
+ def connect
16
+ runner_id = "A"
12
17
  @configuration.slaves.collect do |slave_details|
13
18
  runner_id = runner_id.succ
14
- connect_host(slave_details, runner_id)
19
+ server = start_host(slave_details, runner_id)
20
+ [server, slave_details, runner_id]
21
+ end.collect do |server, slave_details, runner_id|
22
+ configure_host(server, slave_details, runner_id)
15
23
  end.compact
16
24
  end
17
25
 
18
26
  protected
19
- def connect_host(slave_details, runner_id)
27
+ def start_host(slave_details, runner_id)
20
28
  client, server = Nitra::Channel.pipe
21
29
 
22
30
  puts "Starting slave runner #{runner_id} with command '#{slave_details[:command]}'" if configuration.debug
@@ -29,7 +37,10 @@ module Nitra::Slave
29
37
  exec slave_details[:command]
30
38
  end
31
39
  client.close
40
+ server
41
+ end
32
42
 
43
+ def configure_host(server, slave_details, runner_id)
33
44
  slave_config = configuration.dup
34
45
  slave_config.process_count = slave_details.fetch(:cpus)
35
46
 
@@ -43,7 +54,7 @@ module Nitra::Slave
43
54
  puts "Connection to slave runner #{runner_id} successful" if configuration.debug
44
55
  server
45
56
  else
46
- $stderr.puts "Connection to slave runner #{runner_id} FAILED with message: #{response.inspect}"
57
+ $stderr.concat "Connection to slave runner #{runner_id} FAILED with message: #{response.inspect}"
47
58
  Process.kill("KILL", pid)
48
59
  nil
49
60
  end
@@ -65,6 +76,7 @@ module Nitra::Slave
65
76
  @channel.write("command" => "connected")
66
77
 
67
78
  runner = Nitra::Runner.new(response["configuration"], channel, response["runner_id"])
79
+
68
80
  runner.run
69
81
  end
70
82
  end
@@ -0,0 +1,55 @@
1
+ class Nitra::Tasks
2
+ attr_reader :runner
3
+
4
+ def initialize(runner)
5
+ @runner = runner
6
+ if runner.configuration.rake_tasks.keys.any?
7
+ require 'rake'
8
+ Rake.load_rakefile("Rakefile")
9
+ end
10
+ end
11
+
12
+ def run(name, count = 1)
13
+ return unless tasks = runner.configuration.rake_tasks[name]
14
+ runner.debug "Running #{name} tasks: #{tasks.inspect}"
15
+ rd, wr = IO.pipe
16
+ (1..count).collect do |index|
17
+ fork do
18
+ ENV["TEST_ENV_NUMBER"] = index.to_s
19
+ rd.close
20
+ $stdout.reopen(wr)
21
+ $stderr.reopen(wr)
22
+ connect_to_database
23
+ Array(tasks).each do |task|
24
+ Rake::Task[task].invoke
25
+ end
26
+ end
27
+ end
28
+ wr.close
29
+ output = ""
30
+ loop do
31
+ IO.select([rd])
32
+ text = rd.read
33
+ break if text.nil? || text.length.zero?
34
+ output.concat text
35
+ end
36
+ rd.close
37
+ successful = all_children_successful?
38
+ runner.server_channel.write("command" => (successful ? 'stdout' : 'error'), "process" => tasks.inspect, "text" => output)
39
+ exit if !successful
40
+ end
41
+
42
+ private
43
+
44
+ def connect_to_database
45
+ Nitra::RailsTooling.connect_to_database
46
+ end
47
+
48
+ ##
49
+ # Reap the exit codes for any forked processes and report failures.
50
+ #
51
+ def all_children_successful?
52
+ Process.waitall.all? { |pid, process| process.success? }
53
+ end
54
+
55
+ end
data/lib/nitra/utils.rb CHANGED
@@ -1,31 +1,35 @@
1
- class Nitra::Utils
2
- # The following taken and modified from the 'parallel' gem.
3
- # Licensed under the MIT licence, copyright Michael Grosser.
4
- def self.processor_count
5
- @processor_count ||= case `uname`
6
- when /darwin/i
7
- (`which hwprefs` != '' ? `hwprefs thread_count` : `sysctl -n hw.ncpu`).to_i
8
- when /linux/i
9
- `grep -c processor /proc/cpuinfo`.to_i
10
- when /freebsd/i
11
- `sysctl -n hw.ncpu`.to_i
12
- when /solaris2/i
13
- `psrinfo -p`.to_i # this is physical cpus afaik
14
- else
15
- 1
1
+ require 'stringio'
2
+
3
+ module Nitra
4
+ class Utils
5
+ # The following taken and modified from the 'parallel' gem.
6
+ # Licensed under the MIT licence, copyright Michael Grosser.
7
+ def self.processor_count
8
+ @processor_count ||= case `uname`
9
+ when /darwin/i
10
+ (`which hwprefs` != '' ? `hwprefs thread_count` : `sysctl -n hw.ncpu`).to_i
11
+ when /linux/i
12
+ `grep -c processor /proc/cpuinfo`.to_i
13
+ when /freebsd/i
14
+ `sysctl -n hw.ncpu`.to_i
15
+ when /solaris2/i
16
+ `psrinfo -p`.to_i # this is physical cpus afaik
17
+ else
18
+ 1
19
+ end
16
20
  end
17
- end
18
21
 
19
- def self.capture_output
20
- old_stdout = $stdout
21
- old_stderr = $stderr
22
- $stdout = $stderr = io = StringIO.new
23
- begin
24
- yield
25
- ensure
26
- $stdout = old_stdout
27
- $stderr = old_stderr
22
+ def self.capture_output
23
+ old_stdout = $stdout
24
+ old_stderr = $stderr
25
+ $stdout = $stderr = io = StringIO.new
26
+ begin
27
+ yield
28
+ ensure
29
+ $stdout = old_stdout
30
+ $stderr = old_stderr
31
+ end
32
+ io.string
28
33
  end
29
- io.string
30
34
  end
31
35
  end