friendlyfashion-parallel_tests 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/.gitignore +2 -0
  2. data/Gemfile +8 -0
  3. data/Gemfile.lock +44 -0
  4. data/Rakefile +6 -0
  5. data/Readme.md +232 -0
  6. data/ReadmeRails2.md +48 -0
  7. data/bin/parallel_cucumber +2 -0
  8. data/bin/parallel_rspec +2 -0
  9. data/bin/parallel_test +6 -0
  10. data/lib/parallel_tests.rb +30 -0
  11. data/lib/parallel_tests/cli.rb +159 -0
  12. data/lib/parallel_tests/cucumber/gherkin_listener.rb +60 -0
  13. data/lib/parallel_tests/cucumber/runner.rb +90 -0
  14. data/lib/parallel_tests/cucumber/runtime_logger.rb +58 -0
  15. data/lib/parallel_tests/grouper.rb +53 -0
  16. data/lib/parallel_tests/railtie.rb +8 -0
  17. data/lib/parallel_tests/rspec/failures_logger.rb +44 -0
  18. data/lib/parallel_tests/rspec/logger_base.rb +52 -0
  19. data/lib/parallel_tests/rspec/runner.rb +59 -0
  20. data/lib/parallel_tests/rspec/runtime_logger.rb +34 -0
  21. data/lib/parallel_tests/rspec/summary_logger.rb +19 -0
  22. data/lib/parallel_tests/tasks.rb +134 -0
  23. data/lib/parallel_tests/test/runner.rb +134 -0
  24. data/lib/parallel_tests/test/runtime_logger.rb +92 -0
  25. data/lib/parallel_tests/version.rb +3 -0
  26. data/parallel_tests.gemspec +14 -0
  27. data/spec/integration_spec.rb +244 -0
  28. data/spec/parallel_tests/cli_spec.rb +36 -0
  29. data/spec/parallel_tests/cucumber/gherkin_listener_spec.rb +48 -0
  30. data/spec/parallel_tests/cucumber/runner_spec.rb +173 -0
  31. data/spec/parallel_tests/grouper_spec.rb +52 -0
  32. data/spec/parallel_tests/rspec/failure_logger_spec.rb +82 -0
  33. data/spec/parallel_tests/rspec/runner_spec.rb +178 -0
  34. data/spec/parallel_tests/rspec/runtime_logger_spec.rb +76 -0
  35. data/spec/parallel_tests/rspec/summary_logger_spec.rb +37 -0
  36. data/spec/parallel_tests/tasks_spec.rb +151 -0
  37. data/spec/parallel_tests/test/runner_spec.rb +273 -0
  38. data/spec/parallel_tests/test/runtime_logger_spec.rb +84 -0
  39. data/spec/parallel_tests_spec.rb +73 -0
  40. data/spec/spec_helper.rb +151 -0
  41. metadata +109 -0
