parallel_tests 1.1.0 → 1.1.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.
- checksums.yaml +4 -4
- data/lib/parallel_tests/cli.rb +207 -0
- data/lib/parallel_tests/cucumber/failures_logger.rb +25 -0
- data/lib/parallel_tests/cucumber/runner.rb +37 -0
- data/lib/parallel_tests/cucumber/scenario_line_logger.rb +51 -0
- data/lib/parallel_tests/cucumber/scenarios.rb +34 -0
- data/lib/parallel_tests/gherkin/io.rb +41 -0
- data/lib/parallel_tests/gherkin/listener.rb +87 -0
- data/lib/parallel_tests/gherkin/runner.rb +116 -0
- data/lib/parallel_tests/gherkin/runtime_logger.rb +28 -0
- data/lib/parallel_tests/grouper.rb +73 -0
- data/lib/parallel_tests/railtie.rb +8 -0
- data/lib/parallel_tests/rspec/failures_logger.rb +54 -0
- data/lib/parallel_tests/rspec/logger_base.rb +55 -0
- data/lib/parallel_tests/rspec/runner.rb +73 -0
- data/lib/parallel_tests/rspec/runtime_logger.rb +59 -0
- data/lib/parallel_tests/rspec/summary_logger.rb +19 -0
- data/lib/parallel_tests/spinach/runner.rb +19 -0
- data/lib/parallel_tests/tasks.rb +157 -0
- data/lib/parallel_tests/test/runner.rb +187 -0
- data/lib/parallel_tests/test/runtime_logger.rb +98 -0
- data/lib/parallel_tests/version.rb +3 -0
- metadata +22 -1
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'parallel_tests/rspec/failures_logger'
|
2
|
+
|
3
|
+
class ParallelTests::RSpec::SummaryLogger < ParallelTests::RSpec::LoggerBase
|
4
|
+
if RSPEC_3
|
5
|
+
RSpec::Core::Formatters.register self, :dump_failures
|
6
|
+
end
|
7
|
+
|
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
|
17
|
+
end
|
18
|
+
end
|
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
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
module ParallelTests
|
4
|
+
module Tasks
|
5
|
+
class << self
|
6
|
+
def rails_env
|
7
|
+
ENV['RAILS_ENV'] || 'test'
|
8
|
+
end
|
9
|
+
|
10
|
+
def run_in_parallel(cmd, options={})
|
11
|
+
count = " -n #{options[:count]}" unless options[:count].to_s.empty?
|
12
|
+
executable = File.expand_path("../../../bin/parallel_test", __FILE__)
|
13
|
+
command = "#{executable} --exec '#{cmd}'#{count}#{' --non-parallel' if options[:non_parallel]}"
|
14
|
+
abort unless system(command)
|
15
|
+
end
|
16
|
+
|
17
|
+
# this is a crazy-complex solution for a very simple problem:
|
18
|
+
# removing certain lines from the output without chaning the exit-status
|
19
|
+
# normally I'd not do this, but it has been lots of fun and a great learning experience :)
|
20
|
+
#
|
21
|
+
# - sed does not support | without -r
|
22
|
+
# - grep changes 0 exitstatus to 1 if nothing matches
|
23
|
+
# - sed changes 1 exitstatus to 0
|
24
|
+
# - pipefail makes pipe fail with exitstatus of first failed command
|
25
|
+
# - pipefail is not supported in (zsh)
|
26
|
+
# - defining a new rake task like silence_schema would force users to load parallel_tests in test env
|
27
|
+
# - do not use ' since run_in_parallel uses them to quote stuff
|
28
|
+
# - simple system "set -o pipefail" returns nil even though set -o pipefail exists with 0
|
29
|
+
def suppress_output(command, ignore_regex)
|
30
|
+
activate_pipefail = "set -o pipefail"
|
31
|
+
remove_ignored_lines = %Q{(grep -v "#{ignore_regex}" || test 1)}
|
32
|
+
|
33
|
+
if File.executable?('/bin/bash') && system('/bin/bash', '-c', "#{activate_pipefail} 2>/dev/null && test 1")
|
34
|
+
# We need to shell escape single quotes (' becomes '"'"') because
|
35
|
+
# run_in_parallel wraps command in single quotes
|
36
|
+
%Q{/bin/bash -c '"'"'#{activate_pipefail} && (#{command}) | #{remove_ignored_lines}'"'"'}
|
37
|
+
else
|
38
|
+
command
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def check_for_pending_migrations
|
43
|
+
["db:abort_if_pending_migrations", "app:db:abort_if_pending_migrations"].each do |abort_migrations|
|
44
|
+
if Rake::Task.task_defined?(abort_migrations)
|
45
|
+
Rake::Task[abort_migrations].invoke
|
46
|
+
break
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# parallel:spec[:count, :pattern, :options]
|
52
|
+
def parse_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
|
+
pattern = args.shift
|
62
|
+
options = args.shift
|
63
|
+
|
64
|
+
[num_processes, pattern.to_s, options.to_s]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
namespace :parallel do
|
71
|
+
desc "create test databases via db:create --> parallel:create[num_cpus]"
|
72
|
+
task :create, :count do |t,args|
|
73
|
+
ParallelTests::Tasks.run_in_parallel("rake db:create RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
|
74
|
+
end
|
75
|
+
|
76
|
+
desc "drop test databases via db:drop --> parallel:drop[num_cpus]"
|
77
|
+
task :drop, :count do |t,args|
|
78
|
+
ParallelTests::Tasks.run_in_parallel("rake db:drop RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
|
79
|
+
end
|
80
|
+
|
81
|
+
desc "update test databases by dumping and loading --> parallel:prepare[num_cpus]"
|
82
|
+
task(:prepare, [:count]) do |t,args|
|
83
|
+
ParallelTests::Tasks.check_for_pending_migrations
|
84
|
+
if defined?(ActiveRecord) && ActiveRecord::Base.schema_format == :ruby
|
85
|
+
# dump then load in parallel
|
86
|
+
Rake::Task['db:schema:dump'].invoke
|
87
|
+
Rake::Task['parallel:load_schema'].invoke(args[:count])
|
88
|
+
else
|
89
|
+
# there is no separate dump / load for schema_format :sql -> do it safe and slow
|
90
|
+
args = args.to_hash.merge(:non_parallel => true) # normal merge returns nil
|
91
|
+
taskname = Rake::Task.task_defined?('db:test:prepare') ? 'db:test:prepare' : 'app:db:test:prepare'
|
92
|
+
ParallelTests::Tasks.run_in_parallel("rake #{taskname}", args)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# when dumping/resetting takes too long
|
97
|
+
desc "update test databases via db:migrate --> parallel:migrate[num_cpus]"
|
98
|
+
task :migrate, :count do |t,args|
|
99
|
+
ParallelTests::Tasks.run_in_parallel("rake db:migrate RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
|
100
|
+
end
|
101
|
+
|
102
|
+
# just load the schema (good for integration server <-> no development db)
|
103
|
+
desc "load dumped schema for test databases via db:schema:load --> parallel:load_schema[num_cpus]"
|
104
|
+
task :load_schema, :count do |t,args|
|
105
|
+
command = "rake db:schema:load RAILS_ENV=#{ParallelTests::Tasks.rails_env}"
|
106
|
+
ParallelTests::Tasks.run_in_parallel(ParallelTests::Tasks.suppress_output(command, "^ ->\\|^-- "), args)
|
107
|
+
end
|
108
|
+
|
109
|
+
# load the structure from the structure.sql file
|
110
|
+
desc "load structure for test databases via db:structure:load --> parallel:load_structure[num_cpus]"
|
111
|
+
task :load_structure, :count do |t,args|
|
112
|
+
ParallelTests::Tasks.run_in_parallel("rake db:structure:load RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
|
113
|
+
end
|
114
|
+
|
115
|
+
desc "load the seed data from db/seeds.rb via db:seed --> parallel:seed[num_cpus]"
|
116
|
+
task :seed, :count do |t,args|
|
117
|
+
ParallelTests::Tasks.run_in_parallel("rake db:seed RAILS_ENV=#{ParallelTests::Tasks.rails_env}", args)
|
118
|
+
end
|
119
|
+
|
120
|
+
desc "launch given rake command in parallel"
|
121
|
+
task :rake, :command do |t, args|
|
122
|
+
ParallelTests::Tasks.run_in_parallel("RAILS_ENV=#{ParallelTests::Tasks.rails_env} rake #{args.command}")
|
123
|
+
end
|
124
|
+
|
125
|
+
['test', 'spec', 'features', 'features-spinach'].each do |type|
|
126
|
+
desc "run #{type} in parallel with parallel:#{type}[num_cpus]"
|
127
|
+
task type, [:count, :pattern, :options] do |t, args|
|
128
|
+
ParallelTests::Tasks.check_for_pending_migrations
|
129
|
+
|
130
|
+
$LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
131
|
+
require "parallel_tests"
|
132
|
+
|
133
|
+
count, pattern, options = ParallelTests::Tasks.parse_args(args)
|
134
|
+
test_framework = {
|
135
|
+
'spec' => 'rspec',
|
136
|
+
'test' => 'test',
|
137
|
+
'features' => 'cucumber',
|
138
|
+
'features-spinach' => 'spinach',
|
139
|
+
}[type]
|
140
|
+
|
141
|
+
if test_framework == 'spinach'
|
142
|
+
type = 'features'
|
143
|
+
end
|
144
|
+
executable = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'parallel_test')
|
145
|
+
|
146
|
+
command = "#{executable} #{type} --type #{test_framework} " \
|
147
|
+
"-n #{count} " \
|
148
|
+
"--pattern '#{pattern}' " \
|
149
|
+
"--test-options '#{options}'"
|
150
|
+
if ParallelTests::WINDOWS
|
151
|
+
ruby_binary = File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name'])
|
152
|
+
command = "#{ruby_binary} #{command}"
|
153
|
+
end
|
154
|
+
abort unless system(command) # allow to chain tasks e.g. rake parallel:spec parallel:features
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
require 'parallel_tests'
|
2
|
+
|
3
|
+
module ParallelTests
|
4
|
+
module Test
|
5
|
+
class Runner
|
6
|
+
NAME = 'Test'
|
7
|
+
|
8
|
+
class << self
|
9
|
+
# --- usually overwritten by other runners
|
10
|
+
|
11
|
+
def name
|
12
|
+
NAME
|
13
|
+
end
|
14
|
+
|
15
|
+
def runtime_log
|
16
|
+
'tmp/parallel_runtime_test.log'
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_suffix
|
20
|
+
/_(test|spec).rb$/
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_file_name
|
24
|
+
"test"
|
25
|
+
end
|
26
|
+
|
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]}"
|
30
|
+
execute_command(cmd, process_number, num_processes, options)
|
31
|
+
end
|
32
|
+
|
33
|
+
def line_is_result?(line)
|
34
|
+
line.gsub!(/[.F*]/,'')
|
35
|
+
line =~ /\d+ failure/
|
36
|
+
end
|
37
|
+
|
38
|
+
# --- usually used by other runners
|
39
|
+
|
40
|
+
# finds all tests and partitions them into groups
|
41
|
+
def tests_in_groups(tests, num_groups, options={})
|
42
|
+
tests = find_tests(tests, options)
|
43
|
+
|
44
|
+
tests = if options[:group_by] == :found
|
45
|
+
tests.map { |t| [t, 1] }
|
46
|
+
elsif options[:group_by] == :filesize
|
47
|
+
with_filesize_info(tests)
|
48
|
+
else
|
49
|
+
with_runtime_info(tests, options)
|
50
|
+
end
|
51
|
+
Grouper.in_even_groups_by_size(tests, num_groups, options)
|
52
|
+
end
|
53
|
+
|
54
|
+
def execute_command(cmd, process_number, num_processes, options)
|
55
|
+
env = (options[:env] || {}).merge(
|
56
|
+
"TEST_ENV_NUMBER" => test_env_number(process_number),
|
57
|
+
"PARALLEL_TEST_GROUPS" => num_processes
|
58
|
+
)
|
59
|
+
cmd = "nice #{cmd}" if options[:nice]
|
60
|
+
cmd = "#{cmd} 2>&1" if options[:combine_stderr]
|
61
|
+
puts cmd if options[:verbose]
|
62
|
+
|
63
|
+
execute_command_and_capture_output(env, cmd, options[:serialize_stdout])
|
64
|
+
end
|
65
|
+
|
66
|
+
def execute_command_and_capture_output(env, cmd, silence)
|
67
|
+
# make processes descriptive / visible in ps -ef
|
68
|
+
separator = (WINDOWS ? ' & ' : ';')
|
69
|
+
exports = env.map do |k,v|
|
70
|
+
if WINDOWS
|
71
|
+
"(SET \"#{k}=#{v}\")"
|
72
|
+
else
|
73
|
+
"#{k}=#{v};export #{k}"
|
74
|
+
end
|
75
|
+
end.join(separator)
|
76
|
+
cmd = "#{exports}#{separator}#{cmd}"
|
77
|
+
|
78
|
+
output = open("|#{cmd}", "r") { |output| capture_output(output, silence) }
|
79
|
+
exitstatus = $?.exitstatus
|
80
|
+
|
81
|
+
{:stdout => output, :exit_status => exitstatus}
|
82
|
+
end
|
83
|
+
|
84
|
+
def find_results(test_output)
|
85
|
+
test_output.split("\n").map {|line|
|
86
|
+
line.gsub!(/\e\[\d+m/,'')
|
87
|
+
next unless line_is_result?(line)
|
88
|
+
line
|
89
|
+
}.compact
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_env_number(process_number)
|
93
|
+
process_number == 0 ? '' : process_number + 1
|
94
|
+
end
|
95
|
+
|
96
|
+
def summarize_results(results)
|
97
|
+
sums = sum_up_results(results)
|
98
|
+
sums.sort.map{|word, number| "#{number} #{word}#{'s' if number != 1}" }.join(', ')
|
99
|
+
end
|
100
|
+
|
101
|
+
protected
|
102
|
+
|
103
|
+
def executable
|
104
|
+
ENV['PARALLEL_TESTS_EXECUTABLE'] || determine_executable
|
105
|
+
end
|
106
|
+
|
107
|
+
def determine_executable
|
108
|
+
"ruby"
|
109
|
+
end
|
110
|
+
|
111
|
+
def sum_up_results(results)
|
112
|
+
results = results.join(' ').gsub(/s\b/,'') # combine and singularize results
|
113
|
+
counts = results.scan(/(\d+) (\w+)/)
|
114
|
+
counts.inject(Hash.new(0)) do |sum, (number, word)|
|
115
|
+
sum[word] += number.to_i
|
116
|
+
sum
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# read output of the process and print it in chunks
|
121
|
+
def capture_output(out, silence)
|
122
|
+
result = ""
|
123
|
+
loop do
|
124
|
+
begin
|
125
|
+
read = out.readpartial(1000000) # read whatever chunk we can get
|
126
|
+
if Encoding.default_internal
|
127
|
+
read = read.force_encoding(Encoding.default_internal)
|
128
|
+
end
|
129
|
+
result << read
|
130
|
+
unless silence
|
131
|
+
$stdout.print read
|
132
|
+
$stdout.flush
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end rescue EOFError
|
136
|
+
result
|
137
|
+
end
|
138
|
+
|
139
|
+
def with_runtime_info(tests, options = {})
|
140
|
+
log = options[:runtime_log] || runtime_log
|
141
|
+
lines = File.read(log).split("\n") rescue []
|
142
|
+
|
143
|
+
# use recorded test runtime if we got enough data
|
144
|
+
if lines.size * 1.5 > tests.size
|
145
|
+
puts "Using recorded test runtime: #{log}"
|
146
|
+
times = Hash.new(1)
|
147
|
+
lines.each do |line|
|
148
|
+
test, time = line.split(":")
|
149
|
+
next unless test and time
|
150
|
+
times[File.expand_path(test)] = time.to_f
|
151
|
+
end
|
152
|
+
tests.sort.map{|test| [test, times[File.expand_path(test)]] }
|
153
|
+
else # use file sizes
|
154
|
+
with_filesize_info(tests)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def with_filesize_info(tests)
|
159
|
+
# use filesize to group files
|
160
|
+
tests.sort.map { |test| [test, File.stat(test).size] }
|
161
|
+
end
|
162
|
+
|
163
|
+
def find_tests(tests, options = {})
|
164
|
+
(tests || []).map do |file_or_folder|
|
165
|
+
if File.directory?(file_or_folder)
|
166
|
+
files = files_in_folder(file_or_folder, options)
|
167
|
+
files.grep(test_suffix).grep(options[:pattern]||//)
|
168
|
+
else
|
169
|
+
file_or_folder
|
170
|
+
end
|
171
|
+
end.flatten.uniq
|
172
|
+
end
|
173
|
+
|
174
|
+
def files_in_folder(folder, options={})
|
175
|
+
pattern = if options[:symlinks] == false # not nil or true
|
176
|
+
"**/*"
|
177
|
+
else
|
178
|
+
# follow one symlink and direct children
|
179
|
+
# http://stackoverflow.com/questions/357754/can-i-traverse-symlinked-directories-in-ruby-with-a-glob
|
180
|
+
"**{,/*/**}/*"
|
181
|
+
end
|
182
|
+
Dir[File.join(folder, pattern)].uniq
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'parallel_tests'
|
2
|
+
require 'parallel_tests/test/runner'
|
3
|
+
|
4
|
+
module ParallelTests
|
5
|
+
module Test
|
6
|
+
class RuntimeLogger
|
7
|
+
@@has_started = false
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def log(test, start_time, end_time)
|
11
|
+
return if test.is_a? ::Test::Unit::TestSuite # don't log for suites-of-suites
|
12
|
+
|
13
|
+
if !@@has_started # make empty log file
|
14
|
+
File.open(logfile, 'w'){}
|
15
|
+
@@has_started = true
|
16
|
+
end
|
17
|
+
|
18
|
+
locked_appending_to(logfile) do |file|
|
19
|
+
file.puts(message(test, start_time, end_time))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def message(test, start_time, end_time)
|
26
|
+
delta = "%.2f" % (end_time.to_f-start_time.to_f)
|
27
|
+
filename = class_directory(test.class) + class_to_filename(test.class) + ".rb"
|
28
|
+
"#{filename}:#{delta}"
|
29
|
+
end
|
30
|
+
|
31
|
+
# Note: this is a best guess at conventional test directory structure, and may need
|
32
|
+
# tweaking / post-processing to match correctly for any given project
|
33
|
+
def class_directory(suspect)
|
34
|
+
result = "test/"
|
35
|
+
|
36
|
+
if defined?(Rails)
|
37
|
+
result += case suspect.superclass.name
|
38
|
+
when "ActionDispatch::IntegrationTest"
|
39
|
+
"integration/"
|
40
|
+
when "ActionDispatch::PerformanceTest"
|
41
|
+
"performance/"
|
42
|
+
when "ActionController::TestCase"
|
43
|
+
"functional/"
|
44
|
+
when "ActionView::TestCase"
|
45
|
+
"unit/helpers/"
|
46
|
+
else
|
47
|
+
"unit/"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
result
|
51
|
+
end
|
52
|
+
|
53
|
+
# based on https://github.com/grosser/single_test/blob/master/lib/single_test.rb#L117
|
54
|
+
def class_to_filename(suspect)
|
55
|
+
word = suspect.to_s.dup
|
56
|
+
return word unless word.match /^[A-Z]/ and not word.match %r{/[a-z]}
|
57
|
+
|
58
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
59
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
60
|
+
word.gsub!(/\:\:/, '/')
|
61
|
+
word.tr!("-", "_")
|
62
|
+
word.downcase!
|
63
|
+
word
|
64
|
+
end
|
65
|
+
|
66
|
+
def locked_appending_to(file)
|
67
|
+
File.open(file, 'a') do |f|
|
68
|
+
begin
|
69
|
+
f.flock File::LOCK_EX
|
70
|
+
yield f
|
71
|
+
ensure
|
72
|
+
f.flock File::LOCK_UN
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def logfile
|
78
|
+
ParallelTests::Test::Runner.runtime_log
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
require 'test/unit/testsuite'
|
86
|
+
class ::Test::Unit::TestSuite
|
87
|
+
alias :run_without_timing :run unless defined? @@timing_installed
|
88
|
+
|
89
|
+
def run(result, &progress_block)
|
90
|
+
first_test = self.tests.first
|
91
|
+
start_time = ParallelTests.now
|
92
|
+
run_without_timing(result, &progress_block)
|
93
|
+
end_time = ParallelTests.now
|
94
|
+
ParallelTests::Test::RuntimeLogger.log(first_test, start_time, end_time)
|
95
|
+
end
|
96
|
+
|
97
|
+
@@timing_installed = true
|
98
|
+
end
|