vinted-parallel_tests 0.13.3 → 1.7.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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/Readme.md +86 -47
  3. data/bin/parallel_cucumber +5 -1
  4. data/bin/parallel_rspec +5 -1
  5. data/bin/parallel_spinach +9 -0
  6. data/bin/parallel_test +5 -1
  7. data/lib/parallel_tests.rb +15 -2
  8. data/lib/parallel_tests/cli.rb +115 -35
  9. data/lib/parallel_tests/cucumber/failures_logger.rb +9 -7
  10. data/lib/parallel_tests/cucumber/runner.rb +16 -77
  11. data/lib/parallel_tests/cucumber/scenario_line_logger.rb +52 -0
  12. data/lib/parallel_tests/cucumber/scenarios.rb +34 -0
  13. data/lib/parallel_tests/{cucumber → gherkin}/io.rb +1 -1
  14. data/lib/parallel_tests/{cucumber/gherkin_listener.rb → gherkin/listener.rb} +11 -6
  15. data/lib/parallel_tests/gherkin/runner.rb +116 -0
  16. data/lib/parallel_tests/{cucumber → gherkin}/runtime_logger.rb +2 -2
  17. data/lib/parallel_tests/grouper.rb +34 -17
  18. data/lib/parallel_tests/rspec/failures_logger.rb +22 -14
  19. data/lib/parallel_tests/rspec/logger_base.rb +5 -1
  20. data/lib/parallel_tests/rspec/runner.rb +5 -4
  21. data/lib/parallel_tests/rspec/runtime_logger.rb +10 -5
  22. data/lib/parallel_tests/rspec/summary_logger.rb +11 -11
  23. data/lib/parallel_tests/spinach/runner.rb +19 -0
  24. data/lib/parallel_tests/tasks.rb +41 -18
  25. data/lib/parallel_tests/test/runner.rb +80 -28
  26. data/lib/parallel_tests/test/runtime_logger.rb +86 -55
  27. data/lib/parallel_tests/version.rb +1 -1
  28. metadata +18 -35
  29. data/.gitignore +0 -2
  30. data/.rspec +0 -2
  31. data/.travis.yml +0 -6
  32. data/Gemfile +0 -8
  33. data/Gemfile.lock +0 -48
  34. data/Rakefile +0 -6
  35. data/ReadmeRails2.md +0 -48
  36. data/parallel_tests.gemspec +0 -14
  37. data/spec/integration_spec.rb +0 -285
  38. data/spec/parallel_tests/cli_spec.rb +0 -71
  39. data/spec/parallel_tests/cucumber/failure_logger_spec.rb +0 -43
  40. data/spec/parallel_tests/cucumber/gherkin_listener_spec.rb +0 -97
  41. data/spec/parallel_tests/cucumber/runner_spec.rb +0 -179
  42. data/spec/parallel_tests/grouper_spec.rb +0 -52
  43. data/spec/parallel_tests/rspec/failures_logger_spec.rb +0 -82
  44. data/spec/parallel_tests/rspec/runner_spec.rb +0 -187
  45. data/spec/parallel_tests/rspec/runtime_logger_spec.rb +0 -126
  46. data/spec/parallel_tests/rspec/summary_logger_spec.rb +0 -37
  47. data/spec/parallel_tests/tasks_spec.rb +0 -151
  48. data/spec/parallel_tests/test/runner_spec.rb +0 -413
  49. data/spec/parallel_tests/test/runtime_logger_spec.rb +0 -90
  50. data/spec/parallel_tests_spec.rb +0 -137
  51. data/spec/spec_helper.rb +0 -157
@@ -2,28 +2,36 @@ require 'parallel_tests/rspec/logger_base'
2
2
  require 'parallel_tests/rspec/runner'
3
3
 
4
4
  class ParallelTests::RSpec::FailuresLogger < ParallelTests::RSpec::LoggerBase
5
- # RSpec 1: does not keep track of failures, so we do
6
- def example_failed(example, *args)
7
- if RSPEC_1
8
- @failed_examples ||= []
9
- @failed_examples << example
10
- else
11
- super
5
+ if RSPEC_1
6
+ # RSpec 1: does not keep track of failures, so we do
7
+ def example_failed(example, *args)
8
+ if RSPEC_1
9
+ @failed_examples ||= []
10
+ @failed_examples << example
11
+ else
12
+ super
13
+ end
12
14
  end
13
- end
14
15
 
