nitra 0.9.4 → 0.9.5

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # Nitra
2
+ Nitra is a multi-process, optionally multi-server rspec and cucumber runner that uses forking to reduce memory usage and IPC to distribute builds amongst available CPUs efficiently.
3
+
4
+ ## Philosophy
5
+ * Nitra attempts to do the simplest thing possible
6
+ * Nitra (ab)uses unix primitives where possible
7
+ * Nitra doesn't do thing that unix already does better (eg. rsync)
8
+ * IPC is accomplished via pipes and select
9
+ * Forking is used heavily for several reasons
10
+ * Running nitra locally should be easy
11
+ * Running nitra on a cluster should be easy too (though verbose)
12
+ * Config files are a nuisance and should be stuffed into rake files (deals with the verbosity)
13
+
14
+ ## Usage
15
+ nitra [options] [spec_filename [...]]
16
+ -c, --cpus NUMBER Specify the number of CPUs to use on the host, or if specified after a --slave, on the slave
17
+ --cucumber Add full cucumber run, causes any files you list manually to be ignored
18
+ --debug Print debug output
19
+ -p, --print-failures Print failures immediately when they occur
20
+ -q, --quiet Quiet; don't display progress bar
21
+ --rake-after-runner task:1,task:2,task:3
22
+ The list of rake tasks to run, once per runner, in the runner's environment, just before the runner exits
23
+ --rake-before-runner task:1,task:2,task:3
24
+ The list of rake tasks to run, once per runner, in the runner's environment, after the runner starts
25
+ --rake-before-worker task:1,task:2,task:3
26
+ The list of rake tasks to run, once per worker, in the worker's environment, before the worker starts
27
+ --reset r
28
+ Reset database, equivalent to --rake-before-worker db:reset
29
+ --slave-mode Run in slave mode; ignores all other command-line options
30
+ --slave CONNECTION_COMMAND Provide a command that executes "nitra --slave-mode" on another host
31
+ --rspec Add full rspec run, causes any files you list manually to be ignored
32
+ -e, --environment ENV Set the RAILS_ENV to load
33
+ -h, --help Show this message
34
+
35
+ ### Getting started
36
+ First things first add nitra to your Gemfile:
37
+
38
+ gem 'nitra'
39
+
40
+ Then run your specs locally across your cpu's cores:
41
+
42
+ bundle exec nitra --rspec
43
+
44
+ This will just run all your specs using the default number of cpu's reported by your system. Hyperthreaded intels will report a high number which might not be a good fit, you can tune this with the -c option.
45
+
46
+ Clustered commands run slightly differently. Effectively nitra will fork and exec a command that it expects will be a nitra slave, this means we can use ssh as our tunnel of choice. It looks something like this:
47
+
48
+ bundle exec nitra --rspec --slave "ssh your.server.name 'cd your/project && bundle exec nitra --slave-mode'"
49
+
50
+ When nitra --slave command it forks, execs it, and assumes it's another process that's running "nitra --slave-mode".
51
+
52
+ ### Running a build cluster
53
+ Nitra doesn't prescribe how you get your code onto the other machines in your cluster. For example you can run a git checkout on the boxes you want to build on if it's the fastest way for you. For our part - we've had the most success with rsync.
54
+
55
+ Our build is run via rake tasks so we end up with a bunch of generated code to rsync files back and forth - here's a basic version that might help get you up and running:
56
+
57
+ namespace :nitra do
58
+ task :config do
59
+ @servers = [
60
+ {:name => "server1", :port => "66666", :c => "4"},
61
+ {:name => "server2", :port => "99999", :c => "2"},
62
+ {:name => "server3", :port => "77777", :c => "8"},
63
+ {:name => "server4", :port => "88888", :c => "4"}
64
+ ]
65
+ end
66
+
67
+ desc "Sync the local directory and install the gems onto the remote servers"
68
+ task :prep => :config do
69
+ @servers.each do |server|
70
+ fork do
71
+ system "ssh -p #{server[:port]} #{server[:name]} 'mkdir -p nitra/projects/your_project'"
72
+ exec %(rsync -aze 'ssh -q -p #{server[:port]}' --exclude .git --delete ./ #{server[:name]}:nitra/projects/your_project/ && ssh -q -t -p #{server[:port]} #{server[:name]} 'cd nitra/projects/your_project && bundle install --quiet --path ~/gems')
73
+ end
74
+ end
75
+ Process.waitall
76
+ end
77
+
78
+ task :all => :config do
79
+ cmd = %(bundle exec nitra -p -q --rspec --cucumber --rake-before-worker db:reset)
80
+ @servers.each do |server|
81
+ cmd << %( --slave "ssh -p #{server[:port]} #{server[:name]} 'cd nitra/projects/your_project && bundle exec nitra --slave-mode'" -c#{server[:c]})
82
+ end
83
+ system cmd
84
+ end
85
+ end
86
+
87
+ ## Copyright
88
+ Copyright 2012-2013 Roger Nesbitt, Powershop Limited, YouDo Limited. MIT licence.
data/bin/nitra CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
2
3
 
3
4
  require 'nitra'
4
5
 
@@ -7,5 +8,5 @@ Nitra::CommandLine.new(configuration, ARGV)
7
8
  if configuration.slave_mode
8
9
  Nitra::Slave::Server.new.run
9
10
  else
10
- exit Nitra::Client.new(configuration, ARGV).run ? 0 : 1
11
+ exit Nitra::Master.new(configuration, ARGV).run ? 0 : 1
11
12
  end
data/lib/nitra/channel.rb CHANGED
@@ -1,48 +1,50 @@
1
1
  require 'yaml'
2
2
 
3
- class Nitra::Channel
4
- ProtocolInvalidError = Class.new(StandardError)
3
+ module Nitra
4
+ class Channel
5
+ ProtocolInvalidError = Class.new(StandardError)
5
6
 
6
- attr_reader :rd, :wr
7
- attr_accessor :raise_epipe_on_write_error
7
+ attr_reader :rd, :wr
8
+ attr_accessor :raise_epipe_on_write_error
8
9
 
9
- def initialize(rd, wr)
10
- @rd = rd
11
- @wr = wr
12
- end
10
+ def initialize(rd, wr)
11
+ @rd = rd
12
+ @wr = wr
13
+ end
13
14
 
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
15
+ def self.pipe
16
+ c_rd, s_wr = IO.pipe
17
+ s_rd, c_wr = IO.pipe
18
+ [new(c_rd, c_wr), new(s_rd, s_wr)]
19
+ end
19
20
 
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}
21
+ def self.read_select(channels)
22
+ fds = IO.select(channels.collect(&:rd))
23
+ fds.first.collect do |fd|
24
+ channels.detect {|c| c.rd == fd}
25
+ end
24
26
  end
25
- end
26
27
 
27
- def close
28
- rd.close
29
- wr.close
30
- end
28
+ def close
29
+ rd.close
30
+ wr.close
31
+ end
31
32
 
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}"
33
+ def read
34
+ return unless line = rd.gets
35
+ if result = line.strip.match(/\ANITRA,(\d+)\z/)
36
+ data = rd.read(result[1].to_i)
37
+ YAML.load(data)
38
+ else
39
+ raise ProtocolInvalidError, "Expected nitra length line, got #{line.inspect}"
40
+ end
39
41
  end
