parallelized_specs 0.0.5 → 0.0.6

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.
data/Gemfile CHANGED
@@ -1,10 +0,0 @@
1
- source :rubygems
2
-
3
- gem 'parallel'
4
-
5
- group :dev do
6
- gem 'test-unit', :platform => :ruby_19
7
- gem 'rspec', '>=2.4'
8
- gem 'rake'
9
- gem 'jeweler'
10
- end
data/Rakefile CHANGED
@@ -12,7 +12,7 @@ begin
12
12
  gem.email = "jake@instructure.com"
13
13
  gem.homepage = "http://github.com/jakesorce/#{gem.name}"
14
14
  gem.authors = "Jake Sorce, Bryan Madsen"
15
- gem.version = "0.0.5"
15
+ gem.version = "0.0.6"
16
16
  end
17
17
 
18
18
  Jeweler::GemcutterTasks.new
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.5
1
+ 0.0.6
@@ -1,2 +1,95 @@
1
1
  #!/usr/bin/env ruby
2
- exec "#{File.join(File.dirname(__FILE__), 'parallelized_test')} -t spec #{ARGV * ' '}"
2
+ require 'rubygems'
3
+ require 'optparse'
4
+ require 'parallel'
5
+ raise "please ' gem install parallel '" if Gem::Version.new(Parallel::VERSION) < Gem::Version.new('0.4.2')
6
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
7
+ require "parallelized_specs"
8
+
9
+ options = {}
10
+ OptionParser.new do |opts|
11
+ opts.banner = <<BANNER
12
+ Run all tests in parallel, giving each process ENV['TEST_ENV_NUMBER'] ('', '2', '3', ...)
13
+
14
+ [optional] Only run selected files & folders:
15
+ parallelized_spec test/bar test/baz/xxx_text_spec.rb
16
+
17
+ Options are:
18
+ BANNER
19
+ opts.on("-n [PROCESSES]", Integer, "How many processes to use, default: available CPUs"){|n| options[:count] = n }
20
+ opts.on("-p", '--pattern [PATTERN]', "run tests matching this pattern"){|pattern| options[:pattern] = pattern }
21
+ opts.on("--no-sort", "do not sort files before running them"){ |no_sort| options[:no_sort] = no_sort }
22
+ opts.on("-m [FLOAT]", "--multiply-processes [FLOAT]", Float, "use given number as a multiplier of processes to run"){ |multiply| options[:multiply] = multiply }
23
+ opts.on("-r", '--root [PATH]', "execute test commands from this path"){|path| options[:root] = path }
24
+ opts.on("-s [PATTERN]", "--single [PATTERN]", "Run all matching files in only one process") do |pattern|
25
+ options[:single_process] ||= []
26
+ options[:single_process] << /#{pattern}/
27
+ end
28
+ opts.on("-e", '--exec [COMMAND]', "execute this code parallel and with ENV['TEST_ENV_NUM']"){|path| options[:execute] = path }
29
+ opts.on("-o", "--test-options '[OPTIONS]'", "execute test commands with those options"){|arg| options[:test_options] = arg }
30
+ opts.on("-t", "--type [TYPE]", "which type of tests to run? test, spec or features"){|type| options[:type] = type }
31
+ opts.on("--non-parallel", "execute same commands but do not in parallel, needs --exec"){ options[:non_parallel] = true }
32
+ opts.on("--chunk-timeout [TIMEOUT]", "timeout before re-printing the output of a child-process"){|timeout| options[:chunk_timeout] = timeout.to_f }
33
+ opts.on('-v', '--version', 'Show Version'){ puts ParallelizedSpecs::VERSION; exit}
34
+ opts.on("-h", "--help", "Show this.") { puts opts; exit }
35
+ end.parse!
36
+
37
+ raise "--no-sort and --single-process are not supported" if options[:no_sort] and options[:single_process]
38
+
39
+ # get files to run from arguments
40
+ options[:files] = ARGV if ARGV.size > 0
41
+
42
+ num_processes = options[:count] || Parallel.processor_count
43
+ num_processes = num_processes * (options[:multiply] || 1)
44
+
45
+ if options[:execute]
46
+ runs = (0...num_processes).to_a
47
+ results = if options[:non_parallel]
48
+ runs.map do |i|
49
+ ParallelizedSpecs.execute_command(options[:execute], i, options)
50
+ end
51
+ else
52
+ Parallel.map(runs, :in_processes => num_processes) do |i|
53
+ ParallelizedSpecs.execute_command(options[:execute], i, options)
54
+ end
55
+ end.flatten
56
+ abort if results.any?{|r| r[:exit_status] != 0 }
57
+ else
58
+ lib, name, task = {
59
+ 'spec' => %w(specs spec spec),
60
+ }[options[:type]||'spec']
61
+
62
+ require "parallelized_#{lib}"
63
+ klass = eval("Parallelized#{lib.capitalize}")
64
+
65
+ start = Time.now
66
+
67
+ tests_folder = task
68
+ tests_folder = File.join(options[:root], tests_folder) unless options[:root].to_s.empty?
69
+
70
+ groups = klass.tests_in_groups(options[:files] || tests_folder, num_processes, options)
71
+ num_processes = groups.size
72
+
73
+ #adjust processes to groups
74
+ abort "no #{name}s found!" if groups.size == 0
75
+
76
+ num_tests = groups.inject(0){|sum,item| sum + item.size }
77
+ puts "#{num_processes} processes for #{num_tests} #{name}s, ~ #{num_tests / groups.size} #{name}s per process"
78
+
79
+ test_results = Parallel.map(groups, :in_processes => num_processes) do |group|
80
+ klass.run_tests(group, groups.index(group), options)
81
+ end
82
+
83
+ #parse and print results
84
+ results = klass.find_results(test_results.map{|result| result[:stdout] }*"")
85
+ puts ""
86
+ puts klass.summarize_results(results)
87
+
88
+ #report total time taken
89
+ puts ""
90
+ puts "Took #{Time.now - start} seconds"
91
+
92
+ #exit with correct status code so rake parallel:test && echo 123 works
93
+ failed = test_results.any?{|result| result[:exit_status] != 0 }
94
+ abort "#{name.capitalize}s Failed" if failed
95
+ end
@@ -1,6 +1,10 @@
1
- require 'parallelized_tests'
1
+ require 'parallel'
2
+ require 'parallelized_specs/grouper'
3
+ require 'parallelized_specs/railtie'
4
+
5
+ class ParallelizedSpecs
6
+ VERSION = File.read(File.join(File.dirname(__FILE__), '..', 'VERSION')).strip
2
7
 
