parallelized_specs 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,163 @@
1
+ require 'parallel'
2
+ require 'parallel_tests/grouper'
3
+ require 'parallel_tests/railtie'
4
+
5
+ class ParallelTests
6
+ VERSION = File.read( File.join(File.dirname(__FILE__),'..','VERSION') ).strip
7
+
8
+ # parallel:spec[:count, :pattern, :options]
9
+ def self.parse_rake_args(args)
10
+ # order as given by user
11
+ args = [args[:count], args[:pattern], args[:options]]
12
+
13
+ # count given or empty ?
14
+ # parallel:spec[2,models,options]
15
+ # parallel:spec[,models,options]
16
+ count = args.shift if args.first.to_s =~ /^\d*$/
17
+ num_processes = count.to_i unless count.to_s.empty?
18
+ num_processes ||= ENV['PARALLEL_TEST_PROCESSORS'].to_i if ENV['PARALLEL_TEST_PROCESSORS']
19
+ num_processes ||= Parallel.processor_count
20
+
21
+ pattern = args.shift
22
+ options = args.shift
23
+
24
+ [num_processes.to_i, pattern.to_s, options.to_s]
25
+ end
26
+
27
+ # finds all tests and partitions them into groups
28
+ def self.tests_in_groups(root, num_groups, options={})
29
+ tests = find_tests(root, options)
30
+ if options[:no_sort] == true
31
+ Grouper.in_groups(tests, num_groups)
32
+ else
33
+ tests = with_runtime_info(tests)
34
+ Grouper.in_even_groups_by_size(tests, num_groups, options)
35
+ end
36
+ end
37
+
38
+ def self.run_tests(test_files, process_number, options)
39
+ require_list = test_files.map { |filename| %{"#{File.expand_path filename}"} }.join(",")
40
+ cmd = "ruby -Itest -e '[#{require_list}].each {|f| require f }' -- #{options[:test_options]}"
41
+ execute_command(cmd, process_number, options)
42
+ end
43
+
44
+ def self.execute_command(cmd, process_number, options)
45
+ cmd = "TEST_ENV_NUMBER=#{test_env_number(process_number)} ; export TEST_ENV_NUMBER; #{cmd}"
46
+ f = open("|#{cmd}", 'r')
47
+ output = fetch_output(f, options)
48
+ f.close
49
+ {:stdout => output, :exit_status => $?.exitstatus}
50
+ end
51
+
52
+ def self.find_results(test_output)
53
+ test_output.split("\n").map {|line|
54
+ line = line.gsub(/\.|F|\*/,'')
55
+ next unless line_is_result?(line)
56
+ line
57
+ }.compact
58
+ end
59
+
60
+ def self.test_env_number(process_number)
61
+ process_number == 0 ? '' : process_number + 1
62
+ end
63
+
64
+ def self.runtime_log
65
+ 'tmp/parallel_runtime_test.log'
66
+ end
67
+
68
+ def self.summarize_results(results)
69
+ results = results.join(' ').gsub(/s\b/,'') # combine and singularize results
70
+ counts = results.scan(/(\d+) (\w+)/)
71
+ sums = counts.inject(Hash.new(0)) do |sum, (number, word)|
72
+ sum[word] += number.to_i
73
+ sum
74
+ end
75
+ sums.sort.map{|word, number| "#{number} #{word}#{'s' if number != 1}" }.join(', ')
76
+ end
77
+
78
+ protected
79
+
80
+ # read output of the process and print in in chucks
81
+ def self.fetch_output(process, options)
82
+ all = ''
83
+ buffer = ''
84
+ timeout = options[:chunk_timeout] || 0.2
85
+ flushed = Time.now.to_f
86
+
87
+ while char = process.getc
88
+ char = (char.is_a?(Fixnum) ? char.chr : char) # 1.8 <-> 1.9
89
+ all << char
90
+
91
+ # print in chunks so large blocks stay together
92
+ now = Time.now.to_f
93
+ buffer << char
94
+ if flushed + timeout < now
95
+ print buffer
96
+ STDOUT.flush
97
+ buffer = ''
98
+ flushed = now
99
+ end
100
+ end
101
+
102
+ # print the remainder
103
+ print buffer
104
+ STDOUT.flush
105
+
106
+ all
107
+ end
108
+
109
+ # copied from http://github.com/carlhuda/bundler Bundler::SharedHelpers#find_gemfile
110
+ def self.bundler_enabled?
111
+ return true if Object.const_defined?(:Bundler)
112
+
113
+ previous = nil
114
+ current = File.expand_path(Dir.pwd)
115
+
116
+ until !File.directory?(current) || current == previous
117
+ filename = File.join(current, "Gemfile")
118
+ return true if File.exists?(filename)
119
+ current, previous = File.expand_path("..", current), current
120
+ end
121
+
122
+ false
123
+ end
124
+
125
+ def self.line_is_result?(line)
126
+ line =~ /\d+ failure/
127
+ end
128
+
129
+ def self.test_suffix
130
+ "_test.rb"
131
+ end
132
+
133
+ def self.with_runtime_info(tests)
134
+ lines = File.read(runtime_log).split("\n") rescue []
135
+
136
+ # use recorded test runtime if we got enough data
137
+ if lines.size * 1.5 > tests.size
138
+ puts "Using recorded test runtime"
139
+ times = Hash.new(1)
140
+ lines.each do |line|
141
+ test, time = line.split(":")
142
+ next unless test and time
143
+ times[File.expand_path(test)] = time.to_f
144
+ end
145
+ tests.sort.map{|test| [test, times[test]] }
146
+ else # use file sizes
147
+ tests.sort.map{|test| [test, File.stat(test).size] }
148
+ end
149
+ end
150
+
151
+ def self.find_tests(root, options={})
152
+ if root.is_a?(Array)
153
+ root
154
+ else
155
+ # follow one symlink and direct children
156
+ # http://stackoverflow.com/questions/357754/can-i-traverse-symlinked-directories-in-ruby-with-a-glob
157
+ files = Dir["#{root}/**{,/*/**}/*#{test_suffix}"].uniq
158
+ files = files.map{|f| f.sub(root+'/','') }
159
+ files = files.grep(/#{options[:pattern]}/)
160
+ files.map{|f| "#{root}/#{f}" }
161
+ end
162
+ end
163
+ end
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__), "/../parallel_tests/tasks")
@@ -0,0 +1,64 @@
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 = "parallelized_specs"
8
+ s.version = "0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Jake Sorce, Bryan Madsen"]
12
+ s.date = "2012-04-17"
13
+ s.email = "jake@instructure.com"
14
+ s.executables = ["parallel_test", "parallel_spec"]
15
+ s.files = [
16
+ "Gemfile",
17
+ "Gemfile.lock",
18
+ "Rakefile",
19
+ "Readme.md",
20
+ "VERSION",
21
+ "bin/parallel_spec",
22
+ "bin/parallel_test",
23
+ "lib/parallel_specs.rb",
24
+ "lib/parallel_specs/spec_error_count_logger.rb",
25
+ "lib/parallel_specs/spec_error_logger.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_start_finish_logger.rb",
30
+ "lib/parallel_specs/spec_summary_logger.rb",
31
+ "lib/parallel_tests.rb",
32
+ "lib/parallel_tests/grouper.rb",
33
+ "lib/parallel_tests/railtie.rb",
34
+ "lib/parallel_tests/runtime_logger.rb",
35
+ "lib/parallel_tests/tasks.rb",
36
+ "lib/tasks/parallel_tests.rake",
37
+ "parallelized_specs.gemspec",
38
+ "spec/integration_spec.rb",
39
+ "spec/parallel_specs/spec_failure_logger_spec.rb",
40
+ "spec/parallel_specs/spec_runtime_logger_spec.rb",
41
+ "spec/parallel_specs/spec_summary_logger_spec.rb",
42
+ "spec/parallel_specs_spec.rb",
43
+ "spec/parallel_tests/runtime_logger_spec.rb",
44
+ "spec/parallel_tests_spec.rb",
45
+ "spec/spec_helper.rb"
46
+ ]
47
+ s.homepage = "http://github.com/jake/parallelized_specs"
48
+ s.require_paths = ["lib"]
49
+ s.rubygems_version = "1.8.22"
50
+ s.summary = "Run rspec tests in parallel"
51
+
52
+ if s.respond_to? :specification_version then
53
+ s.specification_version = 3
54
+
55
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
56
+ s.add_runtime_dependency(%q<parallel>, [">= 0"])
57
+ else
58
+ s.add_dependency(%q<parallel>, [">= 0"])
59
+ end
60
+ else
61
+ s.add_dependency(%q<parallel>, [">= 0"])
62
+ end
63
+ end
64
+
@@ -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,82 @@
1
+ require 'spec_helper'
2
+
3
+ describe ParallelSpecs::SpecFailuresLogger do
4
+ def silence_warnings
5
+ old_verbose, $VERBOSE = $VERBOSE, nil
6
+ yield
7
+ ensure
8
+ $VERBOSE = old_verbose
9
+ end
10
+
11
+ before do
12
+ @output = OutputLogger.new([])
13
+ @example1 = mock( 'example', :location => "#{Dir.pwd}/spec/path/to/example:123", :full_description => 'should do stuff', :description => 'd' )
14
+ @example2 = mock( 'example', :location => "#{Dir.pwd}/spec/path/to/example2:456", :full_description => 'should do other stuff', :description => 'd')
15
+ @exception1 = mock( :to_s => 'exception', :backtrace => [ '/path/to/error/line:33' ] )
16
+ @failure1 = mock( 'example', :location => "#{Dir.pwd}/example:123", :header => 'header', :exception => @exception1 )
17
+ @logger = ParallelSpecs::SpecFailuresLogger.new( @output )
18
+ end
19
+
20
+ after do
21
+ silence_warnings{ ParallelSpecs::SpecLoggerBase::RSPEC_1 = false }
22
+ end
23
+
24
+ def clean_output
25
+ @output.output.join("\n").gsub(/\e\[\d+m/,'')
26
+ end
27
+
28
+ it "should produce a list of command lines for failing examples" do
29
+ @logger.example_failed @example1
30
+ @logger.example_failed @example2
31
+
32
+ @logger.dump_failures
33
+ @logger.dump_summary(1,2,3,4)
34
+
35
+ clean_output.should =~ /^rspec .*? should do stuff/
36
+ clean_output.should =~ /^rspec .*? should do other stuff/
37
+ end
38
+
39
+ it "should invoke spec for rspec 1" do
40
+ silence_warnings{ ParallelSpecs::SpecLoggerBase::RSPEC_1 = true }
41
+ ParallelSpecs.stub!(:bundler_enabled?).and_return true
42
+ ParallelSpecs.stub!(:run).with("bundle show rspec").and_return "/foo/bar/rspec-1.0.2"
43
+ @logger.example_failed @example1
44
+
45
+ @logger.dump_failures
46
+ @logger.dump_summary(1,2,3,4)
47
+
48
+ clean_output.should =~ /^bundle exec spec/
49
+ end
50
+
51
+ it "should invoke rspec for rspec 2" do
52
+ ParallelSpecs.stub!(:bundler_enabled?).and_return true
53
+ ParallelSpecs.stub!(:run).with("bundle show rspec").and_return "/foo/bar/rspec-2.0.2"
54
+ @logger.example_failed @example1
55
+
56
+ @logger.dump_failures
57
+ @logger.dump_summary(1,2,3,4)
58
+
59
+ clean_output.should =~ /^rspec/
60
+ end
61
+
62
+ it "should return relative paths" do
63
+ @logger.example_failed @example1
64
+ @logger.example_failed @example2
65
+
66
+ @logger.dump_failures
67
+ @logger.dump_summary(1,2,3,4)
68
+
69
+ clean_output.should =~ %r(\./spec/path/to/example:123)
70
+ clean_output.should =~ %r(\./spec/path/to/example2:456)
71
+ end
72
+
73
+
74
+ # should not longer be a problem since its using native rspec methods
75
+ xit "should not log examples without location" do
76
+ example = mock('example', :location => 'bla', :full_description => 'before :all')
77
+ @logger.example_failed example
78
+ @logger.dump_failures
79
+ @logger.dump_summary(1,2,3,4)
80
+ clean_output.should == ''
81
+ end
82
+ end