15
- # RSpec 1: dumps 1 failed spec
16
- def dump_failure(*args)
17
- end
18
-
19
- # RSpec 2: dumps all failed specs
20
- def dump_failures(*args)
16
+ def dump_failure(*args)
17
+ end
18
+ elsif RSPEC_2
19
+ def dump_failures(*args)
20
+ end
21
+ else
22
+ RSpec::Core::Formatters.register self, :dump_summary
21
23
  end
22
24
 
23
25
  def dump_summary(*args)
24
26
  lock_output do
25
27
  if RSPEC_1
26
28
  dump_commands_to_rerun_failed_examples_rspec_1
29
+ elsif RSPEC_3
30
+ notification = args.first
31
+ unless notification.failed_examples.empty?
32
+ colorizer = ::RSpec::Core::Formatters::ConsoleCodes
33
+ output.puts notification.colorized_rerun_commands(colorizer)
34
+ end
27
35
  else
28
36
  dump_commands_to_rerun_failed_examples
29
37
  end
@@ -15,6 +15,8 @@ ParallelTests::RSpec::LoggerBaseBase = base
15
15
 
16
16
  class ParallelTests::RSpec::LoggerBase < ParallelTests::RSpec::LoggerBaseBase
17
17
  RSPEC_1 = !defined?(RSpec::Core::Formatters::BaseTextFormatter) # do not test for Spec, this will trigger deprecation warning in rspec 2
18
+ RSPEC_2 = !RSPEC_1 && RSpec::Core::Version::STRING.start_with?('2')
19
+ RSPEC_3 = !RSPEC_1 && RSpec::Core::Version::STRING.start_with?('3')
18
20
 
19
21
  def initialize(*args)
20
22
  super
@@ -32,10 +34,12 @@ class ParallelTests::RSpec::LoggerBase < ParallelTests::RSpec::LoggerBaseBase
32
34
  end
33
35
 
34
36
  #stolen from Rspec
35
- def close
37
+ def close(*args)
36
38
  @output.close if (IO === @output) & (@output != $stdout)
37
39
  end
38
40
 
41
+ protected
42
+
39
43
  # do not let multiple processes get in each others way
40
44
  def lock_output
41
45
  if File === @output
@@ -3,6 +3,7 @@ require "parallel_tests/test/runner"
3
3
  module ParallelTests
4
4
  module RSpec
5
5
  class Runner < ParallelTests::Test::Runner
6
+ DEV_NULL = (WINDOWS ? "NUL" : "/dev/null")
6
7
  NAME = 'RSpec'
7
8
 
8
9
  class << self
@@ -16,15 +17,15 @@ module ParallelTests
16
17
 
17
18
  def determine_executable
18
19
  cmd = case
19
- when File.exists?("bin/rspec")
20
- "bin/rspec"
20
+ when File.exist?("bin/rspec")
21
+ WINDOWS ? "ruby bin/rspec" : "bin/rspec"
21
22
  when File.file?("script/spec")
22
23
  "script/spec"
23
24
  when ParallelTests.bundler_enabled?
24
25
  cmd = (run("bundle show rspec-core") =~ %r{Could not find gem.*} ? "spec" : "rspec")
25
26
  "bundle exec #{cmd}"
26
27
  else
27
- %w[spec rspec].detect{|cmd| system "#{cmd} --version > /dev/null 2>&1" }
28
+ %w[spec rspec].detect{|cmd| system "#{cmd} --version > #{DEV_NULL} 2>&1" }
28
29
  end
29
30
 
30
31
  cmd or raise("Can't find executables rspec or spec")
@@ -39,7 +40,7 @@ module ParallelTests
39
40
  end
40
41
 
41
42
  def test_suffix
42
- "_spec.rb"
43
+ /_spec\.rb$/
43
44
  end
44
45
 
45
46
  private
@@ -5,7 +5,11 @@ class ParallelTests::RSpec::RuntimeLogger < ParallelTests::RSpec::LoggerBase
5
5
  def initialize(*args)
6
6
  super
7
7
  @example_times = Hash.new(0)
8
- @group_nesting = 0 if !RSPEC_1
8
+ @group_nesting = 0 unless RSPEC_1
9
+ end
10
+
11
+ if RSPEC_3
12
+ RSpec::Core::Formatters.register self, :example_group_started, :example_group_finished, :start_dump
9
13
  end
10
14
 
11
15
  if RSPEC_1
@@ -26,12 +30,13 @@ class ParallelTests::RSpec::RuntimeLogger < ParallelTests::RSpec::LoggerBase
26
30
  super