3
- class ParallelizedSpecs < ParallelizedTests
4
8
  def self.run_tests(test_files, process_number, options)
5
9
  exe = executable # expensive, so we cache
6
10
  version = (exe =~ /\brspec\b/ ? 2 : 1)
@@ -10,24 +14,18 @@ class ParallelizedSpecs < ParallelizedTests
10
14
 
11
15
  def self.executable
12
16
  cmd = if File.file?("script/spec")
13
- "script/spec"
14
- elsif bundler_enabled?
15
- cmd = (run("bundle show rspec") =~ %r{/rspec-1[^/]+$} ? "spec" : "rspec")
16
- "bundle exec #{cmd}"
17
- else
18
- %w[spec rspec].detect{|cmd| system "#{cmd} --version > /dev/null 2>&1" }
19
- end
17
+ "script/spec"
18
+ elsif bundler_enabled?
19
+ cmd = (run("bundle show rspec") =~ %r{/rspec-1[^/]+$} ? "spec" : "rspec")
20
+ "bundle exec #{cmd}"
21
+ else
22
+ %w[spec rspec].detect { |cmd| system "#{cmd} --version > /dev/null 2>&1" }
23
+ end
20
24
  cmd or raise("Can't find executables rspec or spec")
21
25
  end
22
26
 
23
- # legacy <-> people log to this file using rspec options
24
- def self.runtime_log
25
- 'tmp/parallelized_profile.log'
26
- end
27
-
28
27
  protected
