nitra 0.9.4 → 0.9.5
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.
- 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
|