nitra 0.9.3 → 0.9.4
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/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: []
|