29
-
30
- # so it can be stubbed....
28
+ #so it can be stubbed....
31
29
  def self.run(cmd)
32
30
  `#{cmd}`
33
31
  end
@@ -41,7 +39,7 @@ class ParallelizedSpecs < ParallelizedTests
41
39
  end
42
40
 
43
41
  def self.spec_opts(rspec_version)
44
- options_file = ['spec/parallelized_spec.opts', 'spec/spec.opts'].detect{|f| File.file?(f) }
42
+ options_file = %w(spec/parallelized_spec.opts spec/spec.opts).detect { |f| File.file?(f) }
45
43
  return unless options_file
46
44
  "-O #{options_file}"
47
45
  end
@@ -49,4 +47,150 @@ class ParallelizedSpecs < ParallelizedTests
49
47
  def self.test_suffix
50
48
  "_spec.rb"
51
49
  end
50
+
51
+ # parallel:spec[:count, :pattern, :options]
52
+ def self.parse_rake_args(args)
53
+ # order as given by user
54
+ args = [args[:count], args[:pattern], args[:options]]
55
+
56
+ # count given or empty ?
57
+ # parallel:spec[2,models,options]
58
+ # parallel:spec[,models,options]
59
+ count = args.shift if args.first.to_s =~ /^\d*$/
60
+ num_processes = count.to_i unless count.to_s.empty?
61
+ num_processes ||= ENV['PARALLEL_TEST_PROCESSORS'].to_i if ENV['PARALLEL_TEST_PROCESSORS']
62
+ num_processes ||= Parallel.processor_count
63
+
64
+ pattern = args.shift
65
+ options = args.shift
66
+
67
+ [num_processes.to_i, pattern.to_s, options.to_s]
68
+ end
69
+
70
+ # finds all tests and partitions them into groups
71
+ def self.tests_in_groups(root, num_groups, options={})
72
+ tests = find_tests(root, options)
73
+ if options[:no_sort]
74
+ Grouper.in_groups(tests, num_groups)
75
+ else
76
+ tests = with_runtime_info(tests)
77
+ Grouper.in_even_groups_by_size(tests, num_groups, options)
78
+ end
79
+ end
80
+
81
+ def self.execute_command(cmd, process_number, options)
82
+ cmd = "TEST_ENV_NUMBER=#{test_env_number(process_number)} ; export TEST_ENV_NUMBER; #{cmd}"
83
+ f = open("|#{cmd}", 'r')
84
+ output = fetch_output(f, options)
85
+ f.close
86
+ {:stdout => output, :exit_status => $?.exitstatus}
87
+ end
88
+
89
+ def self.find_results(test_output)
90
+ test_output.split("\n").map { |line|
91
+ line = line.gsub(/\.|F|\*/, '')
92
+ next unless line_is_result?(line)
93
+ line
94
+ }.compact
95
+ end
96
+
97
+ def self.test_env_number(process_number)
98
+ process_number == 0 ? '' : process_number + 1
99
+ end
100
+
101
+ def self.runtime_log
102
+ 'tmp/parallelized_runtime_test.log'
103
+ end
104
+
105
+ def self.summarize_results(results)
106
+ results = results.join(' ').gsub(/s\b/, '') # combine and singularize results
107
+ counts = results.scan(/(\d+) (\w+)/)
108
+ sums = counts.inject(Hash.new(0)) do |sum, (number, word)|
109
+ sum[word] += number.to_i
110
+ sum
111
+ end
112
+ sums.sort.map { |word, number| "#{number} #{word}#{'s' if number != 1}" }.join(', ')
113
+ end
114
+
115
+ protected
116
+
117
+ # read output of the process and print in in chucks
118
+ def self.fetch_output(process, options)
119
+ all = ''
120
+ buffer = ''
121
+ timeout = options[:chunk_timeout] || 0.2
122
+ flushed = Time.now.to_f
123
+
124
+ while (char = process.getc)
125
+ char = (char.is_a?(Fixnum) ? char.chr : char) # 1.8 <-> 1.9
126
+ all << char
127
+
128
+ # print in chunks so large blocks stay together
129
+ now = Time.now.to_f
130
+ buffer << char
131
+ if flushed + timeout < now
132
+ print buffer
133
+ STDOUT.flush
134
+ buffer = ''
135
+ flushed = now
136
+ end
137
+ end
138
+
139
+ # print the remainder
140
+ print buffer
141
+ STDOUT.flush
142
+
143
+ all
144
+ end
145
+
146
+ # copied from http://github.com/carlhuda/bundler Bundler::SharedHelpers#find_gemfile
147
+ def self.bundler_enabled?
148
+ return true if Object.const_defined?(:Bundler)
149
+
150
+ previous = nil
151
+ current = File.expand_path(Dir.pwd)
152
+
153
+ until !File.directory?(current) || current == previous
154
+ filename = File.join(current, "Gemfile")
155
+ return true if File.exists?(filename)
156
+ current, previous = File.expand_path("..", current), current
157
+ end
158
+
159
+ false
160
+ end
161
+
162
+ def self.line_is_result?(line)
163
+ line =~ /\d+ failure/
164
+ end
165
+
166
+ def self.with_runtime_info(tests)
167
+ lines = File.read(runtime_log).split("\n") rescue []
168
+
169
+ # use recorded test runtime if we got enough data
170
+ if lines.size * 1.5 > tests.size
171
+ puts "Using recorded test runtime"
172
+ times = Hash.new(1)
173
+ lines.each do |line|
174
+ test, time = line.split(":")
175
+ next unless test and time
176
+ times[File.expand_path(test)] = time.to_f
177
+ end
178
+ tests.sort.map { |test| [test, times[test]] }
179
+ else # use file sizes
180
+ tests.sort.map { |test| [test, File.stat(test).size] }
181
+ end
182
+ end
183
+
184
+ def self.find_tests(root, options={})
185
+ if root.is_a?(Array)
186
+ root
187
+ else
188
+ # follow one symlink and direct children
189
+ # http://stackoverflow.com/questions/357754/can-i-traverse-symlinked-directories-in-ruby-with-a-glob
190
+ files = Dir["#{root}/**{,/*/**}/*#{test_suffix}"].uniq
191
+ files = files.map { |f| f.sub(root+'/', '') }
192
+ files = files.grep(/#{options[:pattern]}/)
193
+ files.map { |f| "#{root}/#{f}" }
194
+ end
195
+ end
52
196
  end
@@ -1,4 +1,4 @@
1
- class ParallelizedTests
1
+ class ParallelizedSpecs
2
2
  class Grouper
3
3
  def self.in_groups(items, num_groups)
4
4
  groups = Array.new(num_groups){ [] }
@@ -1,9 +1,9 @@
1
1
  # add rake tasks if we are inside Rails
2
2
  if defined?(Rails::Railtie)
3
- class ParallelizedTests
3
+ class ParallelizedSpecs
4
4
  class Railtie < ::Rails::Railtie
5
5
  rake_tasks do
6
- load File.expand_path("../../tasks/parallelized_tests.rake", __FILE__)
6
+ load File.expand_path("../tasks/parallelized_specs.rake", __FILE__)
7
7
  end
8
8
  end
9
9
  end
@@ -0,0 +1,26 @@
1
+ class ParallelizedSpecs::RuntimeLogger
2
+ @@has_started = false
3
+
4
+ def self.log(test, start_time, end_time)
5
+
6
+ if !@@has_started # make empty log file
7
+ File.open(ParallelizedSpecs.runtime_log, 'w') do end
8
+ @@has_started = true
9
+ end
10
+
11
+ File.open(ParallelizedSpecs.runtime_log, 'a') do |output|
12
+ begin
13
+ output.flock File::LOCK_EX
14
+ output.puts(self.message(test, start_time, end_time))
15
+ ensure
16
+ output.flock File::LOCK_UN
17
+ end
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
+ message="#{filename}:#{delta}"
25
+ end
26
+ end
@@ -1,23 +1,23 @@
1
1
  namespace :parallel do
2
2
  def run_in_parallel(cmd, options)
3
3
  count = (options[:count] ? options[:count].to_i : nil)
4
- executable = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'parallelized_test')
4
+ executable = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'parallelized_spec')
5
5
  command = "#{executable} --exec '#{cmd}' -n #{count} #{'--non-parallel' if options[:non_parallel]}"
