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