parallel_tests-instructure 0.6.16

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.
@@ -0,0 +1,49 @@
1
+ class ParallelTests
2
+ class Grouper
3
+ def self.in_groups(items, num_groups)
4
+ groups = Array.new(num_groups){ [] }
5
+
6
+ until items.empty?
7
+ num_groups.times do |group_number|
8
+ groups[group_number] << items.shift
9
+ end
10
+ end
11
+
12
+ groups.map!(&:sort!)
13
+ end
14
+
15
+ def self.in_even_groups_by_size(items_with_sizes, num_groups, options={})
16
+ groups = Array.new(num_groups){{:items => [], :size => 0}}
17
+
18
+ # add all files that should run in a single process to one group
19
+ (options[:single_process]||[]).each do |pattern|
20
+ matched, items_with_sizes = items_with_sizes.partition{|item, size| item =~ pattern }
21
+ smallest = smallest_group(groups)
22
+ matched.each{|item,size| add_to_group(smallest, item, size) }
23
+ end
24
+
25
+ # add all other files
26
+ largest_first(items_with_sizes).each do |item, size|
27
+ smallest = smallest_group(groups)
28
+ add_to_group(smallest, item, size)
29
+ end
30
+
31
+ groups.map!{|g| g[:items].sort }
32
+ end
33
+
34
+ def self.largest_first(files)
35
+ files.sort_by{|item, size| size }.reverse
36
+ end
37
+
38
+ private
39
+
40
+ def self.smallest_group(groups)
41
+ groups.min_by{|g| g[:size] }
42
+ end
43
+
44
+ def self.add_to_group(group, item, size)
45
+ group[:items] << item
46
+ group[:size] += size
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,10 @@
1
+ # add rake tasks if we are inside Rails
2
+ if defined?(Rails::Railtie)
3
+ class ParallelTests
4
+ class Railtie < ::Rails::Railtie
5
+ rake_tasks do
6
+ load File.expand_path("../../tasks/parallel_tests.rake", __FILE__)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,78 @@
1
+ class ParallelTests::RuntimeLogger
2
+ @@has_started = false
3
+
4
+ def self.log(test, start_time, end_time)
5
+ return if test.is_a? Test::Unit::TestSuite # don't log for suites-of-suites
6
+
7
+ if !@@has_started # make empty log file
8
+ File.open(ParallelTests.runtime_log, 'w') do end
9
+ @@has_started = true
10
+ end
11
+
12
+ File.open(ParallelTests.runtime_log, 'a') do |output|
13
+ begin
14
+ output.flock File::LOCK_EX
15
+ output.puts(self.message(test, start_time, end_time))
16
+ ensure
17
+ output.flock File::LOCK_UN
18
+ end
19
+ end
20
+ end
21
+
22
+ def self.message(test, start_time, end_time)
23
+ delta="%.2f" % (end_time.to_f-start_time.to_f)
24
+ filename=class_directory(test.class) + class_to_filename(test.class) + ".rb"
25
+ message="#{filename}:#{delta}"
26
+ end
27
+
28
+ # Note: this is a best guess at conventional test directory structure, and may need
29
+ # tweaking / post-processing to match correctly for any given project
30
+ def self.class_directory(suspect)
31
+ result = "test/"
32
+
33
+ if defined?(Rails)
34
+ result += case suspect.superclass.name
35
+ when "ActionDispatch::IntegrationTest"
36
+ "integration/"
37
+ when "ActionDispatch::PerformanceTest"
38
+ "performance/"
39
+ when "ActionController::TestCase"
40
+ "functional/"
41
+ when "ActionView::TestCase"
42
+ "unit/helpers/"
43
+ else
44
+ "unit/"
45
+ end
46
+ end
47
+ result
48
+ end
49
+
50
+ # based on https://github.com/grosser/single_test/blob/master/lib/single_test.rb#L117
51
+ def self.class_to_filename(suspect)
52
+ word = suspect.to_s.dup
53
+ return word unless word.match /^[A-Z]/ and not word.match %r{/[a-z]}
54
+
55
+ word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
56
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
57
+ word.gsub!(/\:\:/,'/')
58
+ word.tr!("-", "_")
59
+ word.downcase!
60
+ word
61
+ end
62
+
63
+ end
64
+
65
+ require 'test/unit/testsuite'
66
+ class Test::Unit::TestSuite
67
+
68
+ alias :run_without_timing :run unless defined? @@timing_installed
69
+
70
+ def run(result, &progress_block)
71
+ start_time=Time.now
72
+ run_without_timing(result, &progress_block)
73
+ end_time=Time.now
74
+ ParallelTests::RuntimeLogger.log(self.tests.first, start_time, end_time)
75
+ end
76
+ @@timing_installed = true
77
+
78
+ end
@@ -0,0 +1,80 @@
1
+ namespace :parallel do
2
+ def run_in_parallel(cmd, options)
3
+ count = (options[:count] ? options[:count].to_i : nil)
4
+ executable = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'parallel_test')
5
+ command = "#{executable} --exec '#{cmd}' -n #{count} #{'--non-parallel' if options[:non_parallel]}"
6
+ abort unless system(command)
7
+ end
8
+
9
+ desc "create test databases via db:create --> parallel:create[num_cpus]"
10
+ task :create, :count do |t,args|
11
+ run_in_parallel('rake db:create RAILS_ENV=test', args)
12
+ end
13
+
14
+ desc "drop test databases via db:drop --> parallel:drop[num_cpus]"
15
+ task :drop, :count do |t,args|
16
+ run_in_parallel('rake db:drop RAILS_ENV=test', args)
17
+ end
18
+
19
+ desc "update test databases by dumping and loading --> parallel:prepare[num_cpus]"
20
+ task(:prepare, [:count] => 'db:abort_if_pending_migrations') do |t,args|
21
+ if defined?(ActiveRecord) && ActiveRecord::Base.schema_format == :ruby
22
+ # dump then load in parallel
23
+ Rake::Task['db:schema:dump'].invoke
24
+ Rake::Task['parallel:load_schema'].invoke(args[:count])
25
+ else
26
+ # there is no separate dump / load for schema_format :sql -> do it safe and slow
27
+ args = args.to_hash.merge(:non_parallel => true) # normal merge returns nil
28
+ run_in_parallel('rake db:test:prepare --trace', args)
29
+ end
30
+ end
31
+
32
+ # when dumping/resetting takes too long
33
+ desc "update test databases via db:migrate --> parallel:migrate[num_cpus]"
34
+ task :migrate, :count do |t,args|
35
+ run_in_parallel('rake db:migrate RAILS_ENV=test', args)
36
+ end
37
+
38
+ # just load the schema (good for integration server <-> no development db)
39
+ desc "load dumped schema for test databases via db:schema:load --> parallel:load_schema[num_cpus]"
40
+ task :load_schema, :count do |t,args|
41
+ run_in_parallel('rake db:test:load', args)
42
+ end
43
+
44
+ ['test', 'spec', 'features'].each do |type|
45
+ desc "run #{type} in parallel with parallel:#{type}[num_cpus]"
46
+ task type, :count, :pattern, :options do |t,args|
47
+ $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), '..'))
48
+ require "parallel_tests"
49
+ count, pattern, options = ParallelTests.parse_rake_args(args)
50
+ executable = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'parallel_test')
51
+ command = "#{executable} --type #{type} -n #{count} -p '#{pattern}' -r '#{Rails.root}' -o '#{options}'"
52
+ abort unless system(command) # allow to chain tasks e.g. rake parallel:spec parallel:features
53
+ end
54
+ end
55
+ end
56
+
57
+ #backwards compatability
58
+ #spec:parallel:prepare
59
+ #spec:parallel
60
+ #test:parallel
61
+ namespace :spec do
62
+ namespace :parallel do
63
+ task :prepare, :count do |t,args|
64
+ $stderr.puts "WARNING -- Deprecated! use parallel:prepare"
65
+ Rake::Task['parallel:prepare'].invoke(args[:count])
66
+ end
67
+ end
68
+
69
+ task :parallel, :count, :pattern do |t,args|
70
+ $stderr.puts "WARNING -- Deprecated! use parallel:spec"
71
+ Rake::Task['parallel:spec'].invoke(args[:count], args[:pattern])
72
+ end
73
+ end
74
+
75
+ namespace :test do
76
+ task :parallel, :count, :pattern do |t,args|
77
+ $stderr.puts "WARNING -- Deprecated! use parallel:test"
78
+ Rake::Task['parallel:test'].invoke(args[:count], args[:pattern])
79
+ end
80
+ end
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__), "/../parallel_tests/tasks")
@@ -0,0 +1,67 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "parallel_tests-instructure"
8
+ s.version = "0.6.16"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Michael Grosser, Bryan Madsen"]
12
+ s.date = "2012-02-27"
13
+ s.executables = ["parallel_cucumber", "parallel_test", "parallel_spec"]
14
+ s.files = [
15
+ "Gemfile",
16
+ "Gemfile.lock",
17
+ "Rakefile",
18
+ "Readme.md",
19
+ "VERSION",
20
+ "bin/parallel_cucumber",
21
+ "bin/parallel_spec",
22
+ "bin/parallel_test",
23
+ "lib/parallel_cucumber.rb",
24
+ "lib/parallel_cucumber/runtime_logger.rb",
25
+ "lib/parallel_specs.rb",
26
+ "lib/parallel_specs/spec_failures_logger.rb",
27
+ "lib/parallel_specs/spec_logger_base.rb",
28
+ "lib/parallel_specs/spec_runtime_logger.rb",
29
+ "lib/parallel_specs/spec_summary_logger.rb",
30
+ "lib/parallel_specs/spec_error_count_logger.rb",
31
+ "lib/parallel_specs/spec_error_logger.rb",
32
+ "lib/parallel_specs/spec_start_finish_logger.rb",
33
+ "lib/parallel_tests.rb",
34
+ "lib/parallel_tests/grouper.rb",
35
+ "lib/parallel_tests/railtie.rb",
36
+ "lib/parallel_tests/runtime_logger.rb",
37
+ "lib/parallel_tests/tasks.rb",
38
+ "lib/tasks/parallel_tests.rake",
39
+ "parallel_tests-instructure.gemspec",
40
+ "spec/integration_spec.rb",
41
+ "spec/parallel_cucumber_spec.rb",
42
+ "spec/parallel_specs/spec_failure_logger_spec.rb",
43
+ "spec/parallel_specs/spec_runtime_logger_spec.rb",
44
+ "spec/parallel_specs/spec_summary_logger_spec.rb",
45
+ "spec/parallel_specs_spec.rb",
46
+ "spec/parallel_tests/runtime_logger_spec.rb",
47
+ "spec/parallel_tests_spec.rb",
48
+ "spec/spec_helper.rb"
49
+ ]
50
+ s.homepage = "http://github.com/bmad/parallel_tests-instructure"
51
+ s.require_paths = ["lib"]
52
+ s.rubygems_version = "1.8.15"
53
+ s.summary = "Run tests / specs / features in parallel"
54
+
55
+ if s.respond_to? :specification_version then
56
+ s.specification_version = 3
57
+
58
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
59
+ s.add_runtime_dependency(%q<parallel>, [">= 0"])
60
+ else
61
+ s.add_dependency(%q<parallel>, [">= 0"])
62
+ end
63
+ else
64
+ s.add_dependency(%q<parallel>, [">= 0"])
65
+ end
66
+ end
67
+
@@ -0,0 +1,133 @@
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
+ `mkdir -p #{File.dirname(path)}` unless File.exist?(File.dirname(path))
19
+ File.open(path, 'w'){|f| f.write content }
20
+ path
21
+ end
22
+
23
+ def bin_folder
24
+ "#{File.expand_path(File.dirname(__FILE__))}/../bin"
25
+ end
26
+
27
+ def executable
28
+ "#{bin_folder}/parallel_test"
29
+ end
30
+
31
+ def run_tests(options={})
32
+ `cd #{folder} && #{executable} --chunk-timeout 999 -t #{options[:type] || 'spec'} -n #{options[:processes]||2} #{options[:add]} 2>&1`
33
+ end
34
+
35
+ it "runs tests in parallel" do
36
+ write 'spec/xxx_spec.rb', 'describe("it"){it("should"){puts "TEST1"}}'
37
+ write 'spec/xxx2_spec.rb', 'describe("it"){it("should"){puts "TEST2"}}'
38
+ result = run_tests
39
+
40
+ # test ran and gave their puts
41
+ result.should include('TEST1')
42
+ result.should include('TEST2')
43
+
44
+ # all results present
45
+ result.scan('1 example, 0 failure').size.should == 2 # 2 results
46
+ result.scan('2 examples, 0 failures').size.should == 1 # 1 summary
47
+ result.scan(/Finished in \d+\.\d+ seconds/).size.should == 2
48
+ result.scan(/Took \d+\.\d+ seconds/).size.should == 1 # parallel summary
49
+ $?.success?.should == true
50
+ end
51
+
52
+ it "does not run any tests if there are none" do
53
+ write 'spec/xxx.rb', 'xxx'
54
+ result = run_tests
55
+ result.should include('No examples found')
56
+ result.should include('Took')
57
+ end
58
+
59
+ it "fails when tests fail" do
60
+ write 'spec/xxx_spec.rb', 'describe("it"){it("should"){puts "TEST1"}}'
61
+ write 'spec/xxx2_spec.rb', 'describe("it"){it("should"){1.should == 2}}'
62
+ result = run_tests
63
+
64
+ result.scan('1 example, 1 failure').size.should == 1
65
+ result.scan('1 example, 0 failure').size.should == 1
66
+ result.scan('2 examples, 1 failure').size.should == 1
67
+ $?.success?.should == false
68
+ end
69
+
70
+ it "can exec given commands with ENV['TEST_ENV_NUM']" do
71
+ result = `#{executable} -e 'ruby -e "print ENV[:TEST_ENV_NUMBER.to_s].to_i"' -n 4`
72
+ result.gsub('"','').split('').sort.should == %w[0 2 3 4]
73
+ end
74
+
75
+ it "can exec given command non-parallel" do
76
+ result = `#{executable} -e 'ruby -e "sleep(rand(10)/100.0); puts ENV[:TEST_ENV_NUMBER.to_s].inspect"' -n 4 --non-parallel`
77
+ result.split("\n").should == %w["" "2" "3" "4"]
78
+ end
79
+
80
+ it "exists with success if all sub-processes returned success" do
81
+ system("#{executable} -e 'cat /dev/null' -n 4").should == true
82
+ end
83
+
84
+ it "exists with failure if any sub-processes returned failure" do
85
+ system("#{executable} -e 'test -e xxxx' -n 4").should == false
86
+ end
87
+
88
+ it "can run through parallel_spec / parallel_cucumber" do
89
+ version = `#{executable} -v`
90
+ `#{bin_folder}/parallel_spec -v`.should == version
91
+ `#{bin_folder}/parallel_cucumber -v`.should == version
92
+ end
93
+
94
+ it "runs faster with more processes" do
95
+ 2.times{|i|
96
+ write "spec/xxx#{i}_spec.rb", 'describe("it"){it("should"){sleep 5}}; $stderr.puts ENV["TEST_ENV_NUMBER"]'
97
+ }
98
+ t = Time.now
99
+ run_tests(:processes => 2)
100
+ expected = 10
101
+ (Time.now - t).should <= expected
102
+ end
103
+
104
+ it "can can with given files" do
105
+ write "spec/x1_spec.rb", "puts '111'"
106
+ write "spec/x2_spec.rb", "puts '222'"
107
+ write "spec/x3_spec.rb", "puts '333'"
108
+ result = run_tests(:add => 'spec/x1_spec.rb spec/x3_spec.rb')
109
+ result.should include('111')
110
+ result.should include('333')
111
+ result.should_not include('222')
112
+ end
113
+
114
+ it "can run with test-options" do
115
+ write "spec/x1_spec.rb", ""
116
+ write "spec/x2_spec.rb", ""
117
+ result = run_tests(:add => "--test-options ' --version'", :processes => 2)
118
+ result.should =~ /\d+\.\d+\.\d+.*\d+\.\d+\.\d+/m # prints version twice
119
+ end
120
+
121
+ it "runs with test::unit" do
122
+ write "test/x1_test.rb", "require 'test/unit'; class XTest < Test::Unit::TestCase; def test_xxx; end; end"
123
+ result = run_tests(:type => :test)
124
+ result.should include('1 test')
125
+ $?.success?.should == true
126
+ end
127
+
128
+ it "passes test options to test::unit" do
129
+ write "test/x1_test.rb", "require 'test/unit'; class XTest < Test::Unit::TestCase; def test_xxx; end; end"
130
+ result = run_tests(:type => :test, :add => '--test-options "-v"')
131
+ result.should include('test_xxx') # verbose output of every test
132
+ end
133
+ end
@@ -0,0 +1,72 @@
1
+ require 'spec_helper'
2
+
3
+ describe ParallelCucumber do
4
+ test_tests_in_groups(ParallelCucumber, 'features', ".feature")
5
+
6
+ describe :run_tests do
7
+ before do
8
+ ParallelCucumber.stub!(:bundler_enabled?).and_return false
9
+ File.stub!(:file?).with('.bundle/environment.rb').and_return false
10
+ File.stub!(:file?).with('script/cucumber').and_return true
11
+ end
12
+
13
+ it "uses TEST_ENV_NUMBER=blank when called for process 0" do
14
+ ParallelCucumber.should_receive(:open).with{|x,y| x=~/TEST_ENV_NUMBER= /}.and_return mocked_process
15
+ ParallelCucumber.run_tests(['xxx'],0,{})
16
+ end
17
+
18
+ it "uses TEST_ENV_NUMBER=2 when called for process 1" do
19
+ ParallelCucumber.should_receive(:open).with{|x,y| x=~/TEST_ENV_NUMBER=2/}.and_return mocked_process
20
+ ParallelCucumber.run_tests(['xxx'],1,{})
21
+ end
22
+
23
+ it "returns the output" do
24
+ io = open('spec/spec_helper.rb')
25
+ ParallelCucumber.stub!(:print)
26
+ ParallelCucumber.should_receive(:open).and_return io
27
+ ParallelCucumber.run_tests(['xxx'],1,{})[:stdout].should =~ /\$LOAD_PATH << File/
28
+ end
29
+
30
+ it "runs bundle exec cucumber when on bundler 0.9" do
31
+ ParallelCucumber.stub!(:bundler_enabled?).and_return true
32
+ ParallelCucumber.should_receive(:open).with{|x,y| x =~ %r{bundle exec cucumber}}.and_return mocked_process
33
+ ParallelCucumber.run_tests(['xxx'],1,{})
34
+ end
35
+
36
+ it "runs script/cucumber when script/cucumber is found" do
37
+ ParallelCucumber.should_receive(:open).with{|x,y| x =~ %r{script/cucumber}}.and_return mocked_process
38
+ ParallelCucumber.run_tests(['xxx'],1,{})
39
+ end
40
+
41
+ it "runs cucumber by default" do
42
+ File.stub!(:file?).with('script/cucumber').and_return false
43
+ ParallelCucumber.should_receive(:open).with{|x,y| x !~ %r{(script/cucumber)|(bundle exec cucumber)}}.and_return mocked_process
44
+ ParallelCucumber.run_tests(['xxx'],1,{})
45
+ end
46
+
47
+ it "uses options passed in" do
48
+ ParallelCucumber.should_receive(:open).with{|x,y| x =~ %r{script/cucumber .* -p default}}.and_return mocked_process
49
+ ParallelCucumber.run_tests(['xxx'],1,:test_options => '-p default')
50
+ end
51
+ end
52
+
53
+ describe :find_results do
54
+ it "finds multiple results in test output" do
55
+ output = <<EOF
56
+ And I should not see "/en/" # features/step_definitions/webrat_steps.rb:87
57
+
58
+ 7 scenarios (3 failed, 4 passed)
59
+ 33 steps (3 failed, 2 skipped, 28 passed)
60
+ /apps/rs/features/signup.feature:2
61
+ Given I am on "/" # features/step_definitions/common_steps.rb:12
62
+ When I click "register" # features/step_definitions/common_steps.rb:6
63
+ And I should have "2" emails # features/step_definitions/user_steps.rb:25
64
+
65
+ 4 scenarios (4 passed)
66
+ 40 steps (40 passed)
67
+
68
+ EOF
69
+ ParallelCucumber.find_results(output).should == ["7 scenarios (3 failed, 4 passed)", "33 steps (3 failed, 2 skipped, 28 passed)", "4 scenarios (4 passed)", "40 steps (40 passed)"]
70
+ end
71
+ end
72
+ end