6
6
  abort unless system(command)
7
7
  end
8
8
 
9
9
  desc "create test databases via db:create --> parallel:create[num_cpus]"
10
- task :create, :count do |t,args|
10
+ task :create, :count do |t, args|
11
11
  run_in_parallel('rake db:create RAILS_ENV=test', args)
12
12
  end
13
13
 
14
14
  desc "drop test databases via db:drop --> parallel:drop[num_cpus]"
15
- task :drop, :count do |t,args|
15
+ task :drop, :count do |t, args|
16
16
  run_in_parallel('rake db:drop RAILS_ENV=test', args)
17
17
  end
18
18
 
19
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|
20
+ task(:prepare, [:count] => 'db:abort_if_pending_migrations') do |t, args|
21
21
  if defined?(ActiveRecord) && ActiveRecord::Base.schema_format == :ruby
22
22
  # dump then load in parallel
23
23
  Rake::Task['db:schema:dump'].invoke
@@ -31,26 +31,24 @@ namespace :parallel do
31
31
 
32
32
  # when dumping/resetting takes too long
33
33
  desc "update test databases via db:migrate --> parallel:migrate[num_cpus]"
34
- task :migrate, :count do |t,args|
34
+ task :migrate, :count do |t, args|
35
35
  run_in_parallel('rake db:migrate RAILS_ENV=test', args)
