parallel_tests-instructure 0.6.16
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 +10 -0
- data/Gemfile.lock +30 -0
- data/Rakefile +20 -0
- data/Readme.md +240 -0
- data/VERSION +1 -0
- data/bin/parallel_cucumber +2 -0
- data/bin/parallel_spec +2 -0
- data/bin/parallel_test +97 -0
- data/lib/parallel_cucumber.rb +36 -0
- data/lib/parallel_cucumber/runtime_logger.rb +57 -0
- data/lib/parallel_specs.rb +52 -0
- data/lib/parallel_specs/spec_error_count_logger.rb +30 -0
- data/lib/parallel_specs/spec_error_logger.rb +45 -0
- data/lib/parallel_specs/spec_failures_logger.rb +43 -0
- data/lib/parallel_specs/spec_logger_base.rb +48 -0
- data/lib/parallel_specs/spec_runtime_logger.rb +34 -0
- data/lib/parallel_specs/spec_start_finish_logger.rb +38 -0
- data/lib/parallel_specs/spec_summary_logger.rb +19 -0
- data/lib/parallel_tests.rb +163 -0
- data/lib/parallel_tests/grouper.rb +49 -0
- data/lib/parallel_tests/railtie.rb +10 -0
- data/lib/parallel_tests/runtime_logger.rb +78 -0
- data/lib/parallel_tests/tasks.rb +80 -0
- data/lib/tasks/parallel_tests.rake +1 -0
- data/parallel_tests-instructure.gemspec +67 -0
- data/spec/integration_spec.rb +133 -0
- data/spec/parallel_cucumber_spec.rb +72 -0
- data/spec/parallel_specs/spec_failure_logger_spec.rb +82 -0
- data/spec/parallel_specs/spec_runtime_logger_spec.rb +76 -0
- data/spec/parallel_specs/spec_summary_logger_spec.rb +33 -0
- data/spec/parallel_specs_spec.rb +165 -0
- data/spec/parallel_tests/runtime_logger_spec.rb +74 -0
- data/spec/parallel_tests_spec.rb +229 -0
- data/spec/spec_helper.rb +149 -0
- metadata +115 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
class ParallelCucumber
|
2
|
+
class RuntimeLogger
|
3
|
+
|
4
|
+
def initialize(step_mother, path_or_io, options=nil)
|
5
|
+
@io = prepare_io(path_or_io)
|
6
|
+
@example_times = Hash.new(0)
|
7
|
+
end
|
8
|
+
|
9
|
+
def before_feature(_)
|
10
|
+
@start_at = Time.now.to_f
|
11
|
+
end
|
12
|
+
|
13
|
+
def after_feature(feature)
|
14
|
+
@example_times[feature.file] += Time.now.to_f - @start_at
|
15
|
+
end
|
16
|
+
|
17
|
+
def after_features(*args)
|
18
|
+
lock_output do
|
19
|
+
@io.puts @example_times.map { |file, time| "#{file}:#{time}" }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def prepare_io(path_or_io)
|
26
|
+
if path_or_io.respond_to?(:write)
|
27
|
+
path_or_io
|
28
|
+
else # its a path
|
29
|
+
File.open(path_or_io, 'w').close # clean out the file
|
30
|
+
file = File.open(path_or_io, 'a')
|
31
|
+
|
32
|
+
at_exit do
|
33
|
+
unless file.closed?
|
34
|
+
file.flush
|
35
|
+
file.close
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
file
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# do not let multiple processes get in each others way
|
44
|
+
def lock_output
|
45
|
+
if File === @io
|
46
|
+
begin
|
47
|
+
@io.flock File::LOCK_EX
|
48
|
+
yield
|
49
|
+
ensure
|
50
|
+
@io.flock File::LOCK_UN
|
51
|
+
end
|
52
|
+
else
|
53
|
+
yield
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'parallel_tests'
|
2
|
+
|
3
|
+
class ParallelSpecs < ParallelTests
|
4
|
+
def self.run_tests(test_files, process_number, options)
|
5
|
+
exe = executable # expensive, so we cache
|
6
|
+
version = (exe =~ /\brspec\b/ ? 2 : 1)
|
7
|
+
cmd = "#{rspec_1_color if version == 1}#{exe} #{options[:test_options]} #{rspec_2_color if version == 2}#{spec_opts(version)} #{test_files*' '}"
|
8
|
+
execute_command(cmd, process_number, options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.executable
|
12
|
+
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
|
20
|
+
cmd or raise("Can't find executables rspec or spec")
|
21
|
+
end
|
22
|
+
|
23
|
+
# legacy <-> people log to this file using rspec options
|
24
|
+
def self.runtime_log
|
25
|
+
'tmp/parallel_profile.log'
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
# so it can be stubbed....
|
31
|
+
def self.run(cmd)
|
32
|
+
`#{cmd}`
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.rspec_1_color
|
36
|
+
'RSPEC_COLOR=1 ; export RSPEC_COLOR ;' if $stdout.tty?
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.rspec_2_color
|
40
|
+
'--color --tty ' if $stdout.tty?
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.spec_opts(rspec_version)
|
44
|
+
options_file = ['spec/parallel_spec.opts', 'spec/spec.opts'].detect{|f| File.file?(f) }
|
45
|
+
return unless options_file
|
46
|
+
"-O #{options_file}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.test_suffix
|
50
|
+
"_spec.rb"
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'parallel_specs'
|
2
|
+
require File.join(File.dirname(__FILE__), 'spec_logger_base')
|
3
|
+
|
4
|
+
class ParallelSpecs::SpecErrorCountLogger < ParallelSpecs::SpecLoggerBase
|
5
|
+
def initialize(options, output=nil)
|
6
|
+
super
|
7
|
+
@passed_examples = []
|
8
|
+
@pending_examples = []
|
9
|
+
@failed_examples = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def example_passed(example)
|
13
|
+
@passed_examples << example
|
14
|
+
end
|
15
|
+
|
16
|
+
def example_pending(*args)
|
17
|
+
@pending_examples << args
|
18
|
+
end
|
19
|
+
|
20
|
+
def example_failed(example, count, failure)
|
21
|
+
@failed_examples << failure
|
22
|
+
end
|
23
|
+
|
24
|
+
def dump_summary(duration, example_count, failure_count, pending_count)
|
25
|
+
lock_output do
|
26
|
+
@output.puts "#{ @failed_examples.size }"
|
27
|
+
end
|
28
|
+
@output.flush
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'parallel_specs'
|
2
|
+
require File.join(File.dirname(__FILE__), 'spec_logger_base')
|
3
|
+
|
4
|
+
class ParallelSpecs::SpecErrorLogger < ParallelSpecs::SpecLoggerBase
|
5
|
+
def initialize(options, output=nil)
|
6
|
+
super
|
7
|
+
@passed_examples = []
|
8
|
+
@pending_examples = []
|
9
|
+
@failed_examples = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def example_passed(example)
|
13
|
+
@passed_examples << example
|
14
|
+
end
|
15
|
+
|
16
|
+
def example_pending(*args)
|
17
|
+
@pending_examples << args
|
18
|
+
end
|
19
|
+
|
20
|
+
def example_failed(example, count, failure)
|
21
|
+
@failed_examples << failure
|
22
|
+
end
|
23
|
+
|
24
|
+
def dump_summary(duration, example_count, failure_count, pending_count)
|
25
|
+
lock_output do
|
26
|
+
env_test_number = ENV['TEST_ENV_NUMBER']
|
27
|
+
env_test_number = 1 if ENV['TEST_ENV_NUMBER'].blank?
|
28
|
+
@output.puts ""
|
29
|
+
@output.puts ""
|
30
|
+
@output.puts "FOR TEST EXECUTOR #{env_test_number}: #{@failed_examples.size} failed, #{@passed_examples.size} passed:"
|
31
|
+
@failed_examples.each.with_index do | failure, i |
|
32
|
+
@output.puts ""
|
33
|
+
@output.puts "#{ i + 1 })"
|
34
|
+
@output.puts failure.header
|
35
|
+
unless failure.exception.nil?
|
36
|
+
@output.puts failure.exception.to_s
|
37
|
+
failure.exception.backtrace.each do | caller |
|
38
|
+
@output.puts caller
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
@output.flush
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'parallel_specs/spec_logger_base'
|
2
|
+
|
3
|
+
class ParallelSpecs::SpecFailuresLogger < ParallelSpecs::SpecLoggerBase
|
4
|
+
# RSpec 1: does not keep track of failures, so we do
|
5
|
+
def example_failed(example, *args)
|
6
|
+
if RSPEC_1
|
7
|
+
@failed_examples ||= []
|
8
|
+
@failed_examples << example
|
9
|
+
else
|
10
|
+
super
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# RSpec 1: dumps 1 failed spec
|
15
|
+
def dump_failure(*args)
|
16
|
+
end
|
17
|
+
|
18
|
+
# RSpec 2: dumps all failed specs
|
19
|
+
def dump_failures(*args)
|
20
|
+
end
|
21
|
+
|
22
|
+
def dump_summary(*args)
|
23
|
+
lock_output do
|
24
|
+
if RSPEC_1
|
25
|
+
dump_commands_to_rerun_failed_examples_rspec_1
|
26
|
+
else
|
27
|
+
dump_commands_to_rerun_failed_examples
|
28
|
+
end
|
29
|
+
end
|
30
|
+
@output.flush
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def dump_commands_to_rerun_failed_examples_rspec_1
|
36
|
+
(@failed_examples||[]).each do |example|
|
37
|
+
file, line = example.location.to_s.split(':')
|
38
|
+
next unless file and line
|
39
|
+
file.gsub!(%r(^.*?/spec/), './spec/')
|
40
|
+
@output.puts "#{ParallelSpecs.executable} #{file}:#{line} # #{example.description}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'parallel_specs'
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'rspec/core/formatters/base_text_formatter'
|
5
|
+
base = RSpec::Core::Formatters::BaseTextFormatter
|
6
|
+
rescue LoadError
|
7
|
+
require 'spec/runner/formatter/base_text_formatter'
|
8
|
+
base = Spec::Runner::Formatter::BaseTextFormatter
|
9
|
+
end
|
10
|
+
ParallelSpecs::SpecLoggerBaseBase = base
|
11
|
+
|
12
|
+
class ParallelSpecs::SpecLoggerBase < ParallelSpecs::SpecLoggerBaseBase
|
13
|
+
RSPEC_1 = !defined?(RSpec::Core::Formatters::BaseTextFormatter) # do not test for Spec, this will trigger deprecation warning in rspec 2
|
14
|
+
|
15
|
+
def initialize(*args)
|
16
|
+
super
|
17
|
+
|
18
|
+
@output ||= args[1] || args[0] # rspec 1 has output as second argument
|
19
|
+
|
20
|
+
if String === @output # a path ?
|
21
|
+
FileUtils.mkdir_p(File.dirname(@output))
|
22
|
+
File.open(@output, 'w'){} # overwrite previous results
|
23
|
+
@output = File.open(@output, 'a')
|
24
|
+
elsif File === @output # close and restart in append mode
|
25
|
+
@output.close
|
26
|
+
@output = File.open(@output.path, 'a')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
#stolen from Rspec
|
31
|
+
def close
|
32
|
+
@output.close if (IO === @output) & (@output != $stdout)
|
33
|
+
end
|
34
|
+
|
35
|
+
# do not let multiple processes get in each others way
|
36
|
+
def lock_output
|
37
|
+
if File === @output
|
38
|
+
begin
|
39
|
+
@output.flock File::LOCK_EX
|
40
|
+
yield
|
41
|
+
ensure
|
42
|
+
@output.flock File::LOCK_UN
|
43
|
+
end
|
44
|
+
else
|
45
|
+
yield
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'parallel_specs/spec_logger_base'
|
2
|
+
|
3
|
+
class ParallelSpecs::SpecRuntimeLogger < ParallelSpecs::SpecLoggerBase
|
4
|
+
def initialize(*args)
|
5
|
+
super
|
6
|
+
@example_times = Hash.new(0)
|
7
|
+
end
|
8
|
+
|
9
|
+
def example_started(*args)
|
10
|
+
@time = Time.now
|
11
|
+
end
|
12
|
+
|
13
|
+
def example_passed(example)
|
14
|
+
file = example.location.split(':').first
|
15
|
+
@example_times[file] += Time.now - @time
|
16
|
+
end
|
17
|
+
|
18
|
+
def dump_summary(*args);end
|
19
|
+
def dump_failures(*args);end
|
20
|
+
def dump_failure(*args);end
|
21
|
+
def dump_pending(*args);end
|
22
|
+
|
23
|
+
def start_dump(*args)
|
24
|
+
return unless ENV['TEST_ENV_NUMBER'] #only record when running in parallel
|
25
|
+
# TODO: Figure out why sometimes time can be less than 0
|
26
|
+
lock_output do
|
27
|
+
@example_times.each do |file, time|
|
28
|
+
relative_path = file.sub(/^#{Regexp.escape Dir.pwd}\//,'')
|
29
|
+
@output.puts "#{relative_path}:#{time > 0 ? time : 0}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
@output.flush
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'parallel_specs'
|
2
|
+
require File.join(File.dirname(__FILE__), 'spec_logger_base')
|
3
|
+
|
4
|
+
class ParallelSpecs::SpecStartFinishLogger < ParallelSpecs::SpecLoggerBase
|
5
|
+
def initialize(options, output=nil)
|
6
|
+
output ||= options # rspec 2 has output as first argument
|
7
|
+
|
8
|
+
output = "#{output}_#{ENV['TEST_ENV_NUMBER']}.log"
|
9
|
+
if String === output
|
10
|
+
FileUtils.mkdir_p(File.dirname(output))
|
11
|
+
File.open(output, 'w'){} # overwrite previous results
|
12
|
+
@output = File.open(output, 'a')
|
13
|
+
elsif File === output
|
14
|
+
output.close # close file opened with 'w'
|
15
|
+
@output = File.open(output.path, 'a')
|
16
|
+
else
|
17
|
+
@output = output
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def example_started(example)
|
22
|
+
@output.puts ""
|
23
|
+
@output.puts "started spec: #{example.description}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def example_passed(example)
|
27
|
+
@output.puts "finished spec: #{example.description}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def example_pending(example, message)
|
31
|
+
@output.puts "finished spec: #{example.description}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def example_failed(example)
|
35
|
+
@output.puts "finished spec: #{example.description}"
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'parallel_specs/spec_failures_logger'
|
2
|
+
|
3
|
+
class ParallelSpecs::SpecSummaryLogger < ParallelSpecs::SpecLoggerBase
|
4
|
+
# RSpec 1: dumps 1 failed spec
|
5
|
+
def dump_failure(*args)
|
6
|
+
lock_output do
|
7
|
+
super
|
8
|
+
end
|
9
|
+
@output.flush
|
10
|
+
end
|
11
|
+
|
12
|
+
# RSpec 2: dumps all failed specs
|
13
|
+
def dump_failures(*args)
|
14
|
+
lock_output do
|
15
|
+
super
|
16
|
+
end
|
17
|
+
@output.flush
|
18
|
+
end
|
19
|
+
end
|
@@ -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
|