40
- end
41
42
 
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
43
+ def write(data)
44
+ encoded = YAML.dump(data)
45
+ wr.write("NITRA,#{encoded.bytesize}\n#{encoded}")
46
+ rescue Errno::EPIPE
47
+ raise if raise_epipe_on_write_error
48
+ end
47
49
  end
48
50
  end
@@ -1,54 +1,74 @@
1
1
  require 'optparse'
2
2
 
3
- class Nitra::CommandLine
4
- attr_reader :configuration
3
+ module Nitra
4
+ class CommandLine
5
+ attr_reader :configuration
5
6
 
6
- def initialize(configuration, argv)
7
- @configuration = configuration
7
+ def initialize(configuration, argv)
8
+ @configuration = configuration
8
9
 
9
- OptionParser.new(argv) do |opts|
10
- opts.banner = "Usage: nitra [options] [spec_filename [...]]"
10
+ OptionParser.new do |opts|
11
+ opts.banner = "Usage: nitra [options] [spec_filename [...]]"
11
12
 
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
13
+ 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|
14
+ configuration.set_process_count n
15
+ end
15
16
 
16
- opts.on("-e", "--environment STRING", String, "The Rails environment to use, defaults to 'nitra'") do |environment|
17
- configuration.environment = environment
18
- end
17
+ opts.on("--cucumber", "Add full cucumber run, causes any files you list manually to be ignored") do
18
+ configuration.add_framework "cucumber"
19
+ end
19
20
 
