parallelized_specs 0.3.23 → 0.3.24
Sign up to get free protection for your applications and to get access to all the features.
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, Shawn Meredith"
|
15
|
-
gem.version = "0.3.
|
15
|
+
gem.version = "0.3.24"
|
16
16
|
end
|
17
17
|
Jeweler::GemcutterTasks.new
|
18
18
|
rescue LoadError
|
data/lib/parallelized_specs.rb
CHANGED
@@ -95,172 +95,221 @@ class ParallelizedSpecs
|
|
95
95
|
end
|
96
96
|
groups = tests_in_groups(files_array || tests || tests_folder, num_processes, options)
|
97
97
|
|
98
|
-
|
98
|
+
num_processes = groups.size
|
99
99
|
|
100
|
-
|
101
|
-
|
100
|
+
#adjust processes to groups
|
101
|
+
abort "no #{name}s found!" if groups.size == 0
|
102
102
|
|
103
|
-
|
104
|
-
|
103
|
+
num_tests = groups.inject(0) { |sum, item| sum + item.size }
|
104
|
+
puts "#{num_processes} processes for #{num_tests} #{name}s, ~ #{num_tests / groups.size} #{name}s per process"
|
105
105
|
|
106
|
-
|
107
|
-
|
108
|
-
|
106
|
+
test_results = Parallel.map(groups, :in_processes => num_processes) do |group|
|
107
|
+
run_tests(group, groups.index(group), options)
|
108
|
+
end
|
109
109
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
110
|
+
#parse and print results
|
111
|
+
results = find_results(test_results.map { |result| result[:stdout] }*"")
|
112
|
+
puts ""
|
113
|
+
puts summarize_results(results)
|
114
114
|
|
115
|
-
|
116
|
-
|
117
|
-
|
115
|
+
#report total time taken
|
116
|
+
puts ""
|
117
|
+
puts "Took #{Time.now - start} seconds"
|
118
118
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
end
|
119
|
+
#exit with correct status code so rake parallel:test && echo 123 works
|
120
|
+
failed = test_results.any? { |result| result[:exit_status] != 0 }
|
121
|
+
abort "#{name.capitalize}s Failed" if failed
|
122
|
+
end
|
123
123
|
|
124
124
|
# parallel:spec[:count, :pattern, :options]
|
125
|
-
def self.parse_rake_args(args)
|
126
|
-
|
127
|
-
|
125
|
+
def self.parse_rake_args(args)
|
126
|
+
# order as given by user
|
127
|
+
args = [args[:count], args[:pattern]]
|
128
128
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
129
|
+
# count given or empty ?
|
130
|
+
count = args.shift if args.first.to_s =~ /^\d*$/
|
131
|
+
num_processes = count.to_i unless count.to_s.empty?
|
132
|
+
num_processes ||= ENV['PARALLEL_TEST_PROCESSORS'].to_i if ENV['PARALLEL_TEST_PROCESSORS']
|
133
|
+
num_processes ||= Parallel.processor_count
|
134
134
|
|
135
|
-
|
135
|
+
pattern = args.shift
|
136
136
|
|
137
|
-
|
138
|
-
end
|
137
|
+
[num_processes.to_i, pattern.to_s]
|
138
|
+
end
|
139
139
|
|
140
140
|
# finds all tests and partitions them into groups
|
141
|
-
def self.tests_in_groups(tests, num_groups, options)
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
141
|
+
def self.tests_in_groups(tests, num_groups, options)
|
142
|
+
if options[:no_sort]
|
143
|
+
Grouper.in_groups(tests, num_groups)
|
144
|
+
else
|
145
|
+
tests = with_runtime_info(tests)
|
146
|
+
Grouper.in_even_groups_by_size(tests, num_groups, options)
|
147
|
+
end
|
147
148
|
end
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
{:stdout => output, :exit_status => $?.exitstatus}
|
156
|
-
end
|
157
|
-
|
158
|
-
def self.find_results(test_output)
|
159
|
-
test_output.split("\n").map { |line|
|
160
|
-
line = line.gsub(/\.|F|\*/, '')
|
161
|
-
next unless line_is_result?(line)
|
162
|
-
line
|
163
|
-
}.compact
|
164
|
-
end
|
165
|
-
|
166
|
-
def self.test_env_number(process_number)
|
167
|
-
process_number == 0 ? '' : process_number + 1
|
168
|
-
end
|
169
|
-
|
170
|
-
def self.runtime_log
|
171
|
-
'tmp/parallelized_runtime_test.log'
|
172
|
-
end
|
173
|
-
|
174
|
-
def self.summarize_results(results)
|
175
|
-
results = results.join(' ').gsub(/s\b/, '') # combine and singularize results
|
176
|
-
counts = results.scan(/(\d+) (\w+)/)
|
177
|
-
sums = counts.inject(Hash.new(0)) do |sum, (number, word)|
|
178
|
-
sum[word] += number.to_i
|
179
|
-
sum
|
149
|
+
|
150
|
+
def self.execute_command(cmd, process_number, options)
|
151
|
+
cmd = "TEST_ENV_NUMBER=#{test_env_number(process_number)} ; export TEST_ENV_NUMBER; #{cmd}"
|
152
|
+
f = open("|#{cmd}", 'r')
|
153
|
+
output = fetch_output(f, options)
|
154
|
+
f.close
|
155
|
+
{:stdout => output, :exit_status => $?.exitstatus}
|
180
156
|
end
|
181
|
-
sums.sort.map { |word, number| "#{number} #{word}#{'s' if number != 1}" }.join(', ')
|
182
|
-
end
|
183
157
|
|
184
|
-
|
158
|
+
def self.find_results(test_output)
|
159
|
+
test_output.split("\n").map { |line|
|
160
|
+
line = line.gsub(/\.|F|\*/, '')
|
161
|
+
next unless line_is_result?(line)
|
162
|
+
line
|
163
|
+
}.compact
|
164
|
+
end
|
185
165
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
if flushed + timeout < now
|
201
|
-
print buffer
|
202
|
-
STDOUT.flush
|
203
|
-
buffer = ''
|
204
|
-
flushed = now
|
166
|
+
def self.test_env_number(process_number)
|
167
|
+
process_number == 0 ? '' : process_number + 1
|
168
|
+
end
|
169
|
+
|
170
|
+
def self.runtime_log
|
171
|
+
'tmp/parallelized_runtime_test.log'
|
172
|
+
end
|
173
|
+
|
174
|
+
def self.summarize_results(results)
|
175
|
+
results = results.join(' ').gsub(/s\b/, '') # combine and singularize results
|
176
|
+
counts = results.scan(/(\d+) (\w+)/)
|
177
|
+
sums = counts.inject(Hash.new(0)) do |sum, (number, word)|
|
178
|
+
sum[word] += number.to_i
|
179
|
+
sum
|
205
180
|
end
|
181
|
+
sums.sort.map { |word, number| "#{number} #{word}#{'s' if number != 1}" }.join(', ')
|
206
182
|
end
|
207
183
|
|
208
|
-
|
209
|
-
|
210
|
-
|
184
|
+
protected
|
185
|
+
|
186
|
+
# read output of the process and print in in chucks
|
187
|
+
def self.fetch_output(process, options)
|
188
|
+
all = ''
|
189
|
+
buffer = ''
|
190
|
+
timeout = options[:chunk_timeout] || 0.2
|
191
|
+
flushed = Time.now.to_f
|
192
|
+
|
193
|
+
while (char = process.getc)
|
194
|
+
char = (char.is_a?(Fixnum) ? char.chr : char) # 1.8 <-> 1.9
|
195
|
+
all << char
|
196
|
+
|
197
|
+
# print in chunks so large blocks stay together
|
198
|
+
now = Time.now.to_f
|
199
|
+
buffer << char
|
200
|
+
if flushed + timeout < now
|
201
|
+
print buffer
|
202
|
+
STDOUT.flush
|
203
|
+
buffer = ''
|
204
|
+
flushed = now
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# print the remainder
|
209
|
+
print buffer
|
210
|
+
STDOUT.flush
|
211
211
|
|
212
|
-
|
213
|
-
end
|
212
|
+
all
|
213
|
+
end
|
214
214
|
|
215
215
|
# copied from http://github.com/carlhuda/bundler Bundler::SharedHelpers#find_gemfile
|
216
|
-
def self.bundler_enabled?
|
217
|
-
|
216
|
+
def self.bundler_enabled?
|
217
|
+
return true if Object.const_defined?(:Bundler)
|
218
218
|
|
219
|
-
|
220
|
-
|
219
|
+
previous = nil
|
220
|
+
current = File.expand_path(Dir.pwd)
|
221
221
|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
222
|
+
until !File.directory?(current) || current == previous
|
223
|
+
filename = File.join(current, "Gemfile")
|
224
|
+
return true if File.exists?(filename)
|
225
|
+
current, previous = File.expand_path("..", current), current
|
226
|
+
end
|
227
227
|
|
228
|
-
|
229
|
-
end
|
228
|
+
false
|
229
|
+
end
|
230
230
|
|
231
|
-
def self.line_is_result?(line)
|
232
|
-
|
233
|
-
end
|
231
|
+
def self.line_is_result?(line)
|
232
|
+
line =~ /\d+ failure/
|
233
|
+
end
|
234
234
|
|
235
|
-
def self.with_runtime_info(tests)
|
236
|
-
|
235
|
+
def self.with_runtime_info(tests)
|
236
|
+
lines = File.read(runtime_log).split("\n") rescue []
|
237
|
+
|
238
|
+
# use recorded test runtime if we got enough data
|
239
|
+
if lines.size * 1.5 > tests.size
|
240
|
+
puts "Using recorded test runtime"
|
241
|
+
times = Hash.new(1)
|
242
|
+
lines.each do |line|
|
243
|
+
test, time = line.split(":")
|
244
|
+
next unless test and time
|
245
|
+
times[File.expand_path(test)] = time.to_f
|
246
|
+
end
|
247
|
+
tests.sort.map { |test| [test, times[test]] }
|
248
|
+
else # use file sizes
|
249
|
+
tests.sort.map { |test| [test, File.stat(test).size] }
|
250
|
+
end
|
251
|
+
end
|
237
252
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
253
|
+
def self.find_tests(root, options={})
|
254
|
+
if root.is_a?(Array)
|
255
|
+
root
|
256
|
+
else
|
257
|
+
# follow one symlink and direct children
|
258
|
+
# http://stackoverflow.com/questions/357754/can-i-traverse-symlinked-directories-in-ruby-with-a-glob
|
259
|
+
files = Dir["#{root}/**{,/*/**}/*#{test_suffix}"].uniq
|
260
|
+
files = files.map { |f| f.sub(root+'/', '') }
|
261
|
+
files = files.grep(/#{options[:pattern]}/)
|
262
|
+
files.map { |f| "/#{f}" }
|
246
263
|
end
|
247
|
-
tests.sort.map { |test| [test, times[test]] }
|
248
|
-
else # use file sizes
|
249
|
-
tests.sort.map { |test| [test, File.stat(test).size] }
|
250
264
|
end
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
265
|
+
|
266
|
+
def self.rerun()
|
267
|
+
rerun_failed_examples = false
|
268
|
+
|
269
|
+
if FileTest.exist?("#{RAILS_ROOT}/tmp/parallel_log/rspec.failures")
|
270
|
+
@error_count = %x{wc -l "#{FILENAME}"}.match(/\d/).to_s #counts the number of lines in the file
|
271
|
+
|
272
|
+
if @error_count.to_i > 1 && @error_count.to_i < 10
|
273
|
+
puts "rerunning #{@error_count} examples again"
|
274
|
+
@rerun_failures ||= []
|
275
|
+
@rerun_passes ||= []
|
276
|
+
|
277
|
+
File.open("#{FILENAME}").each_line do |l|
|
278
|
+
rerun_failed_examples = true
|
279
|
+
puts "#{l} will be ran and marked as a success if it passes"
|
280
|
+
result = %x[bundle exec rake spec #{l}]
|
281
|
+
rerun_status = result.match(/FAILED/).to_s
|
282
|
+
|
283
|
+
if rerun_status == "FAILED"
|
284
|
+
puts "the example failed again"
|
285
|
+
@rerun_failures << l
|
286
|
+
rerun_status = ""
|
287
|
+
else
|
288
|
+
puts "the example passed and is being marked as a success"
|
289
|
+
@rerun_passes << l
|
290
|
+
rerun_status = ""
|
291
|
+
end
|
292
|
+
end #end file loop
|
293
|
+
end
|
294
|
+
|
295
|
+
if rerun_failed_examples
|
296
|
+
if @rerun_failures.count > 0
|
297
|
+
puts "1 or more examples failed on rerun, rspec will mark this build as a failure"
|
298
|
+
#dump errors to file
|
299
|
+
exit(1)
|
300
|
+
elsif @rerun_passes.count >= @error_count.to_i
|
301
|
+
puts "all rerun examples passed, rspec will mark this build as passed"
|
302
|
+
$rerun_success = true
|
303
|
+
exit(0)
|
304
|
+
else
|
305
|
+
put "something unexpected happened not safe to mark the build as passed."
|
306
|
+
exit(1)
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
else
|
311
|
+
"No errors file was found, marking build as a success"
|
312
|
+
exit(0)
|
313
|
+
end
|
263
314
|
end
|
264
|
-
end
|
265
315
|
|
266
|
-
end
|
@@ -7,7 +7,7 @@ class ParallelizedSpecs::ExampleRerunFailuresLogger < ParallelizedSpecs::SpecLog
|
|
7
7
|
if example.location != nil
|
8
8
|
unless !!self.example_group.nested_descriptions.to_s.match(/shared/) || !!self.instance_variable_get(:@example_group).examples.last.location.match(/helper/)
|
9
9
|
@failed_examples ||= []
|
10
|
-
@failed_examples << "#{example.location.match(/spec.*\d/).to_s.chomp}"
|
10
|
+
@failed_examples << "#{example.location.match(/spec.*\d/).to_s.chomp} "
|
11
11
|
end
|
12
12
|
end
|
13
13
|
else
|
@@ -3,9 +3,7 @@ require 'parallelized_specs/spec_logger_base'
|
|
3
3
|
|
4
4
|
module RSpec
|
5
5
|
class ParallelizedSpecs::FailuresFormatter < ParallelizedSpecs::SpecLoggerBase
|
6
|
-
|
7
|
-
#env_test_number = 1 if ENV['TEST_ENV_NUMBER'].nil
|
8
|
-
FILENAME = "#{RAILS_ROOT}/rspec.failures"
|
6
|
+
FILENAME = "#{RAILS_ROOT}/tmp/parallel_log/rspec.failures"
|
9
7
|
|
10
8
|
def example_failed(example, counter, failure)
|
11
9
|
@rerun_examples ||= []
|
@@ -15,41 +13,6 @@ module RSpec
|
|
15
13
|
end
|
16
14
|
|
17
15
|
def dump_summary(*args)
|
18
|
-
rerun_failed_examples = false
|
19
|
-
@rerun_failures ||= []
|
20
|
-
@rerun_passes ||= []
|
21
|
-
@error_count = %x{wc -l "#{FILENAME}"}.match(/\d/).to_s #counts the number of lines in the file
|
22
|
-
|
23
|
-
if @error_count.to_i > 1 && @error_count.to_i < 10
|
24
|
-
@output.puts "rerunning #{@error_count} examples again"
|
25
|
-
File.open("#{FILENAME}").each_line do |l|
|
26
|
-
rerun_failed_examples = true
|
27
|
-
@output.puts "#{l} will be ran and marked as a success if it passes"
|
28
|
-
result = %x[bundle exec rake spec #{l}]
|
29
|
-
rerun_status = result.match(/FAILED/).to_s
|
30
|
-
|
31
|
-
if rerun_status == "FAILED"
|
32
|
-
@output.puts "the example failed again"
|
33
|
-
@rerun_failures << l
|
34
|
-
rerun_status = ""
|
35
|
-
else
|
36
|
-
@output.puts "the example passed and is being marked as a success"
|
37
|
-
@rerun_passes << l
|
38
|
-
rerun_status = ""
|
39
|
-
end
|
40
|
-
end #end file loop
|
41
|
-
end
|
42
|
-
|
43
|
-
if rerun_failed_examples
|
44
|
-
if @rerun_failures.length > 0
|
45
|
-
@output.puts "1 or more examples failed on rerun, rspec will mark this build as a failure"
|
46
|
-
else
|
47
|
-
@output.puts "all rerun examples passed, rspec will mark this build as passed"
|
48
|
-
$rerun_success = true
|
49
|
-
Spec::Runner.options.instance_variable_get(:@reporter).instance_variable_get(:@failures).delete_if { |item| item != 'b' } #placeholder delete all failures in array approach
|
50
|
-
end
|
51
|
-
else
|
52
|
-
end
|
53
16
|
end
|
54
17
|
|
55
18
|
def dump_failures(*args)
|
@@ -44,9 +44,14 @@ namespace :parallel do
|
|
44
44
|
end
|
45
45
|
|
46
46
|
desc "run spec in parallel with parallel:spec[num_cpus]"
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
47
|
+
task 'spec', :count, :pattern, :options, :arguments do |t, args|
|
48
|
+
count, pattern = ParallelizedSpecs.parse_rake_args(args)
|
49
|
+
opts = {:count => count, :pattern => pattern, :root => Rails.root, :files => args[:arguments]}
|
50
|
+
ParallelizedSpecs.execute_parallel_specs(opts)
|
51
|
+
end
|
52
|
+
|
53
|
+
desc "reruns all the failed specs in 1 thread"
|
54
|
+
task 'rerun' do
|
55
|
+
ParallelizedSpecs.rerun()
|
51
56
|
end
|
52
57
|
end
|
data/parallelized_specs.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "parallelized_specs"
|
8
|
-
s.version = "0.3.
|
8
|
+
s.version = "0.3.24"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Jake Sorce, Bryan Madsen, Shawn Meredith"]
|
12
|
-
s.date = "2012-10-
|
12
|
+
s.date = "2012-10-30"
|
13
13
|
s.email = "jake@instructure.com"
|
14
14
|
s.files = [
|
15
15
|
"Gemfile",
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: parallelized_specs
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 35
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 3
|
9
|
-
-
|
10
|
-
version: 0.3.
|
9
|
+
- 24
|
10
|
+
version: 0.3.24
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Jake Sorce, Bryan Madsen, Shawn Meredith
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2012-10-
|
18
|
+
date: 2012-10-30 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: parallel
|