@@ -0,0 +1,19 @@
1
+ require 'parallel_tests/rspec/failures_logger'
2
+
3
+ class ParallelTests::RSpec::SummaryLogger < ParallelTests::RSpec::LoggerBase
4
+ # RSpec 1: dumps 1 failed spec
5
+ def dump_failure(*args)
6
+ lock_output do
7
+ super
8
+ end
9
+ @output.flush
10
+ end
11
+
12
+ # RSpec 2: dumps all failed specs
13
+ def dump_failures(*args)
14
+ lock_output do
15
+ super
16
+ end
17
+ @output.flush
18
+ end
19
+ end
@@ -0,0 +1,134 @@
1
+ require 'rake'
2
+
3
+ module ParallelTests
4
+ module Tasks
5
+ class << self
6
+ def rails_env
7
+ ENV['RAILS_ENV'] || 'test'
8
+ end
9
+
10
+ def run_in_parallel(cmd, options={})
11
+ count = " -n #{options[:count]}" if options[:count]
12
+ executable = File.expand_path("../../../bin/parallel_test", __FILE__)
13
+ command = "#{executable} --exec '#{cmd}'#{count}#{' --non-parallel' if options[:non_parallel]}"
14
+ command << " --advance-number #{options[:advance_number]}" if options[:advance_number]
15
+ abort unless system(command)
16
+ end
17
+
18
+ # this is a crazy-complex solution for a very simple problem:
19
+ # removing certain lines from the output without chaning the exit-status
20
+ # normally I'd not do this, but it has been lots of fun and a great learning experience :)
21
+ #
22
+ # - sed does not support | without -r
23
+ # - grep changes 0 exitstatus to 1 if nothing matches
24
+ # - sed changes 1 exitstatus to 0
25
+ # - pipefail makes pipe fail with exitstatus of first failed command
26
+ # - pipefail is not supported in (zsh)
27
+ # - defining a new rake task like silence_schema would force users to load parallel_tests in test env
28
+ # - do not use ' since run_in_parallel uses them to quote stuff
29
+ # - simple system "set -o pipefail" returns nil even though set -o pipefail exists with 0
30
+ def suppress_output(command, ignore_regex)
31
+ activate_pipefail = "set -o pipefail"
32
+ remove_ignored_lines = %Q{(grep -v "#{ignore_regex}" || test 1)}
33
+
34
+ if system("#{activate_pipefail} && test 1")
35
+ "#{activate_pipefail} && (#{command}) | #{remove_ignored_lines}"
36
+ else
37
+ command
38
+ end
39
+ end
40
+
41
+ def check_for_pending_migrations
42
+ abort_migrations = "db:abort_if_pending_migrations"
43
+ if Rake::Task.task_defined?(abort_migrations)
44
+ Rake::Task[abort_migrations].invoke
45
+ end
46
+ end
47
+
48
+ # parallel:spec[:count, :pattern, :options]
49
+ def parse_args(args)
50
+ # order as given by user
51
+ args = [args[:count], args[:pattern], args[:options]]
52
+
53
+ # count given or empty ?
54
+ # parallel:spec[2,models,options]
55
+ # parallel:spec[,models,options]
56
+ count = args.shift if args.first.to_s =~ /^\d*$/
57
+ num_processes = count.to_i unless count.to_s.empty?
58
+ pattern = args.shift
59
+ options = args.shift
60
+
61
+ [num_processes, pattern.to_s, options.to_s]
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ namespace :parallel do
68
+ desc "create test databases via db:create --> parallel:create[num_cpus]"
69
+ task :create, :count do |t,args|
70
+ ParallelTests::Tasks.run_in_parallel("rake db:create RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
71
+ end
72
+
73
+ desc "drop test databases via db:drop --> parallel:drop[num_cpus]"
74
+ task :drop, :count do |t,args|
75
+ ParallelTests::Tasks.run_in_parallel("rake db:drop RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
76
+ end
77
+
78
+ desc "update test databases by dumping and loading --> parallel:prepare[num_cpus]"
79
+ task(:prepare, [:count]) do |t,args|
80
+ ParallelTests::Tasks.check_for_pending_migrations
81
+ if defined?(ActiveRecord) && ActiveRecord::Base.schema_format == :ruby
82
+ # dump then load in parallel
83
+ Rake::Task['db:schema:dump'].invoke
84
+ Rake::Task['parallel:load_schema'].invoke(args[:count])
85
+ else
86
+ # there is no separate dump / load for schema_format :sql -> do it safe and slow
87
+ args = args.to_hash.merge(:non_parallel => true) # normal merge returns nil
88
+ ParallelTests::Tasks.run_in_parallel('rake db:test:prepare --trace', args)
89
+ end
90
+ end
91
+
92
+ # when dumping/resetting takes too long
93
+ desc "update test databases via db:migrate --> parallel:migrate[num_cpus]"
94
+ task :migrate, :count do |t,args|
95
+ ParallelTests::Tasks.run_in_parallel("rake db:migrate RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
96
+ end
97
+
98
+ # just load the schema (good for integration server <-> no development db)
99
+ desc "load dumped schema for test databases via db:schema:load --> parallel:load_schema[num_cpus]"
100
+ task :load_schema, :count do |t,args|
101
+ command = "rake db:schema:load RAILS_ENV=#{ParallelTests::Tasks.rails_env}"
102
+ ParallelTests::Tasks.run_in_parallel(ParallelTests::Tasks.suppress_output(command, "^ ->\\|^-- "), args)
103
+ end
104
+
105
+ desc "load the seed data from db/seeds.rb via db:seed --> parallel:seed[num_cpus]"
106
+ task :seed, :count do |t,args|
107
+ ParallelTests::Tasks.run_in_parallel("rake db:seed RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
108
+ end
109
+
110
+ ['test', 'spec', 'features'].each do |type|
111
+ desc "run #{type} in parallel with parallel:#{type}[num_cpus]"
112
+ task type, [:count, :pattern, :options] do |t, args|
113
+ ParallelTests::Tasks.check_for_pending_migrations
114
+
115
+ $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), '..'))
116
+ require "parallel_tests"
117
+
118
+ count, pattern, options = ParallelTests::Tasks.parse_args(args)
119
+ test_framework = {
120
+ 'spec' => 'rspec',
121
+ 'test' => 'test',
122
+ 'features' => 'cucumber'
123
+ }[type]
124
+
125
+ executable = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'parallel_test')
126
+ command = "#{executable} #{type} --type #{test_framework} " \
127
+ "-n #{count} " \
128
+ "--pattern '#{pattern}' " \
129
+ "--test-options '#{options}'"
130
+
131
+ abort unless system(command) # allow to chain tasks e.g. rake parallel:spec parallel:features
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,134 @@
1
+ module ParallelTests
2
+ module Test
3
+ class Runner
4
+ # --- usually overwritten by other runners
5
+
6
+ def self.runtime_log
7
+ 'tmp/parallel_runtime_test.log'
8
+ end
9
+
10
+ def self.test_suffix
11
+ "_test.rb"
12
+ end
13
+
14
+ def self.test_file_name
15
+ "test"
16
+ end
17
+
18
+ def self.run_tests(test_files, process_number, options)
19
+ require_list = test_files.map { |filename| %{"#{File.expand_path filename}"} }.join(",")
20
+ cmd = "ruby -Itest -e '[#{require_list}].each {|f| require f }' -- #{options[:test_options]}"
21
+ execute_command(cmd, process_number, options)
22
+ end
23
+
24
+ def self.line_is_result?(line)
25
+ line =~ /\d+ failure/
26
+ end
27
+
28
+ # --- usually used by other runners
29
+
30
+ # finds all tests and partitions them into groups
31
+ def self.tests_in_groups(tests, num_groups, options={})
32
+ tests = find_tests(tests, options)
33
+
34
+ tests = if options[:group_by] == :found
35
+ tests.map { |t| [t, 1] }
36
+ else
37
+ with_runtime_info(tests)
38
+ end
39
+ Grouper.in_even_groups_by_size(tests, num_groups, options)
40
+ end
41
+
42
+ def self.execute_command(cmd, process_number, options)
43
+ cmd = "TEST_ENV_NUMBER=#{test_env_number(process_number, options)} ; export TEST_ENV_NUMBER; #{cmd}"
44
+ f = open("|#{cmd}", 'r')
45
+ output = fetch_output(f)
46
+ f.close
47
+ {:stdout => output, :exit_status => $?.exitstatus}
48
+ end
49
+
50
+ def self.find_results(test_output)
51
+ test_output.split("\n").map {|line|
52
+ line = line.gsub(/\.|F|\*/,'').gsub(/\e\[\d+m/,'')
53
+ next unless line_is_result?(line)
54
+ line
55
+ }.compact
56
+ end
57
+
58
+ def self.test_env_number(process_number, options)
59
+ n = options[:advance_number].to_i + process_number + 1
60
+ n == 0 ? '' : n
61
+ end
62
+
63
+ def self.summarize_results(results)
64
+ sums = sum_up_results(results)
65
+ sums.sort.map{|word, number| "#{number} #{word}#{'s' if number != 1}" }.join(', ')
66
+ end
67
+
68
+ protected
69
+
70
+ def self.sum_up_results(results)
71
+ results = results.join(' ').gsub(/s\b/,'') # combine and singularize results
72
+ counts = results.scan(/(\d+) (\w+)/)
73
+ sums = counts.inject(Hash.new(0)) do |sum, (number, word)|
74
+ sum[word] += number.to_i
75
+ sum
76
+ end
77
+
78
+ sums
79
+ end
80
+
81
+ # read output of the process and print it in chunks
82
+ def self.fetch_output(process)
83
+ all = ''
84
+ while buffer = process.readpartial(1000000)
85
+ all << buffer
86
+ $stdout.print buffer
87
+ $stdout.flush
88
+ end rescue EOFError
89
+
90
+ all
91
+ end
92
+
93
+ def self.with_runtime_info(tests)
94
+ lines = File.read(runtime_log).split("\n") rescue []
95
+
96
+ # use recorded test runtime if we got enough data
97
+ if lines.size * 1.5 > tests.size
98
+ puts "Using recorded test runtime"
99
+ times = Hash.new(1)
100
+ lines.each do |line|
101
+ test, time = line.split(":")
102
+ next unless test and time
103
+ times[File.expand_path(test)] = time.to_f
104
+ end
105
+ tests.sort.map{|test| [test, times[File.expand_path(test)]] }
106
+ else # use file sizes
107
+ tests.sort.map{|test| [test, File.stat(test).size] }
108
+ end
109
+ end
110
+
111
+ def self.find_tests(tests, options = {})
112
+ (tests || []).map do |file_or_folder|
113
+ if File.directory?(file_or_folder)
114
+ files = files_in_folder(file_or_folder, options)
115
+ files.grep(/#{Regexp.escape test_suffix}$/).grep(options[:pattern]||//)
116
+ else
117
+ file_or_folder
118
+ end
119
+ end.flatten.uniq
120
+ end
121
+
122
+ def self.files_in_folder(folder, options={})
123
+ pattern = if options[:symlinks] == false # not nil or true
124
+ "**/*"
125
+ else
126
+ # follow one symlink and direct children
127
+ # http://stackoverflow.com/questions/357754/can-i-traverse-symlinked-directories-in-ruby-with-a-glob
128
+ "**{,/*/**}/*"
129
+ end
130
+ Dir[File.join(folder, pattern)].uniq
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,92 @@
1
+ require 'parallel_tests/test/runner'
2
+
3
+ module ParallelTests
4
+ module Test
5
+ class RuntimeLogger
6
+ @@has_started = false
7
+
8
+ def self.log(test, start_time, end_time)
9
+ return if test.is_a? ::Test::Unit::TestSuite # don't log for suites-of-suites
10
+
11
+ if !@@has_started # make empty log file
12
+ File.open(logfile, 'w'){}
13
+ @@has_started = true
14
+ end
15
+
16
+ locked_appending_to(logfile) do |file|
17
+ file.puts(message(test, start_time, end_time))
18
+ end
19
+ end
20
+
21
+ def self.message(test, start_time, end_time)
22
+ delta = "%.2f" % (end_time.to_f-start_time.to_f)
23
+ filename = class_directory(test.class) + class_to_filename(test.class) + ".rb"
24
+ "#{filename}:#{delta}"
25
+ end
26
+
27
+ # Note: this is a best guess at conventional test directory structure, and may need
28
+ # tweaking / post-processing to match correctly for any given project
29
+ def self.class_directory(suspect)
30
+ result = "test/"
31
+
32
+ if defined?(Rails)
33
+ result += case suspect.superclass.name
34
+ when "ActionDispatch::IntegrationTest"
35
+ "integration/"
36
+ when "ActionDispatch::PerformanceTest"
37
+ "performance/"
38
+ when "ActionController::TestCase"
39
+ "functional/"
40
+ when "ActionView::TestCase"
41
+ "unit/helpers/"
42
+ else
43
+ "unit/"
44
+ end
45
+ end
46
+ result
47
+ end
48
+
49
+ # based on https://github.com/grosser/single_test/blob/master/lib/single_test.rb#L117
50
+ def self.class_to_filename(suspect)
51
+ word = suspect.to_s.dup
52
+ return word unless word.match /^[A-Z]/ and not word.match %r{/[a-z]}
53
+
54
+ word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
55
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
56
+ word.gsub!(/\:\:/, '/')
57
+ word.tr!("-", "_")
58
+ word.downcase!
59
+ word
60
+ end
61
+
62
+ def self.locked_appending_to(file)
63
+ File.open(file, 'a') do |f|
64
+ begin
65
+ f.flock File::LOCK_EX
66
+ yield f
67
+ ensure
68
+ f.flock File::LOCK_UN
69
+ end
70
+ end
71
+ end
72
+
73
+ def self.logfile
74
+ ParallelTests::Test::Runner.runtime_log
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ require 'test/unit/testsuite'
81
+ class ::Test::Unit::TestSuite
82
+ alias :run_without_timing :run unless defined? @@timing_installed
83
+
84
+ def run(result, &progress_block)
85
+ start_time=Time.now
86
+ run_without_timing(result, &progress_block)
87
+ end_time=Time.now
88
+ ParallelTests::Test::RuntimeLogger.log(self.tests.first, start_time, end_time)
89
+ end
90
+
91
+ @@timing_installed = true
92
+ end
@@ -0,0 +1,3 @@
1
+ module ParallelTests
2
+ VERSION = Version = '0.9.0'
3
+ end
@@ -0,0 +1,14 @@
1
+ $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
2
+ name = "friendlyfashion-parallel_tests"
3
+ require "parallel_tests/version"
4
+
5
+ Gem::Specification.new name, ParallelTests::VERSION do |s|
6
+ s.summary = "Run Test::Unit / RSpec / Cucumber in parallel"
7
+ s.authors = ["Laurynas Butkus", "Tomas Varaneckas", "Justas Janauskas"]
8
+ s.email = ["laurynas.butkus@gmail.com", "tomas.varaneckas@gmail.com", "jjanauskas@gmail.com"]
9
+ s.homepage = "http://github.com/friendlyfashion/parallel_tests"
10
+ s.files = `git ls-files`.split("\n")
11
+ s.license = "MIT"
12
+ s.executables = ["parallel_cucumber", "parallel_rspec", "parallel_test"]
13
+ s.add_runtime_dependency "parallel"
14
+ end
@@ -0,0 +1,244 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'CLI' do
4
+ before do
5
+ `rm -rf #{folder}`
6
+ end
7
+
8
+ after do
9
+ `rm -rf #{folder}`
10
+ end
11
+
12
+ def folder
13
+ "/tmp/parallel_tests_tests"
14
+ end
15
+
16
+ def write(file, content)
17
+ path = "#{folder}/#{file}"
18
+ ensure_folder File.dirname(path)
19
+ File.open(path, 'w'){|f| f.write content }
20
+ path
21
+ end
22
+
23
+ def read(file)
24
+ File.read "#{folder}/#{file}"
25
+ end
26
+
27
+ def bin_folder
28
+ "#{File.expand_path(File.dirname(__FILE__))}/../bin"
29
+ end
30
+
31
+ def executable(options={})
32
+ "#{bin_folder}/parallel_#{options[:type] || 'test'}"
33
+ end
34
+
35
+ def ensure_folder(folder)
36
+ `mkdir -p #{folder}` unless File.exist?(folder)
37
+ end
38
+
39
+ def run_tests(test_folder, options={})
40
+ ensure_folder folder
41
+ processes = "-n #{options[:processes]||2}" unless options[:processes] == false
42
+ command = "cd #{folder} && #{options[:export]} #{executable(options)} #{test_folder} #{processes} #{options[:add]} 2>&1"
43
+ result = `#{command}`
44
+ raise "FAILED #{command}\n#{result}" if $?.success? == !!options[:fail]
45
+ result
46
+ end
47
+
48
+ it "runs tests in parallel" do
49
+ write 'spec/xxx_spec.rb', 'describe("it"){it("should"){puts "TEST1"}}'
50
+ write 'spec/xxx2_spec.rb', 'describe("it"){it("should"){puts "TEST2"}}'
51
+ result = run_tests "spec", :type => 'rspec'
52
+
53
+ # test ran and gave their puts
54
+ result.should include('TEST1')
55
+ result.should include('TEST2')
56
+
57
+ # all results present
58
+ result.scan('1 example, 0 failure').size.should == 2 # 2 results
59
+ result.scan('2 examples, 0 failures').size.should == 1 # 1 summary
60
+ result.scan(/Finished in \d+\.\d+ seconds/).size.should == 2
61
+ result.scan(/Took \d+\.\d+ seconds/).size.should == 1 # parallel summary
62
+ end
63
+
64
+ it "does not run any tests if there are none" do
65
+ write 'spec/xxx_spec.rb', '1'
66
+ result = run_tests "spec", :type => 'rspec'
67
+ result.should include('No examples found')
68
+ result.should include('Took')
69
+ end
70
+
71
+ it "fails when tests fail" do
72
+ write 'spec/xxx_spec.rb', 'describe("it"){it("should"){puts "TEST1"}}'
73
+ write 'spec/xxx2_spec.rb', 'describe("it"){it("should"){1.should == 2}}'
74
+ result = run_tests "spec", :fail => true, :type => 'rspec'
75
+
76
+ result.scan('1 example, 1 failure').size.should == 1
77
+ result.scan('1 example, 0 failure').size.should == 1
78
+ result.scan('2 examples, 1 failure').size.should == 1
79
+ end
80
+
81
+ context "with given commands" do
82
+ it "can exec given commands with ENV['TEST_ENV_NUM']" do
83
+ result = `#{executable} -e 'ruby -e "print ENV[:TEST_ENV_NUMBER.to_s].to_i"' -n 4`
84
+ result.gsub('"','').split('').sort.should == %w[0 2 3 4]
85
+ end
86
+
87
+ it "can exec given command non-parallel" do
88
+ result = `#{executable} -e 'ruby -e "sleep(rand(10)/100.0); puts ENV[:TEST_ENV_NUMBER.to_s].inspect"' -n 4 --non-parallel`
89
+ result.split("\n").should == %w["" "2" "3" "4"]
90
+ end
91
+
92
+ it "exists with success if all sub-processes returned success" do
93
+ system("#{executable} -e 'cat /dev/null' -n 4").should == true
94
+ end
95
+
96
+ it "exists with failure if any sub-processes returned failure" do
97
+ system("#{executable} -e 'test -e xxxx' -n 4").should == false
98
+ end
99
+ end
100
+
101
+ it "runs through parallel_rspec" do
102
+ version = `#{executable} -v`
103
+ `#{bin_folder}/parallel_rspec -v`.should == version
104
+ end
105
+
106
+ it "runs through parallel_cucumber" do
107
+ version = `#{executable} -v`
108
+ `#{bin_folder}/parallel_cucumber -v`.should == version
109
+ end
110
+
111
+ it "runs with --group-by found" do
112
+ # it only tests that it does not blow up, as it did before fixing...
113
+ write "spec/x1_spec.rb", "puts '111'"
114
+ run_tests "spec", :type => 'rspec', :add => '--group-by found'
115
+ end
116
+
117
+ it "runs faster with more processes" do
118
+ 2.times{|i|
119
+ write "spec/xxx#{i}_spec.rb", 'describe("it"){it("should"){sleep 5}}; $stderr.puts ENV["TEST_ENV_NUMBER"]'
120
+ }
121
+ t = Time.now
122
+ run_tests("spec", :processes => 2, :type => 'rspec')
123
+ expected = 10
124
+ (Time.now - t).should <= expected
125
+ end
126
+
127
+ it "can run with given files" do
128
+ write "spec/x1_spec.rb", "puts '111'"
129
+ write "spec/x2_spec.rb", "puts '222'"
130
+ write "spec/x3_spec.rb", "puts '333'"
131
+ result = run_tests "spec/x1_spec.rb spec/x3_spec.rb", :type => 'rspec'
132
+ result.should include('111')
133
+ result.should include('333')
134
+ result.should_not include('222')
135
+ end
136
+
137
+ it "runs successfully without any files" do
138
+ results = run_tests "", :type => 'rspec'
139
+ results.should include("2 processes for 0 specs")
140
+ results.should include("Took")
141
+ end
142
+
143
+ it "can run with test-options" do
144
+ write "spec/x1_spec.rb", "111"
145
+ write "spec/x2_spec.rb", "111"
146
+ result = run_tests "spec",
147
+ :add => "--test-options ' --version'",
148
+ :processes => 2,
149
+ :type => 'rspec'
150
+ result.should =~ /\d+\.\d+\.\d+.*\d+\.\d+\.\d+/m # prints version twice
151
+ end
152
+
153
+ it "runs with PARALLEL_TEST_PROCESSORS processes" do
154
+ processes = 5
155
+ processes.times{|i|
156
+ write "spec/x#{i}_spec.rb", "puts %{ENV-\#{ENV['TEST_ENV_NUMBER']}-}"
157
+ }
158
+ result = run_tests "spec",
159
+ :export => "PARALLEL_TEST_PROCESSORS=#{processes}",
160
+ :processes => processes,
161
+ :type => 'rspec'
162
+ result.scan(/ENV-.?-/).should =~ ["ENV--", "ENV-2-", "ENV-3-", "ENV-4-", "ENV-5-"]
163
+ end
164
+
165
+ it "filters test by given pattern and relative paths" do
166
+ write "spec/x_spec.rb", "puts 'XXX'"
167
+ write "spec/y_spec.rb", "puts 'YYY'"
168
+ write "spec/z_spec.rb", "puts 'ZZZ'"
169
+ result = run_tests "spec", :add => "-p '^spec/(x|z)'", :type => "rspec"
170
+ result.should include('XXX')
171
+ result.should_not include('YYY')
172
+ result.should include('ZZZ')
173
+ end
174
+
175
+ context "Test::Unit" do
176
+ it "runs" do
177
+ write "test/x1_test.rb", "require 'test/unit'; class XTest < Test::Unit::TestCase; def test_xxx; end; end"
178
+ result = run_tests("test")
179
+ result.should include('1 test')
180
+ end
181
+
182
+ it "passes test options" do
183
+ write "test/x1_test.rb", "require 'test/unit'; class XTest < Test::Unit::TestCase; def test_xxx; end; end"
184
+ result = run_tests("test", :add => '--test-options "-v"')
185
+ result.should include('test_xxx') # verbose output of every test
186
+ end
187
+
188
+ it "runs successfully without any files" do
189
+ results = run_tests("")
190
+ results.should include("2 processes for 0 tests")
191
+ results.should include("Took")
192
+ end
193
+ end
194
+
195
+ context "Cucumber" do
196
+ before do
197
+ write "features/steps/a.rb", "
198
+ Given('I print TEST_ENV_NUMBER'){ puts \"YOUR TEST ENV IS \#{ENV['TEST_ENV_NUMBER']}!\" }
199
+ And('I sleep a bit'){ sleep 0.2 }
200
+ "
201
+ end
202
+
203
+ it "passes TEST_ENV_NUMBER when running with pattern (issue #86)" do
204
+ write "features/good1.feature", "Feature: xxx\n Scenario: xxx\n Given I print TEST_ENV_NUMBER"
205
+ write "features/good2.feature", "Feature: xxx\n Scenario: xxx\n Given I print TEST_ENV_NUMBER"
206
+ write "features/b.feature", "Feature: xxx\n Scenario: xxx\n Given I FAIL"
207
+ write "features/steps/a.rb", "Given('I print TEST_ENV_NUMBER'){ puts \"YOUR TEST ENV IS \#{ENV['TEST_ENV_NUMBER']}!\" }"
208
+
209
+ result = run_tests "features", :type => "cucumber", :add => '--pattern good'
210
+
211
+ result.should include('YOUR TEST ENV IS 2!')
212
+ result.should include('YOUR TEST ENV IS !')
213
+ result.should_not include('I FAIL')
214
+ end
215
+
216
+ it "writes a runtime log" do
217
+ log = "tmp/parallel_runtime_cucumber.log"
218
+ write(log, "x")
219
+ 2.times{|i|
220
+ # needs sleep so that runtime loggers dont overwrite each other initially
221
+ write "features/good#{i}.feature", "Feature: xxx\n Scenario: xxx\n Given I print TEST_ENV_NUMBER\n And I sleep a bit"
222
+ }
223
+ run_tests "features", :type => "cucumber"
224
+ read(log).gsub(/\.\d+/,'').split("\n").should =~ [
225
+ "features/good0.feature:0",
226
+ "features/good1.feature:0"
227
+ ]
228
+ end
229
+
230
+ it "runs each feature once when there are more processes then features (issue #89)" do
231
+ 2.times{|i|
232
+ write "features/good#{i}.feature", "Feature: xxx\n Scenario: xxx\n Given I print TEST_ENV_NUMBER"
233
+ }
234
+ result = run_tests "features", :type => "cucumber", :add => '-n 3'
235
+ result.scan(/YOUR TEST ENV IS \d?!/).sort.should == ["YOUR TEST ENV IS !", "YOUR TEST ENV IS 2!"]
236
+ end
237
+
238
+ it "runs successfully without any files" do
239
+ results = run_tests("", :type => "cucumber")
240
+ results.should include("2 processes for 0 features")
241
+ results.should include("Took")
242
+ end
243
+ end
244
+ end