20
- opts.on("--load", "Load schema into database before running specs") do
21
- configuration.load_schema = true
22
- end
21
+ opts.on("--debug", "Print debug output") do
22
+ configuration.debug = true
23
+ end
23
24
 
24
- opts.on("--migrate", "Migrate database before running specs") do
25
- configuration.migrate = true
26
- end
25
+ opts.on("-p", "--print-failures", "Print failures immediately when they occur") do
26
+ configuration.print_failures = true
27
+ end
27
28
 
28
- opts.on("-q", "--quiet", "Quiet; don't display progress bar") do
29
- configuration.quiet = true
30
- end
29
+ opts.on("-q", "--quiet", "Quiet; don't display progress bar") do
30
+ configuration.quiet = true
31
+ end
31
32
 
32
- opts.on("-p", "--print-failures", "Print failures immediately when they occur") do
33
- configuration.print_failures = true
34
- end
33
+ opts.on("--rake-after-runner task:1,task:2,task:3", Array, "The list of rake tasks to run, once per runner, in the runner's environment, just before the runner exits") do |rake_tasks|
34
+ configuration.add_rake_task(:after_runner, rake_tasks)
35
+ end
35
36
 
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
37
+ opts.on("--rake-before-runner task:1,task:2,task:3", Array, "The list of rake tasks to run, once per runner, in the runner's environment, after the runner starts") do |rake_tasks|
38
+ configuration.add_rake_task(:before_runner, rake_tasks)
39
+ end
39
40
 
40
- opts.on("--slave-mode", "Run in slave mode; ignores all other command-line options") do
41
- configuration.slave_mode = true
42
- end
41
+ opts.on("--rake-before-worker task:1,task:2,task:3", Array, "The list of rake tasks to run, once per worker, in the worker's environment, before the worker starts") do |rake_tasks|
42
+ configuration.add_rake_task(:before_worker, rake_tasks)
43
+ end
43
44
 
44
- opts.on("--debug", "Print debug output") do
45
- configuration.debug = true
46
- end
45
+ opts.on("-r", "--reset", "Reset database, equivalent to --rake-before-worker db:reset") do
46
+ configuration.add_rake_task(:before_worker, "db:reset")
47
+ end
47
48
 