36
36
  end
37
37
 
38
38
  # just load the schema (good for integration server <-> no development db)
39
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|
40
+ task :load_schema, :count do |t, args|
41
41
  run_in_parallel('rake db:test:load', args)
42
42
  end
43
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, :arguments do |t,args|
47
- $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), '..'))
48
- require "parallelized_tests"
49
- count, pattern, options = ParallelizedTests.parse_rake_args(args)
50
- executable = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'parallelized_test')
51
- command = "#{executable} --type #{type} -n #{count} -p '#{pattern}' -r '#{Rails.root}' -o '#{options}' #{args[:arguments]}"
52
- abort unless system(command) # allow to chain tasks e.g. rake parallel:spec parallel:features
53
- end
44
+ desc "run spec in parallel with parallel:spec[num_cpus]"
45
+ task 'spec', :count, :pattern, :options, :arguments do |t, args|
46
+ $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), '..'))
47
+ require "parallelized_specs"
48
+ count, pattern, options = ParallelizedSpecs.parse_rake_args(args)
49
+ executable = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'parallelized_spec')
50
+ command = "#{executable} --type 'spec' -n #{count} -p '#{pattern}' -r '#{Rails.root}' -o '#{options}' #{args[:arguments]}"
51
+ abort unless system(command) # allow to chain tasks e.g. rake parallel:spec parallel:features
54
52
  end
55
53
  end
56
54
 
@@ -60,20 +58,20 @@ end
60
58
  #test:parallel
61
59
  namespace :spec do
62
60
  namespace :parallel do
63
- task :prepare, :count do |t,args|
61
+ task :prepare, :count do |t, args|
64
62
  $stderr.puts "WARNING -- Deprecated! use parallel:prepare"
65
63
  Rake::Task['parallel:prepare'].invoke(args[:count])
66
64
  end
67
65
  end
68
66
 
69
- task :parallel, :count, :pattern do |t,args|
67
+ task :parallel, :count, :pattern do |t, args|
70
68
  $stderr.puts "WARNING -- Deprecated! use parallel:spec"
71
69
  Rake::Task['parallel:spec'].invoke(args[:count], args[:pattern])
72
70
  end
73
71
  end
74
72
 
75
73
  namespace :test do
76
- task :parallel, :count, :pattern do |t,args|
74
+ task :parallel, :count, :pattern do |t, args|
77
75
  $stderr.puts "WARNING -- Deprecated! use parallel:test"
78
76
  Rake::Task['parallel:test'].invoke(args[:count], args[:pattern])
79
77
  end