nitra 0.9.3 → 0.9.4
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/nitra +7 -43
- data/lib/nitra/channel.rb +48 -0
- data/lib/nitra/client.rb +43 -0
- data/lib/nitra/command_line.rb +54 -0
- data/lib/nitra/configuration.rb +22 -0
- data/lib/nitra/master.rb +68 -0
- data/lib/nitra/progress.rb +8 -0
- data/lib/nitra/runner.rb +127 -0
- data/lib/nitra/slave.rb +71 -0
- data/lib/nitra/utils.rb +31 -0
- data/lib/nitra/worker.rb +128 -0
- data/lib/nitra.rb +12 -239
- metadata +14 -4
data/bin/nitra
CHANGED
@@ -1,47 +1,11 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
require 'nitra'
|
4
|
-
require 'optparse'
|
5
4
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
end
|
14
|
-
|
15
|
-
opts.on("-e", "--environment STRING", String, "The Rails environment to use, defaults to 'nitra'") do |environment|
|
16
|
-
nitra.environment = environment
|
17
|
-
end
|
18
|
-
|
19
|
-
opts.on("--load", "Load schema into database before running specs") do
|
20
|
-
nitra.load_schema = true
|
21
|
-
end
|
22
|
-
|
23
|
-
opts.on("--migrate", "Migrate database before running specs") do
|
24
|
-
nitra.migrate = true
|
25
|
-
end
|
26
|
-
|
27
|
-
opts.on("-q", "--quiet", "Quiet; don't display progress bar") do
|
28
|
-
nitra.quiet = true
|
29
|
-
end
|
30
|
-
|
31
|
-
opts.on("-p", "--print-failures", "Print failures immediately when they occur") do
|
32
|
-
nitra.print_failures = true
|
33
|
-
end
|
34
|
-
|
35
|
-
opts.on("--debug", "Print debug output") do
|
36
|
-
nitra.debug = true
|
37
|
-
end
|
38
|
-
|
39
|
-
opts.on_tail("-h", "--help", "Show this message") do
|
40
|
-
puts opts
|
41
|
-
exit
|
42
|
-
end
|
43
|
-
end.parse!
|
44
|
-
|
45
|
-
nitra.files = ARGV
|
46
|
-
|
47
|
-
exit nitra.run
|
5
|
+
configuration = Nitra::Configuration.new
|
6
|
+
Nitra::CommandLine.new(configuration, ARGV)
|
7
|
+
if configuration.slave_mode
|
8
|
+
Nitra::Slave::Server.new.run
|
9
|
+
else
|
10
|
+
exit Nitra::Client.new(configuration, ARGV).run ? 0 : 1
|
11
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
class Nitra::Channel
|
4
|
+
ProtocolInvalidError = Class.new(StandardError)
|
5
|
+
|
6
|
+
attr_reader :rd, :wr
|
7
|
+
attr_accessor :raise_epipe_on_write_error
|
8
|
+
|
9
|
+
def initialize(rd, wr)
|
10
|
+
@rd = rd
|
11
|
+
@wr = wr
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.pipe
|
15
|
+
c_rd, s_wr = IO.pipe
|
16
|
+
s_rd, c_wr = IO.pipe
|
17
|
+
[new(c_rd, c_wr), new(s_rd, s_wr)]
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.read_select(channels)
|
21
|
+
fds = IO.select(channels.collect(&:rd))
|
22
|
+
fds.first.collect do |fd|
|
23
|
+
channels.detect {|c| c.rd == fd}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def close
|
28
|
+
rd.close
|
29
|
+
wr.close
|
30
|
+
end
|
31
|
+
|
32
|
+
def read
|
33
|
+
return unless line = rd.gets
|
34
|
+
if result = line.strip.match(/\ANITRA,(\d+)\z/)
|
35
|
+
data = rd.read(result[1].to_i)
|
36
|
+
YAML.load(data)
|
37
|
+
else
|
38
|
+
raise ProtocolInvalidError, "Expected nitra length line, got #{line.inspect}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def write(data)
|
43
|
+
encoded = YAML.dump(data)
|
44
|
+
wr.write("NITRA,#{encoded.length}\n#{encoded}")
|
45
|
+
rescue Errno::EPIPE
|
46
|
+
raise if raise_epipe_on_write_error
|
47
|
+
end
|
48
|
+
end
|
data/lib/nitra/client.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
class Nitra::Client
|
2
|
+
attr_reader :configuration, :files
|
3
|
+
|
4
|
+
def initialize(configuration, files = nil)
|
5
|
+
@configuration = configuration
|
6
|
+
@files = files
|
7
|
+
@columns = (ENV['COLUMNS'] || 120).to_i
|
8
|
+
end
|
9
|
+
|
10
|
+
def run
|
11
|
+
start_time = Time.now
|
12
|
+
|
13
|
+
master = Nitra::Master.new(configuration, files)
|
14
|
+
progress = master.run do |progress, data|
|
15
|
+
print_progress(progress)
|
16
|
+
if data && configuration.print_failures && data["failure_count"] != 0
|
17
|
+
puts unless configuration.quiet
|
18
|
+
puts "=== output for #{data["filename"]} #{'='*40}"
|
19
|
+
puts data["text"].gsub(/\n\n\n+/, "\n\n")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
puts progress.output.gsub(/\n\n\n+/, "\n\n")
|
24
|
+
|
25
|
+
puts "\n#{progress.files_completed}/#{progress.file_count} files processed, #{progress.example_count} examples, #{progress.failure_count} failures"
|
26
|
+
puts "#{$aborted ? "Aborted after" : "Finished in"} #{"%0.1f" % (Time.now-start_time)} seconds" unless configuration.quiet
|
27
|
+
|
28
|
+
!$aborted && progress.files_completed == progress.file_count && progress.failure_count.zero?
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
def print_progress(progress)
|
33
|
+
return if configuration.quiet
|
34
|
+
|
35
|
+
bar_length = @columns - 50
|
36
|
+
progress_factor = progress.files_completed / progress.file_count.to_f
|
37
|
+
length_completed = (progress_factor * bar_length).to_i
|
38
|
+
length_to_go = bar_length - length_completed
|
39
|
+
print "\r[#{"X" * length_completed}#{"." * length_to_go}] #{progress.files_completed}/#{progress.file_count} (#{"%0.1f%%" % (progress_factor*100)}) * #{progress.example_count} examples, #{progress.failure_count} failures\r"
|
40
|
+
puts if configuration.debug
|
41
|
+
$stdout.flush
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
class Nitra::CommandLine
|
4
|
+
attr_reader :configuration
|
5
|
+
|
6
|
+
def initialize(configuration, argv)
|
7
|
+
@configuration = configuration
|
8
|
+
|
9
|
+
OptionParser.new(argv) do |opts|
|
10
|
+
opts.banner = "Usage: nitra [options] [spec_filename [...]]"
|
11
|
+
|
12
|
+
opts.on("-c", "--cpus NUMBER", Integer, "Specify the number of CPUs to use on the host, or if specified after a --slave, on the slave") do |n|
|
13
|
+
configuration.set_process_count n
|
14
|
+
end
|
15
|
+
|
16
|
+
opts.on("-e", "--environment STRING", String, "The Rails environment to use, defaults to 'nitra'") do |environment|
|
17
|
+
configuration.environment = environment
|
18
|
+
end
|
19
|
+
|
20
|
+
opts.on("--load", "Load schema into database before running specs") do
|
21
|
+
configuration.load_schema = true
|
22
|
+
end
|
23
|
+
|
24
|
+
opts.on("--migrate", "Migrate database before running specs") do
|
25
|
+
configuration.migrate = true
|
26
|
+
end
|
27
|
+
|
28
|
+
opts.on("-q", "--quiet", "Quiet; don't display progress bar") do
|
29
|
+
configuration.quiet = true
|
30
|
+
end
|
31
|
+
|
32
|
+
opts.on("-p", "--print-failures", "Print failures immediately when they occur") do
|
33
|
+
configuration.print_failures = true
|
34
|
+
end
|
35
|
+
|
36
|
+
opts.on("--slave CONNECTION_COMMAND", String, "Provide a command that executes \"nitra --slave-mode\" on another host") do |connection_command|
|
37
|
+
configuration.slaves << {:command => connection_command, :cpus => nil}
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.on("--slave-mode", "Run in slave mode; ignores all other command-line options") do
|
41
|
+
configuration.slave_mode = true
|
42
|
+
end
|
43
|
+
|
44
|
+
opts.on("--debug", "Print debug output") do
|
45
|
+
configuration.debug = true
|
46
|
+
end
|
47
|
+
|
48
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
49
|
+
puts opts
|
50
|
+
exit
|
51
|
+
end
|
52
|
+
end.parse!
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Nitra::Configuration
|
2
|
+
attr_accessor :load_schema, :migrate, :debug, :quiet, :print_failures, :fork_for_each_file
|
3
|
+
attr_accessor :process_count, :environment, :slaves, :slave_mode
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
self.environment = "nitra"
|
7
|
+
self.fork_for_each_file = true
|
8
|
+
self.slaves = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def calculate_default_process_count
|
12
|
+
self.process_count ||= Nitra::Utils.processor_count
|
13
|
+
end
|
14
|
+
|
15
|
+
def set_process_count(n)
|
16
|
+
if slaves.empty?
|
17
|
+
self.process_count = n
|
18
|
+
else
|
19
|
+
slaves.last[:cpus] = n
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/nitra/master.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
class Nitra::Master
|
2
|
+
attr_reader :configuration, :files
|
3
|
+
|
4
|
+
def initialize(configuration, files = nil)
|
5
|
+
@configuration = configuration
|
6
|
+
@files = files
|
7
|
+
end
|
8
|
+
|
9
|
+
def run
|
10
|
+
@files = Dir["spec/**/*_spec.rb"] if files.nil? || files.empty?
|
11
|
+
return if files.empty?
|
12
|
+
|
13
|
+
progress = Nitra::Progress.new
|
14
|
+
progress.file_count = @files.length
|
15
|
+
yield progress, nil
|
16
|
+
|
17
|
+
runners = []
|
18
|
+
|
19
|
+
if configuration.process_count > 0
|
20
|
+
client, runner = Nitra::Channel.pipe
|
21
|
+
fork do
|
22
|
+
runner.close
|
23
|
+
Nitra::Runner.new(configuration, client, "A").run
|
24
|
+
end
|
25
|
+
client.close
|
26
|
+
runners << runner
|
27
|
+
end
|
28
|
+
|
29
|
+
slave = Nitra::Slave::Client.new(configuration)
|
30
|
+
runners += slave.connect("")
|
31
|
+
|
32
|
+
while runners.length > 0
|
33
|
+
Nitra::Channel.read_select(runners).each do |channel|
|
34
|
+
if data = channel.read
|
35
|
+
case data["command"]
|
36
|
+
when "next"
|
37
|
+
channel.write "filename" => files.shift
|
38
|
+
when "result"
|
39
|
+
progress.files_completed += 1
|
40
|
+
progress.example_count += data["example_count"] || 0
|
41
|
+
progress.failure_count += data["failure_count"] || 0
|
42
|
+
progress.output << data["text"]
|
43
|
+
yield progress, data
|
44
|
+
when "debug"
|
45
|
+
if configuration.debug
|
46
|
+
puts "[DEBUG] #{data["text"]}"
|
47
|
+
end
|
48
|
+
when "stdout"
|
49
|
+
if configuration.debug
|
50
|
+
puts "STDOUT for #{data["process"]} #{data["filename"]}:\n#{data["text"]}" unless data["text"].empty?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
else
|
54
|
+
runners.delete channel
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
debug "waiting for all children to exit..."
|
60
|
+
Process.waitall
|
61
|
+
progress
|
62
|
+
end
|
63
|
+
|
64
|
+
protected
|
65
|
+
def debug(*text)
|
66
|
+
puts "master: #{text.join}" if configuration.debug
|
67
|
+
end
|
68
|
+
end
|
data/lib/nitra/runner.rb
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
class Nitra::Runner
|
4
|
+
attr_reader :configuration, :server_channel, :runner_id
|
5
|
+
|
6
|
+
def initialize(configuration, server_channel, runner_id)
|
7
|
+
@configuration = configuration
|
8
|
+
@server_channel = server_channel
|
9
|
+
@runner_id = runner_id
|
10
|
+
|
11
|
+
configuration.calculate_default_process_count
|
12
|
+
server_channel.raise_epipe_on_write_error = true
|
13
|
+
end
|
14
|
+
|
15
|
+
def run
|
16
|
+
ENV["RAILS_ENV"] = configuration.environment
|
17
|
+
|
18
|
+
initialise_database
|
19
|
+
|
20
|
+
load_rails_environment
|
21
|
+
|
22
|
+
pipes = start_workers
|
23
|
+
|
24
|
+
trap("SIGTERM") { $aborted = true }
|
25
|
+
trap("SIGINT") { $aborted = true }
|
26
|
+
|
27
|
+
hand_out_files_to_workers(pipes)
|
28
|
+
rescue Errno::EPIPE
|
29
|
+
ensure
|
30
|
+
trap("SIGTERM", "DEFAULT")
|
31
|
+
trap("SIGINT", "DEFAULT")
|
32
|
+
end
|
33
|
+
|
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
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def load_rails_environment
|
56
|
+
debug "loading rails environment..."
|
57
|
+
|
58
|
+
ENV["TEST_ENV_NUMBER"] = "1"
|
59
|
+
|
60
|
+
output = Nitra::Utils.capture_output do
|
61
|
+
require 'spec/spec_helper'
|
62
|
+
end
|
63
|
+
|
64
|
+
server_channel.write("command" => "stdout", "process" => "rails initialisation", "text" => output)
|
65
|
+
|
66
|
+
ActiveRecord::Base.connection.disconnect!
|
67
|
+
end
|
68
|
+
|
69
|
+
def start_workers
|
70
|
+
(0...configuration.process_count).collect do |index|
|
71
|
+
Nitra::Worker.new(runner_id, index, configuration).fork_and_run
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def hand_out_files_to_workers(pipes)
|
76
|
+
while !$aborted && pipes.length > 0
|
77
|
+
Nitra::Channel.read_select(pipes).each do |worker_channel|
|
78
|
+
unless data = worker_channel.read
|
79
|
+
pipes.delete worker_channel
|
80
|
+
debug "worker #{worker_channel} unexpectedly died."
|
81
|
+
next
|
82
|
+
end
|
83
|
+
|
84
|
+
case data['command']
|
85
|
+
when "debug", "stdout"
|
86
|
+
server_channel.write(data)
|
87
|
+
|
88
|
+
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)
|
103
|
+
|
104
|
+
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
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def debug(*text)
|
122
|
+
server_channel.write(
|
123
|
+
"command" => "debug",
|
124
|
+
"text" => "runner #{runner_id}: #{text.join}"
|
125
|
+
) if configuration.debug
|
126
|
+
end
|
127
|
+
end
|
data/lib/nitra/slave.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
module Nitra::Slave
|
2
|
+
class Client
|
3
|
+
attr_reader :configuration
|
4
|
+
|
5
|
+
def initialize(configuration)
|
6
|
+
@configuration = configuration
|
7
|
+
end
|
8
|
+
|
9
|
+
def connect(runner_id_base)
|
10
|
+
runner_id = "#{runner_id_base}A"
|
11
|
+
|
12
|
+
@configuration.slaves.collect do |slave_details|
|
13
|
+
runner_id = runner_id.succ
|
14
|
+
connect_host(slave_details, runner_id)
|
15
|
+
end.compact
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
def connect_host(slave_details, runner_id)
|
20
|
+
client, server = Nitra::Channel.pipe
|
21
|
+
|
22
|
+
puts "Starting slave runner #{runner_id} with command '#{slave_details[:command]}'" if configuration.debug
|
23
|
+
|
24
|
+
pid = fork do
|
25
|
+
server.close
|
26
|
+
$stdin.reopen(client.rd)
|
27
|
+
$stdout.reopen(client.wr)
|
28
|
+
$stderr.reopen(client.wr)
|
29
|
+
exec slave_details[:command]
|
30
|
+
end
|
31
|
+
client.close
|
32
|
+
|
33
|
+
slave_config = configuration.dup
|
34
|
+
slave_config.process_count = slave_details.fetch(:cpus)
|
35
|
+
|
36
|
+
server.write(
|
37
|
+
"command" => "configuration",
|
38
|
+
"runner_id" => runner_id,
|
39
|
+
"configuration" => slave_config)
|
40
|
+
response = server.read
|
41
|
+
|
42
|
+
if response["command"] == "connected"
|
43
|
+
puts "Connection to slave runner #{runner_id} successful" if configuration.debug
|
44
|
+
server
|
45
|
+
else
|
46
|
+
$stderr.puts "Connection to slave runner #{runner_id} FAILED with message: #{response.inspect}"
|
47
|
+
Process.kill("KILL", pid)
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class Server
|
54
|
+
attr_reader :channel
|
55
|
+
|
56
|
+
def run
|
57
|
+
@channel = Nitra::Channel.new($stdin, $stdout)
|
58
|
+
|
59
|
+
response = @channel.read
|
60
|
+
unless response && response["command"] == "configuration"
|
61
|
+
puts "handshake failed"
|
62
|
+
exit 1
|
63
|
+
end
|
64
|
+
|
65
|
+
@channel.write("command" => "connected")
|
66
|
+
|
67
|
+
runner = Nitra::Runner.new(response["configuration"], channel, response["runner_id"])
|
68
|
+
runner.run
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/lib/nitra/utils.rb
ADDED
@@ -0,0 +1,31 @@
|
|
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
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
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
|
28
|
+
end
|
29
|
+
io.string
|
30
|
+
end
|
31
|
+
end
|
data/lib/nitra/worker.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
class Nitra::Worker
|
5
|
+
attr_reader :runner_id, :worker_number, :configuration, :channel
|
6
|
+
|
7
|
+
def initialize(runner_id, worker_number, configuration)
|
8
|
+
@runner_id = runner_id
|
9
|
+
@worker_number = worker_number
|
10
|
+
@configuration = configuration
|
11
|
+
end
|
12
|
+
|
13
|
+
def fork_and_run
|
14
|
+
client, server = Nitra::Channel.pipe
|
15
|
+
|
16
|
+
fork do
|
17
|
+
server.close
|
18
|
+
@channel = client
|
19
|
+
run
|
20
|
+
end
|
21
|
+
|
22
|
+
client.close
|
23
|
+
server
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
def run
|
28
|
+
trap("SIGTERM") { Process.kill("SIGKILL", Process.pid) }
|
29
|
+
trap("SIGINT") { Process.kill("SIGKILL", Process.pid) }
|
30
|
+
|
31
|
+
debug "started"
|
32
|
+
|
33
|
+
ENV["TEST_ENV_NUMBER"] = (worker_number + 1).to_s
|
34
|
+
|
35
|
+
# Find the database config for this TEST_ENV_NUMBER and manually initialise a connection.
|
36
|
+
database_config = YAML.load(ERB.new(IO.read("#{Rails.root}/config/database.yml")).result)[ENV["RAILS_ENV"]]
|
37
|
+
ActiveRecord::Base.establish_connection(database_config)
|
38
|
+
Rails.cache.reset if Rails.cache.respond_to?(:reset)
|
39
|
+
|
40
|
+
# RSpec doesn't like it when you change the IO between invocations. So we make one object and flush it
|
41
|
+
# after every invocation.
|
42
|
+
io = StringIO.new
|
43
|
+
|
44
|
+
# When rspec processes the first spec file, it does initialisation like loading in fixtures into the
|
45
|
+
# database. If we're forking for each file, we need to initialise first so it doesn't try to initialise
|
46
|
+
# for every single file.
|
47
|
+
if configuration.fork_for_each_file
|
48
|
+
debug "running empty spec to make rspec run its initialisation"
|
49
|
+
file = Tempfile.new("nitra")
|
50
|
+
begin
|
51
|
+
file.write("require 'spec_helper'; describe('nitra preloading') { it('preloads the fixtures') { 1.should == 1 } }\n")
|
52
|
+
file.close
|
53
|
+
output = Nitra::Utils.capture_output do
|
54
|
+
RSpec::Core::CommandLine.new(["-f", "p", file.path]).run(io, io)
|
55
|
+
end
|
56
|
+
channel.write("command" => "stdout", "process" => "init rspec", "text" => output) unless output.empty?
|
57
|
+
ensure
|
58
|
+
file.close unless file.closed?
|
59
|
+
file.unlink
|
60
|
+
end
|
61
|
+
RSpec.reset
|
62
|
+
io.string = ""
|
63
|
+
end
|
64
|
+
|
65
|
+
# Loop until our master tells us we're finished.
|
66
|
+
loop do
|
67
|
+
debug "announcing availability"
|
68
|
+
channel.write("command" => "ready")
|
69
|
+
|
70
|
+
debug "waiting for next job"
|
71
|
+
data = channel.read
|
72
|
+
if data.nil? || data["command"] == "close"
|
73
|
+
debug "channel closed, exiting"
|
74
|
+
exit
|
75
|
+
end
|
76
|
+
|
77
|
+
filename = data.fetch("filename").chomp
|
78
|
+
debug "starting to process #{filename}"
|
79
|
+
|
80
|
+
perform_rspec_for_filename = lambda do
|
81
|
+
begin
|
82
|
+
result = RSpec::Core::CommandLine.new(["-f", "p", filename]).run(io, io)
|
83
|
+
rescue LoadError
|
84
|
+
io << "\nCould not load file #{filename}\n\n"
|
85
|
+
result = 1
|
86
|
+
end
|
87
|
+
|
88
|
+
channel.write("command" => "result", "filename" => filename, "return_code" => result.to_i, "text" => io.string)
|
89
|
+
end
|
90
|
+
|
91
|
+
if configuration.fork_for_each_file
|
92
|
+
rd, wr = IO.pipe
|
93
|
+
pid = fork do
|
94
|
+
rd.close
|
95
|
+
$stdout.reopen(wr)
|
96
|
+
$stderr.reopen(wr)
|
97
|
+
perform_rspec_for_filename.call
|
98
|
+
end
|
99
|
+
wr.close
|
100
|
+
stdout_buffer = ""
|
101
|
+
loop do
|
102
|
+
IO.select([rd])
|
103
|
+
text = rd.read
|
104
|
+
break if text.nil? || text.length.zero?
|
105
|
+
stdout_buffer << text
|
106
|
+
end
|
107
|
+
rd.close
|
108
|
+
Process.wait(pid) if pid
|
109
|
+
else
|
110
|
+
stdout_buffer = Nitra::Utils.capture_output do
|
111
|
+
perform_rspec_for_filename.call
|
112
|
+
end
|
113
|
+
io.string = ""
|
114
|
+
RSpec.reset
|
115
|
+
end
|
116
|
+
channel.write("command" => "stdout", "process" => "rspec", "filename" => filename, "text" => stdout_buffer) unless stdout_buffer.empty?
|
117
|
+
|
118
|
+
debug "#{filename} processed"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def debug(*text)
|
123
|
+
channel.write(
|
124
|
+
"command" => "debug",
|
125
|
+
"text" => "worker #{runner_id}.#{worker_number}: #{text.join}"
|
126
|
+
) if configuration.debug
|
127
|
+
end
|
128
|
+
end
|
data/lib/nitra.rb
CHANGED
@@ -1,240 +1,13 @@
|
|
1
|
-
|
2
|
-
require 'tempfile'
|
3
|
-
|
4
|
-
class Nitra
|
5
|
-
attr_accessor :load_schema, :migrate, :debug, :quiet, :print_failures, :fork_for_each_file
|
6
|
-
attr_accessor :files
|
7
|
-
attr_accessor :process_count, :environment
|
8
|
-
|
9
|
-
def initialize
|
10
|
-
self.process_count = 4
|
11
|
-
self.environment = "nitra"
|
12
|
-
self.fork_for_each_file = true
|
13
|
-
end
|
14
|
-
|
15
|
-
def run
|
16
|
-
start_time = Time.now
|
17
|
-
ENV["RAILS_ENV"] = environment
|
18
|
-
|
19
|
-
initialise_database
|
20
|
-
|
21
|
-
load_rails_environment
|
22
|
-
|
23
|
-
pipes = fork_workers
|
24
|
-
|
25
|
-
self.files = Dir["spec/**/*_spec.rb"] if files.nil? || files.empty?
|
26
|
-
return if files.empty?
|
27
|
-
|
28
|
-
trap("SIGTERM") { $aborted = true }
|
29
|
-
trap("SIGINT") { $aborted = true }
|
30
|
-
|
31
|
-
return_code, result = hand_out_files_to_workers(files, pipes)
|
32
|
-
|
33
|
-
trap("SIGTERM", "DEFAULT")
|
34
|
-
trap("SIGINT", "DEFAULT")
|
35
|
-
|
36
|
-
print_result(result)
|
37
|
-
puts "\n#{$aborted ? "Aborted after" : "Finished in"} #{"%0.1f" % (Time.now-start_time)} seconds" unless quiet
|
38
|
-
|
39
|
-
$aborted ? 255 : return_code
|
40
|
-
end
|
41
|
-
|
42
|
-
protected
|
43
|
-
def print_result(result)
|
44
|
-
puts result.gsub(/\n\n\n+/, "\n\n")
|
45
|
-
end
|
46
|
-
|
47
|
-
def print_progress
|
48
|
-
unless quiet
|
49
|
-
bar_length = @columns - 50
|
50
|
-
progress = @files_completed / @file_count.to_f
|
51
|
-
length_completed = (progress * bar_length).to_i
|
52
|
-
length_to_go = bar_length - length_completed
|
53
|
-
print "[#{"X" * length_completed}#{"." * length_to_go}] #{@files_completed}/#{@file_count} (#{"%0.1f%%" % (progress*100)}) * #{@example_count} examples, #{@failure_count} failures\r"
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
def initialise_database
|
58
|
-
if load_schema
|
59
|
-
process_count.times do |index|
|
60
|
-
puts "initialising database #{index+1}..." unless quiet
|
61
|
-
ENV["TEST_ENV_NUMBER"] = (index + 1).to_s
|
62
|
-
system("bundle exec rake db:drop db:create db:schema:load")
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
if migrate
|
67
|
-
process_count.times do |index|
|
68
|
-
puts "migrating database #{index+1}..." unless quiet
|
69
|
-
ENV["TEST_ENV_NUMBER"] = (index + 1).to_s
|
70
|
-
system("bundle exec rake db:migrate")
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
def load_rails_environment
|
76
|
-
puts "loading rails environment..." if debug
|
77
|
-
|
78
|
-
ENV["TEST_ENV_NUMBER"] = "1"
|
79
|
-
|
80
|
-
require 'spec/spec_helper'
|
81
|
-
|
82
|
-
ActiveRecord::Base.connection.disconnect!
|
83
|
-
end
|
84
|
-
|
85
|
-
def fork_workers
|
86
|
-
(0...process_count).collect do |index|
|
87
|
-
server_sender_pipe = IO.pipe
|
88
|
-
client_sender_pipe = IO.pipe
|
89
|
-
|
90
|
-
fork do
|
91
|
-
trap("SIGTERM") { Process.kill("SIGKILL", Process.pid) }
|
92
|
-
trap("SIGINT") { Process.kill("SIGKILL", Process.pid) }
|
93
|
-
|
94
|
-
server_sender_pipe[1].close
|
95
|
-
client_sender_pipe[0].close
|
96
|
-
rd = server_sender_pipe[0]
|
97
|
-
wr = client_sender_pipe[1]
|
98
|
-
|
99
|
-
ENV["TEST_ENV_NUMBER"] = (index + 1).to_s
|
100
|
-
|
101
|
-
# Find the database config for this TEST_ENV_NUMBER and manually initialise a connection.
|
102
|
-
database_config = YAML.load(ERB.new(IO.read("#{Rails.root}/config/database.yml")).result)[ENV["RAILS_ENV"]]
|
103
|
-
ActiveRecord::Base.establish_connection(database_config)
|
104
|
-
Rails.cache.reset if Rails.cache.respond_to?(:reset)
|
105
|
-
|
106
|
-
# RSpec doesn't like it when you change the IO between invocations. So we make one object and flush it
|
107
|
-
# after every invocation.
|
108
|
-
io = StringIO.new
|
109
|
-
|
110
|
-
# When rspec processes the first spec file, it does initialisation like loading in fixtures into the
|
111
|
-
# database. If we're forking for each file, we need to initialise first so it doesn't try to initialise
|
112
|
-
# for every single file.
|
113
|
-
if fork_for_each_file
|
114
|
-
puts "running empty spec to make rspec run its initialisation" if debug
|
115
|
-
file = Tempfile.new("nitra")
|
116
|
-
begin
|
117
|
-
file.write("require 'spec_helper'; describe('nitra preloading') { it('preloads the fixtures') { 1.should == 1 } }\n")
|
118
|
-
file.close
|
119
|
-
RSpec::Core::CommandLine.new(["-f", "p", file.path]).run(io, io)
|
120
|
-
ensure
|
121
|
-
file.close unless file.closed?
|
122
|
-
file.unlink
|
123
|
-
end
|
124
|
-
RSpec.reset
|
125
|
-
io.string = ""
|
126
|
-
end
|
127
|
-
|
128
|
-
# OK, we're good to receive requests. Tell our master.
|
129
|
-
puts "announcing availability" if debug
|
130
|
-
wr.write("0,0\n")
|
131
|
-
|
132
|
-
# Loop until our master tells us we're finished.
|
133
|
-
loop do
|
134
|
-
puts "#{index} waiting for next job" if debug
|
135
|
-
filename = rd.gets
|
136
|
-
exit if filename.blank?
|
137
|
-
filename = filename.chomp
|
138
|
-
puts "#{index} starting to process #{filename}" if debug
|
139
|
-
|
140
|
-
perform_rspec_for_filename = lambda do
|
141
|
-
begin
|
142
|
-
result = RSpec::Core::CommandLine.new(["-f", "p", filename]).run(io, io)
|
143
|
-
rescue LoadError
|
144
|
-
io << "\nCould not load file #{filename}\n\n"
|
145
|
-
result = 1
|
146
|
-
end
|
147
|
-
|
148
|
-
wr.write("#{result.to_i},#{io.string.length}\n#{io.string}")
|
149
|
-
end
|
150
|
-
|
151
|
-
if fork_for_each_file
|
152
|
-
pid = fork(&perform_rspec_for_filename)
|
153
|
-
Process.wait(pid) if pid
|
154
|
-
else
|
155
|
-
perform_rspec_for_filename.call
|
156
|
-
io.string = ""
|
157
|
-
RSpec.reset
|
158
|
-
end
|
159
|
-
|
160
|
-
puts "#{index} #{filename} processed" if debug
|
161
|
-
end
|
162
|
-
end
|
163
|
-
|
164
|
-
server_sender_pipe[0].close
|
165
|
-
client_sender_pipe[1].close
|
166
|
-
[client_sender_pipe[0], server_sender_pipe[1]]
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
def hand_out_files_to_workers(files, pipes)
|
171
|
-
puts "Running rspec on #{files.length} files spread across #{process_count} processes\n\n" unless quiet
|
172
|
-
|
173
|
-
@columns = (ENV['COLUMNS'] || 120).to_i
|
174
|
-
@file_count = files.length
|
175
|
-
@files_completed = 0
|
176
|
-
@example_count = 0
|
177
|
-
@failure_count = 0
|
178
|
-
|
179
|
-
result = ""
|
180
|
-
worst_return_code = 0
|
181
|
-
readers = pipes.collect(&:first)
|
182
|
-
|
183
|
-
while !$aborted && readers.length > 0
|
184
|
-
print_progress
|
185
|
-
fds = IO.select(readers)
|
186
|
-
fds.first.each do |fd|
|
187
|
-
unless value = fd.gets
|
188
|
-
readers.delete(fd)
|
189
|
-
worst_return_code = 255
|
190
|
-
if readers.empty?
|
191
|
-
puts "Worker unexpectedly died. No more workers to run specs - dying."
|
192
|
-
else
|
193
|
-
puts "Worker unexpectedly died. Trying to continue with fewer workers."
|
194
|
-
end
|
195
|
-
break
|
196
|
-
end
|
197
|
-
|
198
|
-
return_code, length = value.split(",")
|
199
|
-
worst_return_code = return_code.to_i if worst_return_code < return_code.to_i
|
200
|
-
|
201
|
-
if length.to_i > 0
|
202
|
-
data = fd.read(length.to_i)
|
203
|
-
|
204
|
-
@files_completed += 1
|
205
|
-
failure_count = 0
|
206
|
-
|
207
|
-
if m = data.match(/(\d+) examples?, (\d+) failure/)
|
208
|
-
@example_count += m[1].to_i
|
209
|
-
failure_count += m[2].to_i
|
210
|
-
end
|
211
|
-
|
212
|
-
@failure_count += failure_count
|
213
|
-
stripped_data = data.gsub(/^[.FP*]+$/, '').gsub(/\nFailed examples:.+/m, '').gsub(/^Finished in.+$/, '').gsub(/^\d+ example.+$/, '').gsub(/^No examples found.$/, '').gsub(/^Failures:$/, '')
|
214
|
-
|
215
|
-
if print_failures && failure_count > 0
|
216
|
-
print_result(stripped_data)
|
217
|
-
else
|
218
|
-
result << stripped_data
|
219
|
-
end
|
220
|
-
else
|
221
|
-
puts "ZERO LENGTH" if debug
|
222
|
-
end
|
223
|
-
|
224
|
-
wr = pipes.detect {|rd, wr| rd == fd}[1]
|
225
|
-
if files.length.zero?
|
226
|
-
wr.puts ""
|
227
|
-
readers.delete(fd)
|
228
|
-
else
|
229
|
-
puts "master is sending #{files.first} to fd #{wr}" if debug
|
230
|
-
wr.puts files.shift
|
231
|
-
end
|
232
|
-
end
|
233
|
-
end
|
234
|
-
|
235
|
-
print_progress
|
236
|
-
puts "" unless quiet
|
237
|
-
|
238
|
-
[worst_return_code, result]
|
239
|
-
end
|
1
|
+
module Nitra
|
240
2
|
end
|
3
|
+
|
4
|
+
require 'nitra/channel'
|
5
|
+
require 'nitra/client'
|
6
|
+
require 'nitra/command_line'
|
7
|
+
require 'nitra/configuration'
|
8
|
+
require 'nitra/master'
|
9
|
+
require 'nitra/progress'
|
10
|
+
require 'nitra/runner'
|
11
|
+
require 'nitra/slave'
|
12
|
+
require 'nitra/utils'
|
13
|
+
require 'nitra/worker'
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: nitra
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 51
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 9
|
9
|
-
-
|
10
|
-
version: 0.9.
|
9
|
+
- 4
|
10
|
+
version: 0.9.4
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Roger Nesbitt
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2012-06-
|
18
|
+
date: 2012-06-08 00:00:00 +12:00
|
19
19
|
default_executable:
|
20
20
|
dependencies: []
|
21
21
|
|
@@ -31,6 +31,16 @@ files:
|
|
31
31
|
- README
|
32
32
|
- lib/nitra.rb
|
33
33
|
- bin/nitra
|
34
|
+
- lib/nitra/channel.rb
|
35
|
+
- lib/nitra/client.rb
|
36
|
+
- lib/nitra/command_line.rb
|
37
|
+
- lib/nitra/configuration.rb
|
38
|
+
- lib/nitra/master.rb
|
39
|
+
- lib/nitra/progress.rb
|
40
|
+
- lib/nitra/runner.rb
|
41
|
+
- lib/nitra/slave.rb
|
42
|
+
- lib/nitra/utils.rb
|
43
|
+
- lib/nitra/worker.rb
|
34
44
|
has_rdoc: true
|
35
45
|
homepage:
|
36
46
|
licenses: []
|