48
- opts.on_tail("-h", "--help", "Show this message") do
49
- puts opts
50
- exit
51
- end
52
- end.parse!
49
+ opts.on("--slave-mode", "Run in slave mode; ignores all other command-line options") do
50
+ configuration.slave_mode = true
51
+ end
52
+
53
+ opts.on("--slave CONNECTION_COMMAND", String, "Provide a command that executes \"nitra --slave-mode\" on another host") do |connection_command|
54
+ configuration.add_slave connection_command
55
+ end
56
+
57
+ opts.on("--rspec", "Add full rspec run, causes any files you list manually to be ignored") do
58
+ configuration.add_framework "rspec"
59
+ end
60
+
61
+ opts.on("-e", "--environment ENV", String, "Set the RAILS_ENV to load") do |env|
62
+ configuration.environment = env
63
+ end
64
+
65
+ opts.on_tail("-h", "--help", "Show this message") do
66
+ puts opts
67
+ exit
68
+ end
69
+ end.parse!(argv)
70
+
71
+ configuration.set_default_framework
72
+ end
53
73
  end
54
74
  end
@@ -1,22 +1,44 @@
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
1
+ require 'nitra/utils'
10
2
 
11
- def calculate_default_process_count
12
- self.process_count ||= Nitra::Utils.processor_count
13
- end
3
+ module Nitra
4
+ class Configuration
5
+ attr_accessor :debug, :quiet, :print_failures, :rake_tasks
6
+ attr_accessor :process_count, :environment, :slaves, :slave_mode, :framework, :frameworks
7
+
8
+ def initialize
9
+ self.environment = "nitra"
10
+ self.slaves = []
11
+ self.rake_tasks = {}
12
+ self.frameworks = []
13
+ calculate_default_process_count
14
+ end
15
+
16
+ def add_framework(framework)
17
+ frameworks << framework
18
+ end
19
+
20
+ def add_rake_task(name, list)
21
+ rake_tasks[name] = list
22
+ end
23
+
24
+ def add_slave(command)
25
+ slaves << {:command => command, :cpus => nil}
26
+ end
27
+
28
+ def set_default_framework
29
+ self.framework = frameworks.first if frameworks.any?
30
+ end
31
+
32
+ def calculate_default_process_count
33
+ self.process_count ||= Nitra::Utils.processor_count
34
+ end
14
35
 
15
- def set_process_count(n)
16
- if slaves.empty?
17
- self.process_count = n
18
- else
19
- slaves.last[:cpus] = n
36
+ def set_process_count(n)
37
+ if slaves.empty?
38
+ self.process_count = n
39
+ else
40
+ slaves.last[:cpus] = n
41
+ end
20
42
  end
21
43
  end
22
44
  end
@@ -0,0 +1,32 @@
1
+ require 'cucumber/rb_support/rb_language'
2
+ require 'cucumber/runtime'
3
+ module Cucumber
4
+ module RbSupport
5
+ class RbLanguage
6
+ # Reloading support files is bad for us. Idealy we'd subclass but since
7
+ # Cucumber's internals are a bit shit and insists on using the new keyword
8
+ # everywhere we have to monkeypatch it out or spend another 6 months
9
+ # rewriting it and getting patches accepted...
10
+ def load_code_file(code_file)
11
+ require File.expand_path(code_file)
12
+ end
13
+ end
14
+ end
15
+ class ResetableRuntime < Runtime
16
+ # Cucumber lacks a reset hook like the one Rspec has so we need to patch one in...
17
+ # Call this after configure so that the correct configuration is used to create the result set.
18
+ def reset
19
+ @results = Results.new(nil)
20
+ end
21
+
22
+ # Cucumber > 1.1.0 memoizes @loader which means we can't load in new files.
23
+ # Patch it back to how it used to work.
24
+ def features
25
+ @loader = Runtime::FeaturesLoader.new(
26
+ @configuration.feature_files,
27
+ @configuration.filters,
28
+ @configuration.tag_expression)
29
+ @loader.features
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,59 @@
1
+ ##
2
+ # Nitra::Formatter print out data regarding the run.
3
+ #
4
+ module Nitra
5
+ class Formatter
6
+ attr_accessor :start_time, :progress, :configuration
7
+
8
+ def initialize(progress, configuration)
9
+ self.progress = progress
10
+ self.configuration = configuration
11
+ end
12
+
13
+ def start
14
+ self.start_time = Time.now
15
+ print_progress
16
+ end
17
+
18
+ def print_progress
19
+ print_failures
20
+ print_bar
21
+ end
22
+
23
+ def finish
24
+ puts progress.filtered_output
25
+
26
+ puts "\n#{overview}"
27
+ puts "#{$aborted ? "Aborted after" : "Finished in"} #{"%0.1f" % (Time.now-start_time)} seconds"
28
+ end
29
+
30
+ private
31
+
32
+ ##
33
+ # Print the progress bar, doesn't do anything if we're in debug.
34
+ #
35
+ def print_bar
36
+ return if configuration.quiet || configuration.debug || progress.file_count == 0
37
+ total = 50
38
+ completed = (progress.files_completed / progress.file_count.to_f * total).to_i
39
+ print "\r[#{"X" * completed}#{"." * (total - completed)}] #{overview}\r"
40
+ $stdout.flush
41
+ end
42
+
43
+ ##
44
+ # Prints the output in the progress object and resets it if we've got eager printing turned on.
45
+ #
46
+ def print_failures
47
+ return unless progress.output.length > 0 && configuration.print_failures
48
+ puts progress.filtered_output
49
+ progress.output = ""
50
+ end
51
+
52
+ ##
53
+ # A simple overview of files processed so far and success/failure numbers.
54
+ #
55
+ def overview
56
+ "#{progress.files_completed}/#{progress.file_count} files | #{progress.example_count} examples, #{progress.failure_count} failures"
57
+ end
58
+ end
59
+ end
data/lib/nitra/master.rb CHANGED
@@ -1,18 +1,24 @@
1
1
  class Nitra::Master
