parallelized_specs 0.3.23 → 0.3.24
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/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
|