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.
@@ -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