2
- attr_reader :configuration, :files
2
+ attr_reader :configuration, :files, :frameworks, :current_framework
3
3
 
4
4
  def initialize(configuration, files = nil)
5
5
  @configuration = configuration
6
- @files = files
6
+ @frameworks = configuration.frameworks
7
+ if @frameworks.any?
8
+ load_files_from_framework_list
9
+ else
10
+ map_files_to_frameworks(files)
11
+ end
12
+ @current_framework = @frameworks.shift
13
+ @configuration.framework = @current_framework
7
14
  end
8
15
 
9
16
  def run
10
- @files = Dir["spec/**/*_spec.rb"] if files.nil? || files.empty?
11
- return if files.empty?
17
+ return if files_remaining == 0
12
18
 
13
19
  progress = Nitra::Progress.new
14
- progress.file_count = @files.length
15
- yield progress, nil
20
+ progress.file_count = files_remaining
21
+ formatter = Nitra::Formatter.new(progress, configuration)
16
22
 
17
23
  runners = []
18
24
 
@@ -27,24 +33,40 @@ class Nitra::Master
27
33
  end
28
34
 
29
35
  slave = Nitra::Slave::Client.new(configuration)
30
- runners += slave.connect("")
36
+ runners += slave.connect
37
+
38
+ formatter.start
31
39
 
32
40
  while runners.length > 0
33
41
  Nitra::Channel.read_select(runners).each do |channel|
34
42
  if data = channel.read
35
43
  case data["command"]
36
44
  when "next"
37
- channel.write "filename" => files.shift
45
+ if files_remaining == 0
46
+ channel.write "command" => "drain"
47
+ elsif data["framework"] == current_framework
48
+ channel.write "command" => "file", "filename" => next_file
49
+ else
50
+ channel.write "command" => "framework", "framework" => current_framework
51
+ end
52
+
38
53
  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
54
+ examples = data["example_count"] || 0
55
+ failures = data["failure_count"] || 0
56
+ failure = data["return_code"].to_i != 0
57
+ progress.file_progress(examples, failures, failure, data["text"])
58
+ formatter.print_progress
59
+
60
+ when "error"
61
+ progress.fail("ERROR " + data["process"] + " " + data["text"])
62
+ formatter.progress
63
+ runners.delete channel
64
+
44
65
  when "debug"
