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/README.md +88 -0
- data/bin/nitra +2 -1
- data/lib/nitra/channel.rb +37 -35
- data/lib/nitra/command_line.rb +58 -38
- data/lib/nitra/configuration.rb +39 -17
- data/lib/nitra/ext/cucumber.rb +32 -0
- data/lib/nitra/formatter.rb +59 -0
- data/lib/nitra/master.rb +69 -14
- data/lib/nitra/progress.rb +19 -1
- data/lib/nitra/rails_tooling.rb +22 -0
- data/lib/nitra/runner.rb +125 -68
- data/lib/nitra/slave.rb +18 -6
- data/lib/nitra/tasks.rb +55 -0
- data/lib/nitra/utils.rb +30 -26
- data/lib/nitra/worker.rb +175 -92
- data/lib/nitra/workers/cucumber.rb +62 -0
- data/lib/nitra/workers/rspec.rb +65 -0
- data/lib/nitra.rb +5 -1
- data/spec/nitra/channel_spec.rb +44 -0
- data/spec/nitra/command_line_spec.rb +133 -0
- data/spec/nitra/configuration_spec.rb +60 -0
- data/spec/nitra/formatter.rb +126 -0
- data/spec/nitra/tasks_spec.rb +16 -0
- data/spec/nitra/worker_spec.rb +13 -0
- metadata +78 -46
- data/README +0 -3
- data/lib/nitra/client.rb +0 -43
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
|
-
|
8
|
-
|
9
|
-
@
|
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
|
-
|
17
|
-
|
18
|
-
initialise_database
|
21
|
+
tasks.run(:before_runner)
|
19
22
|
|
20
23
|
load_rails_environment
|
21
24
|
|
22
|
-
|
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
|
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
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
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 '
|
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
|
-
(
|
71
|
-
|
65
|
+
(1..configuration.process_count).collect do |index|
|
66
|
+
start_worker(index)
|
72
67
|
end
|
73
68
|
end
|
74
69
|
|
75
|
-
def
|
76
|
-
|
77
|
-
|
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
|
-
|
80
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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"
|
124
|
-
"
|
125
|
-
|
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
|
-
|
10
|
-
|
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
|
-
|
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
|
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.
|
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
|
data/lib/nitra/tasks.rb
ADDED
@@ -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
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|