parallelized_specs 0.0.1

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,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