45
66
  if configuration.debug
46
67
  puts "[DEBUG] #{data["text"]}"
47
68
  end
69
+
48
70
  when "stdout"
49
71
  if configuration.debug
50
72
  puts "STDOUT for #{data["process"]} #{data["filename"]}:\n#{data["text"]}" unless data["text"].empty?
@@ -58,11 +80,44 @@ class Nitra::Master
58
80
 
59
81
  debug "waiting for all children to exit..."
60
82
  Process.waitall
61
- progress
83
+
84
+ formatter.finish
85
+
86
+ !$aborted && progress.files_completed == progress.file_count && progress.failure_count.zero? && !progress.failure
62
87
  end
63
88
 
64
89
  protected
65
90
  def debug(*text)
66
91
  puts "master: #{text.join}" if configuration.debug
67
92
  end
93
+
94
+ def map_files_to_frameworks(files)
95
+ @files = files.group_by do |filename|
96
+ framework_name, framework_class = Nitra::Workers::Worker.worker_classes.find {|framework_name, framework_class| framework_class.filename_match?(filename)}
97
+ framework_name
98
+ end
99
+ @frameworks = @files.keys
100
+ end
101
+
102
+ def load_files_from_framework_list
103
+ @files = frameworks.inject({}) do |result, framework_name|
104
+ result[framework_name] = Nitra::Workers::Worker.worker_classes[framework_name].files
105
+ result
106
+ end
107
+ end
108
+
109
+ def files_remaining
110
+ files.values.inject(0) {|sum, filenames| sum + filenames.length}
111
+ end
112
+
113
+ def current_framework_files
114
+ files[current_framework]
115
+ end
116
+
117
+ def next_file
118
+ raise if files_remaining == 0
119
+ file = current_framework_files.shift
120
+ @current_framework = frameworks.shift if current_framework_files.length == 0
121
+ file
122
+ end
68
123
  end
@@ -1,8 +1,26 @@
1
1
  class Nitra::Progress
2
- attr_accessor :file_count, :files_completed, :example_count, :failure_count, :output
2
+ attr_accessor :file_count, :files_completed, :example_count, :failure_count, :output, :failure
3
3
 
4
4
  def initialize
5
5
  @file_count = @files_completed = @example_count = @failure_count = 0
6
6
  @output = ""
7
+ @failure = false
8
+ end
9
+
10
+ def file_progress(examples, failures, failure, text)
11
+ self.files_completed += 1
12
+ self.example_count += examples
13
+ self.failure_count += failures
14
+ self.failure ||= failure
15
+ self.output.concat text
16
+ end
17
+
18
+ def fail(message)
19
+ self.failure = true
20
+ self.output.concat message
21
+ end
22
+
23
+ def filtered_output
24
+ output.gsub(/\n\n\n+/, "\n\n")
7
25
  end
8
26
  end
@@ -0,0 +1,22 @@
1
+ module Nitra
2
+ class RailsTooling
3
+ ##
4
+ # Find the database config for the current TEST_ENV_NUMBER and manually initialise a connection.
5
+ #
6
+ def self.connect_to_database
7
+ return unless defined?(Rails)
8
+ # Config files are read at load time. Since we load rails in one env then
9
+ # change some flags to get different environments through forking we need
10
+ # always reload our database config...
11
+ ActiveRecord::Base.configurations = YAML.load(ERB.new(IO.read("#{Rails.root}/config/database.yml")).result)
12
+
13
+ ActiveRecord::Base.clear_all_connections!
14
+ ActiveRecord::Base.establish_connection
15
+ end
16
+
17
+ def self.reset_cache
18
+ return unless defined?(Rails)
19
+ Rails.cache.reset if Rails.cache.respond_to?(:reset)
20
+ end
21
+ end
22
+ end