27
31
  end
28
32
 
29
- def example_group_finished(example_group)
33
+ def example_group_finished(notification)
30
34
  @group_nesting -= 1
31
35
  if @group_nesting == 0
32
- @example_times[example_group.file_path] += ParallelTests.now - @time
36
+ path = (RSPEC_3 ? notification.group.file_path : notification.file_path)
37
+ @example_times[path] += ParallelTests.now - @time
33
38
  end
34
- super
39
+ super if defined?(super)
35
40
  end
36
41
  end
37
42
 
@@ -45,7 +50,7 @@ class ParallelTests::RSpec::RuntimeLogger < ParallelTests::RSpec::LoggerBase
45
50
  # TODO: Figure out why sometimes time can be less than 0
46
51
  lock_output do
47
52
  @example_times.each do |file, time|
48
- relative_path = file.sub(/^#{Regexp.escape Dir.pwd}\//,'')
53
+ relative_path = file.sub(/^#{Regexp.escape Dir.pwd}\//,'').sub(/^\.\//, "")
49
54
  @output.puts "#{relative_path}:#{time > 0 ? time : 0}"
50
55
  end
51
56
  end
@@ -1,19 +1,19 @@
1
1
  require 'parallel_tests/rspec/failures_logger'
2
2
 
3
3
  class ParallelTests::RSpec::SummaryLogger < ParallelTests::RSpec::LoggerBase
4
- # RSpec 1: dumps 1 failed spec
5
- def dump_failure(*args)
6
- lock_output do
7
- super
8
- end
9
- @output.flush
4
+ if RSPEC_3
5
+ RSpec::Core::Formatters.register self, :dump_failures
10
6
  end
11
7
 
12
- # RSpec 2: dumps all failed specs
13
- def dump_failures(*args)
14
- lock_output do
15
- super
8
+ if RSPEC_1
9
+ def dump_failure(*args)
10
+ lock_output { super }
11
+ @output.flush
12
+ end
13
+ else
14
+ def dump_failures(*args)
15
+ lock_output { super }
16
+ @output.flush
16
17
  end
17
- @output.flush
18
18
  end
19
19
  end
@@ -0,0 +1,19 @@
1
+ require "parallel_tests/gherkin/runner"
2
+
3
+ module ParallelTests
4
+ module Spinach
5
+ class Runner < ParallelTests::Gherkin::Runner
6
+ class << self
7
+ def name
8
+ 'spinach'
9
+ end
10
+
11
+ def runtime_logging
12
+ #Not Yet Supported
13
+ ""
14
+ end
15
+
16
+ end
17
+ end
18
+ end
19
+ end
@@ -7,8 +7,12 @@ module ParallelTests
7
7
  ENV['RAILS_ENV'] || 'test'
8
8
  end
9
9
 
10
+ def purge_before_load
11
+ "db:test:purge" if Gem::Version.new(Rails.version) > Gem::Version.new('4.2.0')
12
+ end
13
+
10
14
  def run_in_parallel(cmd, options={})
11
- count = " -n #{options[:count]}" if options[:count]
15
+ count = " -n #{options[:count]}" unless options[:count].to_s.empty?
12
16
  executable = File.expand_path("../../../bin/parallel_test", __FILE__)
13
17
  command = "#{executable} --exec '#{cmd}'#{count}#{' --non-parallel' if options[:non_parallel]}"
14
18
  command << " --advance-number #{options[:advance_number]}" if options[:advance_number]
@@ -31,17 +35,21 @@ module ParallelTests
31
35
  activate_pipefail = "set -o pipefail"
32
36
  remove_ignored_lines = %Q{(grep -v "#{ignore_regex}" || test 1)}
33
37
 
34
- if system("#{activate_pipefail} 2>/dev/null && test 1")
35
- "#{activate_pipefail} && (#{command}) | #{remove_ignored_lines}"
38
+ if File.executable?('/bin/bash') && system('/bin/bash', '-c', "#{activate_pipefail} 2>/dev/null && test 1")
39
+ # We need to shell escape single quotes (' becomes '"'"') because
40
+ # run_in_parallel wraps command in single quotes
41
+ %Q{/bin/bash -c '"'"'#{activate_pipefail} && (#{command}) | #{remove_ignored_lines}'"'"'}
36
42
  else
37
43
  command
38
44
  end
39
45
  end
40
46
 
41
47
  def check_for_pending_migrations
42
- abort_migrations = "db:abort_if_pending_migrations"
43
- if Rake::Task.task_defined?(abort_migrations)
44
- Rake::Task[abort_migrations].invoke
48
+ ["db:abort_if_pending_migrations", "app:db:abort_if_pending_migrations"].each do |abort_migrations|
49
+ if Rake::Task.task_defined?(abort_migrations)
50
+ Rake::Task[abort_migrations].invoke
51
+ break
52
+ end
45
53
  end
46
54
  end
47
55
 
@@ -66,17 +74,17 @@ end
66
74
 
67
75
  namespace :parallel do
68
76
  desc "create test databases via db:create --> parallel:create[num_cpus]"
69
- task :create, :count do |t,args|
77
+ task :create, :count do |_,args|
70
78
  ParallelTests::Tasks.run_in_parallel("rake db:create RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
71
79
  end
72
80
 
73
81
  desc "drop test databases via db:drop --> parallel:drop[num_cpus]"
74
- task :drop, :count do |t,args|
82
+ task :drop, :count do |_,args|
75
83
  ParallelTests::Tasks.run_in_parallel("rake db:drop RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
76
84
  end
77
85
 
78
86
  desc "update test databases by dumping and loading --> parallel:prepare[num_cpus]"
79
- task(:prepare, [:count]) do |t,args|
87
+ task(:prepare, [:count]) do |_,args|
80
88
  ParallelTests::Tasks.check_for_pending_migrations
81
89
  if defined?(ActiveRecord) && ActiveRecord::Base.schema_format == :ruby
82
90
  # dump then load in parallel
@@ -85,34 +93,41 @@ namespace :parallel do
85
93
  else
86
94
  # there is no separate dump / load for schema_format :sql -> do it safe and slow
87
95
  args = args.to_hash.merge(:non_parallel => true) # normal merge returns nil
88
- ParallelTests::Tasks.run_in_parallel('rake db:test:prepare --trace', args)
96
+ taskname = Rake::Task.task_defined?('db:test:prepare') ? 'db:test:prepare' : 'app:db:test:prepare'
97
+ ParallelTests::Tasks.run_in_parallel("rake #{taskname}", args)
89
98
  end
90
99
  end
91
100
 
92
101
  # when dumping/resetting takes too long
93
102
  desc "update test databases via db:migrate --> parallel:migrate[num_cpus]"
94
- task :migrate, :count do |t,args|
103
+ task :migrate, :count do |_,args|
95
104
  ParallelTests::Tasks.run_in_parallel("rake db:migrate RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
96
105
  end
97
106
 
98
107
  # just load the schema (good for integration server <-> no development db)
99
108
  desc "load dumped schema for test databases via db:schema:load --> parallel:load_schema[num_cpus]"
100
- task :load_schema, :count do |t,args|
101
- command = "rake db:schema:load RAILS_ENV=#{ParallelTests::Tasks.rails_env}"
109
+ task :load_schema, :count do |_,args|
110
+ command = "rake #{ParallelTests::Tasks.purge_before_load} db:schema:load RAILS_ENV=#{ParallelTests::Tasks.rails_env}"
102
111
  ParallelTests::Tasks.run_in_parallel(ParallelTests::Tasks.suppress_output(command, "^ ->\\|^-- "), args)
103
112
  end
104
113
 
114
+ # load the structure from the structure.sql file
115
+ desc "load structure for test databases via db:structure:load --> parallel:load_structure[num_cpus]"
116
+ task :load_structure, :count do |_,args|
117
+ ParallelTests::Tasks.run_in_parallel("rake #{ParallelTests::Tasks.purge_before_load} db:structure:load RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
118
+ end
119
+
105
120
  desc "load the seed data from db/seeds.rb via db:seed --> parallel:seed[num_cpus]"
106
- task :seed, :count do |t,args|
121
+ task :seed, :count do |_,args|
107
122
  ParallelTests::Tasks.run_in_parallel("rake db:seed RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
108
123
  end
109
124
 
110
125
  desc "launch given rake command in parallel"
111
- task :rake, :command do |t, args|
126
+ task :rake, :command do |_, args|
112
127
  ParallelTests::Tasks.run_in_parallel("RAILS_ENV=#{ParallelTests::Tasks.rails_env} rake #{args.command}")
113
128
  end
114
129
 
115
- ['test', 'spec', 'features'].each do |type|
130
+ ['test', 'spec', 'features', 'features-spinach'].each do |type|
116
131
  desc "run #{type} in parallel with parallel:#{type}[num_cpus]"
117
132
  task type, [:count, :pattern, :options] do |t, args|
118
133
  ParallelTests::Tasks.check_for_pending_migrations
@@ -124,15 +139,23 @@ namespace :parallel do
124
139
  test_framework = {
125
140
  'spec' => 'rspec',
126
141
  'test' => 'test',
127
- 'features' => 'cucumber'
142
+ 'features' => 'cucumber',
143
+ 'features-spinach' => 'spinach',
128
144
  }[type]
129
145
 
146
+ if test_framework == 'spinach'
147
+ type = 'features'
148
+ end
130
149
  executable = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'parallel_test')
150
+
131
151
  command = "#{executable} #{type} --type #{test_framework} " \
132
152
  "-n #{count} " \
133
153
  "--pattern '#{pattern}' " \
134
154
  "--test-options '#{options}'"
135
-
155
+ if ParallelTests::WINDOWS
156
+ ruby_binary = File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name'])
157
+ command = "#{ruby_binary} #{command}"
158
+ end
136
159
  abort unless system(command) # allow to chain tasks e.g. rake parallel:spec parallel:features
137
160
  end
138
161
  end
@@ -1,4 +1,4 @@
1
- require 'open3'
1
+ require 'parallel_tests'
2
2
 
3
3
  module ParallelTests
4
4
  module Test
@@ -17,7 +17,7 @@ module ParallelTests
17
17
  end
18
18
 
19
19
  def test_suffix
20
- "_test.rb"
20
+ /_(test|spec).rb$/
21
21
  end
22
22
 
23
23
  def test_file_name
@@ -25,12 +25,13 @@ module ParallelTests
25
25
  end
26
26
 
27
27
  def run_tests(test_files, process_number, num_processes, options)
28
- require_list = test_files.map { |filename| %{"#{File.expand_path filename}"} }.join(",")
29
- cmd = "#{executable} -Itest -e '[#{require_list}].each {|f| require f }' -- #{options[:test_options]}"
28
+ require_list = test_files.map { |file| file.sub(" ", "\\ ") }.join(" ")
29
+ cmd = "#{executable} -Itest -e '%w[#{require_list}].each { |f| require %{./\#{f}} }' -- #{options[:test_options]}"
30
30
  execute_command(cmd, process_number, num_processes, options)
31
31
  end
32
32
 
33
33
  def line_is_result?(line)
34
+ line.gsub!(/[.F*]/,'')
34
35
  line =~ /\d+ failure/
35
36
  end
36
37
 
@@ -38,31 +39,59 @@ module ParallelTests
38
39
 
39
40
  # finds all tests and partitions them into groups
40
41
  def tests_in_groups(tests, num_groups, options={})
42
+ tests = tests_with_size(tests, options)
43
+ Grouper.in_even_groups_by_size(tests, num_groups, options)
44
+ end
45
+
46
+ def tests_with_size(tests, options)
41
47
  tests = find_tests(tests, options)
42
48
 
43
- tests = if options[:group_by] == :found
44
- tests.map { |t| [t, 1] }
49
+ case options[:group_by]
50
+ when :found
51
+ tests.map! { |t| [t, 1] }
52
+ when :filesize
53
+ sort_by_filesize(tests)
54
+ when :runtime
55
+ sort_by_runtime(tests, runtimes(tests, options), options.merge(allowed_missing: 0.5))
56
+ when nil
57
+ # use recorded test runtime if we got enough data
58
+ runtimes = runtimes(tests, options) rescue []
59
+ if runtimes.size * 1.5 > tests.size
60
+ puts "Using recorded test runtime"
61
+ sort_by_runtime(tests, runtimes)
62
+ else
63
+ sort_by_filesize(tests)
64
+ end
45
65
  else
46
- with_runtime_info(tests)
66
+ raise ArgumentError, "Unsupported option #{options[:group_by]}"
47
67
  end
48
- Grouper.in_even_groups_by_size(tests, num_groups, options)
68
+
69
+ tests
49
70
  end
50
71
 
51
- def execute_command(cmd, process_number, num_processes, options)
72
+ def execute_command(cmd, process_number, num_processes, options)
52
73
  env = (options[:env] || {}).merge(
53
74
  "TEST_ENV_NUMBER" => test_env_number(process_number, options),
54
75
  "PARALLEL_TEST_GROUPS" => num_processes
55
76
  )
56
77
  cmd = "nice #{cmd}" if options[:nice]
78
+ cmd = "#{cmd} 2>&1" if options[:combine_stderr]
79
+ puts cmd if options[:verbose]
80
+
57
81
  execute_command_and_capture_output(env, cmd, options[:serialize_stdout])
58
82
  end
59
83
 
60
84
  def execute_command_and_capture_output(env, cmd, silence)
61
85
  # make processes descriptive / visible in ps -ef
86
+ separator = (WINDOWS ? ' & ' : ';')
62
87
  exports = env.map do |k,v|
63
- "#{k}=#{v};export #{k}"
64
- end.join(";")
65
- cmd = "#{exports};#{cmd}"
88
+ if WINDOWS
89
+ "(SET \"#{k}=#{v}\")"
90
+ else
91
+ "#{k}=#{v};export #{k}"
92
+ end
93
+ end.join(separator)
94
+ cmd = "#{exports}#{separator}#{cmd}"
66
95
 
67
96
  output = open("|#{cmd}", "r") { |output| capture_output(output, silence) }
68
97
  exitstatus = $?.exitstatus
@@ -72,7 +101,7 @@ module ParallelTests
72
101
 
73
102
  def find_results(test_output)
74
103
  test_output.split("\n").map {|line|
75
- line = line.gsub(/\.|F|\*/,'').gsub(/\e\[\d+m/,'')
104
+ line.gsub!(/\e\[\d+m/,'')
76
105
  next unless line_is_result?(line)
77
106
  line
78
107
  }.compact
@@ -113,6 +142,9 @@ module ParallelTests
113
142
  loop do
114
143
  begin
115
144
  read = out.readpartial(1000000) # read whatever chunk we can get
145
+ if Encoding.default_internal
146
+ read = read.force_encoding(Encoding.default_internal)
147
+ end
116
148
  result << read
117
149
  unless silence
118
150
  $stdout.print read
@@ -123,29 +155,49 @@ module ParallelTests
123
155
  result
124
156
  end
125
157
 
126
- def with_runtime_info(tests)
127
- lines = File.read(runtime_log).split("\n") rescue []
158
+ def sort_by_runtime(tests, runtimes, options={})
159
+ allowed_missing = options[:allowed_missing] || 1.0
160
+ allowed_missing = tests.size * allowed_missing
128
161
 
129
- # use recorded test runtime if we got enough data
130
- if lines.size * 1.5 > tests.size
131
- puts "Using recorded test runtime"
132
- times = Hash.new(1)
133
- lines.each do |line|
134
- test, time = line.split(":")
135
- next unless test and time
136
- times[File.expand_path(test)] = time.to_f
137
- end
138
- tests.sort.map{|test| [test, times[File.expand_path(test)]] }
139
- else # use file sizes
140
- tests.sort.map{|test| [test, File.stat(test).size] }
162
+ # set know runtime for each test
163
+ tests.sort!
164
+ tests.map! do |test|
165
+ allowed_missing -= 1 unless time = runtimes[test]
166
+ raise "Too little runtime info" if allowed_missing < 0
167
+ [test, time]
168
+ end
169
+
170
+ if options[:verbose]
171
+ puts "Runtime found for #{tests.count(&:last)} of #{tests.size} tests"
172
+ end
173
+
174
+ # fill gaps with unknown-runtime if given, average otherwise
175
+ known, unknown = tests.partition(&:last)
176
+ average = (known.any? ? known.map!(&:last).inject(:+) / known.size : 1)
177
+ unknown_runtime = options[:unknown_runtime] || average
178
+ unknown.each { |set| set[1] = unknown_runtime }
179
+ end
180
+
181
+ def runtimes(tests, options)
182
+ log = options[:runtime_log] || runtime_log
183
+ lines = File.read(log).split("\n")
184
+ lines.each_with_object({}) do |line, times|
185
+ test, time = line.split(":", 2)
186
+ next unless test and time
187
+ times[test] = time.to_f if tests.include?(test)
141
188
  end
142
189
  end
143
190
 
191
+ def sort_by_filesize(tests)
192
+ tests.sort!
193
+ tests.map! { |test| [test, File.stat(test).size] }
194
+ end
195
+
144
196
  def find_tests(tests, options = {})
145
197
  (tests || []).map do |file_or_folder|
146
198
  if File.directory?(file_or_folder)
147
199
  files = files_in_folder(file_or_folder, options)
148
- files.grep(/#{Regexp.escape test_suffix}$/).grep(options[:pattern]||//)
200
+ files.grep(options[:suffix]||test_suffix).grep(options[:pattern]||//)
149
201
  else
150
202
  file_or_folder
151
203
  end