parallel_tests 0.13.3 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. data/Gemfile +1 -0
  2. data/Gemfile.lock +8 -2
  3. data/Rakefile +5 -1
  4. data/Readme.md +5 -2
  5. data/bin/parallel_spinach +5 -0
  6. data/lib/parallel_tests.rb +40 -38
  7. data/lib/parallel_tests/cli.rb +2 -2
  8. data/lib/parallel_tests/cucumber/failures_logger.rb +2 -2
  9. data/lib/parallel_tests/cucumber/runner.rb +5 -88
  10. data/lib/parallel_tests/{cucumber → gherkin}/io.rb +1 -1
  11. data/lib/parallel_tests/{cucumber/gherkin_listener.rb → gherkin/listener.rb} +2 -2
  12. data/lib/parallel_tests/gherkin/runner.rb +102 -0
  13. data/lib/parallel_tests/{cucumber → gherkin}/runtime_logger.rb +2 -2
  14. data/lib/parallel_tests/grouper.rb +41 -41
  15. data/lib/parallel_tests/rspec/failures_logger.rb +1 -1
  16. data/lib/parallel_tests/rspec/runner.rb +50 -48
  17. data/lib/parallel_tests/spinach/runner.rb +19 -0
  18. data/lib/parallel_tests/tasks.rb +7 -3
  19. data/lib/parallel_tests/test/runner.rb +125 -123
  20. data/lib/parallel_tests/test/runtime_logger.rb +57 -53
  21. data/lib/parallel_tests/version.rb +1 -1
  22. data/parallel_tests.gemspec +2 -2
  23. data/spec/integration_spec.rb +61 -0
  24. data/spec/parallel_tests/cucumber/failure_logger_spec.rb +1 -1
  25. data/spec/parallel_tests/cucumber/runner_spec.rb +5 -172
  26. data/spec/parallel_tests/{cucumber/gherkin_listener_spec.rb → gherkin/listener_spec.rb} +3 -3
  27. data/spec/parallel_tests/gherkin/runner_behaviour.rb +177 -0
  28. data/spec/parallel_tests/rspec/{failure_logger_spec.rb → failures_logger_spec.rb} +0 -0
  29. data/spec/parallel_tests/spinach/runner_spec.rb +12 -0
  30. data/spec/parallel_tests/test/runtime_logger_spec.rb +1 -1
  31. data/spec/parallel_tests_spec.rb +2 -2
  32. data/spec/spec_helper.rb +1 -1
  33. metadata +16 -10
@@ -1,7 +1,7 @@
1
- require 'parallel_tests/cucumber/io'
1
+ require 'parallel_tests/gherkin/io'
2
2
 
3
3
  module ParallelTests
4
- module Cucumber
4
+ module Gherkin
5
5
  class RuntimeLogger
6
6
  include Io
7
7
 
@@ -1,56 +1,56 @@
1
1
  module ParallelTests
2
2
  class Grouper
3
- def self.in_even_groups_by_size(items_with_sizes, num_groups, options = {})
4
- groups = Array.new(num_groups) { {:items => [], :size => 0} }
5
-
6
- # add all files that should run in a single process to one group
7
- (options[:single_process] || []).each do |pattern|
8
- matched, items_with_sizes = items_with_sizes.partition { |item, size| item =~ pattern }
9
- matched.each { |item, size| add_to_group(groups.first, item, size) }
3
+ class << self
4
+ def by_steps(tests, num_groups, options)
5
+ features_with_steps = build_features_with_steps(tests, options)
6
+ in_even_groups_by_size(features_with_steps, num_groups)
10
7
  end
11
8
 
12
- groups_to_fill = (options[:isolate] ? groups[1..-1] : groups)
9
+ def in_even_groups_by_size(items_with_sizes, num_groups, options = {})
10
+ groups = Array.new(num_groups) { {:items => [], :size => 0} }
13
11
 
14
- # add all other files
15
- largest_first(items_with_sizes).each do |item, size|
16
- smallest = smallest_group(groups_to_fill)
17
- add_to_group(smallest, item, size)
18
- end
12
+ # add all files that should run in a single process to one group
13
+ (options[:single_process] || []).each do |pattern|
14
+ matched, items_with_sizes = items_with_sizes.partition { |item, size| item =~ pattern }
15
+ matched.each { |item, size| add_to_group(groups.first, item, size) }
16
+ end
19
17
 
20
- groups.map!{|g| g[:items].sort }
21
- end
18
+ groups_to_fill = (options[:isolate] ? groups[1..-1] : groups)
22
19
 
23
- def self.largest_first(files)
24
- files.sort_by{|item, size| size }.reverse
25
- end
20
+ # add all other files
21
+ largest_first(items_with_sizes).each do |item, size|
22
+ smallest = smallest_group(groups_to_fill)
23
+ add_to_group(smallest, item, size)
24
+ end
26
25
 
27
- private
26
+ groups.map!{|g| g[:items].sort }
27
+ end
28
28
 
29
- def self.smallest_group(groups)
30
- groups.min_by{|g| g[:size] }
31
- end
29
+ private
32
30
 
33
- def self.add_to_group(group, item, size)
34
- group[:items] << item
35
- group[:size] += size
36
- end
31
+ def largest_first(files)
32
+ files.sort_by{|item, size| size }.reverse
33
+ end
37
34
 
38
- def self.by_steps(tests, num_groups, options)
39
- features_with_steps = build_features_with_steps(tests, options)
40
- in_even_groups_by_size(features_with_steps, num_groups)
41
- end
35
+ def smallest_group(groups)
36
+ groups.min_by{|g| g[:size] }
37
+ end
42
38
 
43
- private
44
-
45
- def self.build_features_with_steps(tests, options)
46
- require 'parallel_tests/cucumber/gherkin_listener'
47
- listener = Cucumber::GherkinListener.new
48
- listener.ignore_tag_pattern = Regexp.compile(options[:ignore_tag_pattern]) if options[:ignore_tag_pattern]
49
- parser = Gherkin::Parser::Parser.new(listener, true, 'root')
50
- tests.each{|file|
51
- parser.parse(File.read(file), file, 0)
52
- }
53
- listener.collect.sort_by{|_,value| -value }
39
+ def add_to_group(group, item, size)
40
+ group[:items] << item
41
+ group[:size] += size
42
+ end
43
+
44
+ def build_features_with_steps(tests, options)
45
+ require 'parallel_tests/gherkin/listener'
46
+ listener = ParallelTests::Gherkin::Listener.new
47
+ listener.ignore_tag_pattern = Regexp.compile(options[:ignore_tag_pattern]) if options[:ignore_tag_pattern]
48
+ parser = ::Gherkin::Parser::Parser.new(listener, true, 'root')
49
+ tests.each{|file|
50
+ parser.parse(File.read(file), file, 0)
51
+ }
52
+ listener.collect.sort_by{|_,value| -value }
53
+ end
54
54
  end
55
55
  end
56
56
  end
@@ -38,7 +38,7 @@ class ParallelTests::RSpec::FailuresLogger < ParallelTests::RSpec::LoggerBase
38
38
  file, line = example.location.to_s.split(':')
39
39
  next unless file and line
40
40
  file.gsub!(%r(^.*?/spec/), './spec/')
41
- @output.puts "#{ParallelTests::RSpec::Runner.executable} #{file}:#{line} # #{example.description}"
41
+ @output.puts "#{ParallelTests::RSpec::Runner.send(:executable)} #{file}:#{line} # #{example.description}"
42
42
  end
43
43
  end
44
44
  end
@@ -5,65 +5,67 @@ module ParallelTests
5
5
  class Runner < ParallelTests::Test::Runner
6
6
  NAME = 'RSpec'
7
7
 
8
- def self.run_tests(test_files, process_number, num_processes, options)
9
- exe = executable # expensive, so we cache
10
- version = (exe =~ /\brspec\b/ ? 2 : 1)
11
- cmd = [exe, options[:test_options], (rspec_2_color if version == 2), spec_opts, *test_files].compact.join(" ")
12
- options = options.merge(:env => rspec_1_color) if version == 1
13
- execute_command(cmd, process_number, num_processes, options)
14
- end
15
-
16
- def self.determine_executable
17
- cmd = case
18
- when File.exists?("bin/rspec")
19
- "bin/rspec"
20
- when File.file?("script/spec")
21
- "script/spec"
22
- when ParallelTests.bundler_enabled?
23
- cmd = (run("bundle show rspec-core") =~ %r{Could not find gem.*} ? "spec" : "rspec")
24
- "bundle exec #{cmd}"
25
- else
26
- %w[spec rspec].detect{|cmd| system "#{cmd} --version > /dev/null 2>&1" }
8
+ class << self
9
+ def run_tests(test_files, process_number, num_processes, options)
10
+ exe = executable # expensive, so we cache
11
+ version = (exe =~ /\brspec\b/ ? 2 : 1)
12
+ cmd = [exe, options[:test_options], (rspec_2_color if version == 2), spec_opts, *test_files].compact.join(" ")
13
+ options = options.merge(:env => rspec_1_color) if version == 1
14
+ execute_command(cmd, process_number, num_processes, options)
27
15
  end
28
16
 
29
- cmd or raise("Can't find executables rspec or spec")
30
- end
17
+ def determine_executable
18
+ cmd = case
19
+ when File.exists?("bin/rspec")
20
+ "bin/rspec"
21
+ when File.file?("script/spec")
22
+ "script/spec"
23
+ when ParallelTests.bundler_enabled?
24
+ cmd = (run("bundle show rspec-core") =~ %r{Could not find gem.*} ? "spec" : "rspec")
25
+ "bundle exec #{cmd}"
26
+ else
27
+ %w[spec rspec].detect{|cmd| system "#{cmd} --version > /dev/null 2>&1" }
28
+ end
31
29
 
32
- def self.runtime_log
33
- 'tmp/parallel_runtime_rspec.log'
34
- end
30
+ cmd or raise("Can't find executables rspec or spec")
31
+ end
35
32
 
36
- def self.test_file_name
37
- "spec"
38
- end
33
+ def runtime_log
34
+ 'tmp/parallel_runtime_rspec.log'
35
+ end
39
36
 
40
- def self.test_suffix
41
- "_spec.rb"
42
- end
37
+ def test_file_name
38
+ "spec"
39
+ end
43
40
 
44
- private
41
+ def test_suffix
42
+ "_spec.rb"
43
+ end
45
44
 
46
- # so it can be stubbed....
47
- def self.run(cmd)
48
- `#{cmd}`
49
- end
45
+ private
50
46
 
51
- def self.rspec_1_color
52
- if $stdout.tty?
53
- {'RSPEC_COLOR' => "1"}
54
- else
55
- {}
47
+ # so it can be stubbed....
48
+ def run(cmd)
49
+ `#{cmd}`
56
50
  end
57
- end
58
51
 
59
- def self.rspec_2_color
60
- '--color --tty' if $stdout.tty?
61
- end
52
+ def rspec_1_color
53
+ if $stdout.tty?
54
+ {'RSPEC_COLOR' => "1"}
55
+ else
56
+ {}
57
+ end
58
+ end
59
+
60
+ def rspec_2_color
61
+ '--color --tty' if $stdout.tty?
62
+ end
62
63
 
63
- def self.spec_opts
64
- options_file = ['.rspec_parallel', 'spec/parallel_spec.opts', 'spec/spec.opts'].detect{|f| File.file?(f) }
65
- return unless options_file
66
- "-O #{options_file}"
64
+ def spec_opts
65
+ options_file = ['.rspec_parallel', 'spec/parallel_spec.opts', 'spec/spec.opts'].detect{|f| File.file?(f) }
66
+ return unless options_file
67
+ "-O #{options_file}"
68
+ end
67
69
  end
68
70
  end
69
71
  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
@@ -111,7 +111,7 @@ namespace :parallel do
111
111
  ParallelTests::Tasks.run_in_parallel("RAILS_ENV=#{ParallelTests::Tasks.rails_env} rake #{args.command}")
112
112
  end
113
113
 
114
- ['test', 'spec', 'features'].each do |type|
114
+ ['test', 'spec', 'features', 'features-spinach'].each do |type|
115
115
  desc "run #{type} in parallel with parallel:#{type}[num_cpus]"
116
116
  task type, [:count, :pattern, :options] do |t, args|
117
117
  ParallelTests::Tasks.check_for_pending_migrations
@@ -123,15 +123,19 @@ namespace :parallel do
123
123
  test_framework = {
124
124
  'spec' => 'rspec',
125
125
  'test' => 'test',
126
- 'features' => 'cucumber'
126
+ 'features' => 'cucumber',
127
+ 'features-spinach' => 'spinach',
127
128
  }[type]
128
129
 
130
+ if test_framework == 'spinach'
131
+ type = 'features'
132
+ end
129
133
  executable = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'parallel_test')
134
+
130
135
  command = "#{executable} #{type} --type #{test_framework} " \
131
136
  "-n #{count} " \
132
137
  "--pattern '#{pattern}' " \
133
138
  "--test-options '#{options}'"
134
-
135
139
  abort unless system(command) # allow to chain tasks e.g. rake parallel:spec parallel:features
136
140
  end
137
141
  end
@@ -5,160 +5,162 @@ module ParallelTests
5
5
  class Runner
6
6
  NAME = 'Test'
7
7
 
8
- # --- usually overwritten by other runners
8
+ class << self
9
+ # --- usually overwritten by other runners
9
10
 
10
- def self.name
11
- NAME
12
- end
11
+ def name
12
+ NAME
13
+ end
13
14
 
14
- def self.runtime_log
15
- 'tmp/parallel_runtime_test.log'
16
- end
15
+ def runtime_log
16
+ 'tmp/parallel_runtime_test.log'
17
+ end
17
18
 
18
- def self.test_suffix
19
- "_test.rb"
20
- end
19
+ def test_suffix
20
+ "_test.rb"
21
+ end
21
22
 
22
- def self.test_file_name
23
- "test"
24
- end
23
+ def test_file_name
24
+ "test"
25
+ end
25
26
 
26
- def self.run_tests(test_files, process_number, num_processes, options)
27
- require_list = test_files.map { |filename| %{"#{File.expand_path filename}"} }.join(",")
28
- cmd = "#{executable} -Itest -e '[#{require_list}].each {|f| require f }' -- #{options[:test_options]}"
29
- execute_command(cmd, process_number, num_processes, options)
30
- end
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
31
32
 
32
- def self.line_is_result?(line)
33
- line =~ /\d+ failure/
34
- end
33
+ def line_is_result?(line)
34
+ line =~ /\d+ failure/
35
+ end
35
36
 
36
- # --- usually used by other runners
37
+ # --- usually used by other runners
37
38
 
38
- # finds all tests and partitions them into groups
39
- def self.tests_in_groups(tests, num_groups, options={})
40
- tests = find_tests(tests, options)
39
+ # finds all tests and partitions them into groups
40
+ def tests_in_groups(tests, num_groups, options={})
41
+ tests = find_tests(tests, options)
41
42
 
42
- tests = if options[:group_by] == :found
43
- tests.map { |t| [t, 1] }
44
- else
45
- with_runtime_info(tests)
43
+ tests = if options[:group_by] == :found
44
+ tests.map { |t| [t, 1] }
45
+ else
46
+ with_runtime_info(tests)
47
+ end
48
+ Grouper.in_even_groups_by_size(tests, num_groups, options)
46
49
  end
47
- Grouper.in_even_groups_by_size(tests, num_groups, options)
48
- end
49
50
 
50
- def self.execute_command(cmd, process_number, num_processes, options)
51
- env = (options[:env] || {}).merge(
52
- "TEST_ENV_NUMBER" => test_env_number(process_number),
53
- "PARALLEL_TEST_GROUPS" => num_processes
54
- )
55
- cmd = "nice #{cmd}" if options[:nice]
56
- execute_command_and_capture_output(env, cmd, options[:serialize_stdout])
57
- end
51
+ def execute_command(cmd, process_number, num_processes, options)
52
+ env = (options[:env] || {}).merge(
53
+ "TEST_ENV_NUMBER" => test_env_number(process_number),
54
+ "PARALLEL_TEST_GROUPS" => num_processes
55
+ )
56
+ cmd = "nice #{cmd}" if options[:nice]
57
+ execute_command_and_capture_output(env, cmd, options[:serialize_stdout])
58
+ end
58
59
 
59
- def self.execute_command_and_capture_output(env, cmd, silence)
60
- # make processes descriptive / visible in ps -ef
61
- exports = env.map do |k,v|
62
- "#{k}=#{v};export #{k}"
63
- end.join(";")
64
- cmd = "#{exports};#{cmd}"
60
+ def execute_command_and_capture_output(env, cmd, silence)
61
+ # make processes descriptive / visible in ps -ef
62
+ exports = env.map do |k,v|
63
+ "#{k}=#{v};export #{k}"
64
+ end.join(";")
65
+ cmd = "#{exports};#{cmd}"
65
66
 
66
- output = open("|#{cmd}", "r") { |output| capture_output(output, silence) }
67
- exitstatus = $?.exitstatus
67
+ output = open("|#{cmd}", "r") { |output| capture_output(output, silence) }
68
+ exitstatus = $?.exitstatus
68
69
 
69
- {:stdout => output, :exit_status => exitstatus}
70
- end
70
+ {:stdout => output, :exit_status => exitstatus}
71
+ end
71
72
 
72
- def self.find_results(test_output)
73
- test_output.split("\n").map {|line|
74
- line = line.gsub(/\.|F|\*/,'').gsub(/\e\[\d+m/,'')
75
- next unless line_is_result?(line)
76
- line
77
- }.compact
78
- end
73
+ def find_results(test_output)
74
+ test_output.split("\n").map {|line|
75
+ line = line.gsub(/\.|F|\*/,'').gsub(/\e\[\d+m/,'')
76
+ next unless line_is_result?(line)
77
+ line
78
+ }.compact
79
+ end
79
80
 
80
- def self.test_env_number(process_number)
81
- process_number == 0 ? '' : process_number + 1
82
- end
81
+ def test_env_number(process_number)
82
+ process_number == 0 ? '' : process_number + 1
83
+ end
83
84
 
84
- def self.summarize_results(results)
85
- sums = sum_up_results(results)
86
- sums.sort.map{|word, number| "#{number} #{word}#{'s' if number != 1}" }.join(', ')
87
- end
85
+ def summarize_results(results)
86
+ sums = sum_up_results(results)
87
+ sums.sort.map{|word, number| "#{number} #{word}#{'s' if number != 1}" }.join(', ')
88
+ end
88
89
 
89
- protected
90
+ protected
90
91
 
91
- def self.executable
92
- ENV['PARALLEL_TESTS_EXECUTABLE'] || determine_executable
93
- end
92
+ def executable
93
+ ENV['PARALLEL_TESTS_EXECUTABLE'] || determine_executable
94
+ end
94
95
 
95
- def self.determine_executable
96
- "ruby"
97
- end
96
+ def determine_executable
97
+ "ruby"
98
+ end
98
99
 
99
- def self.sum_up_results(results)
100
- results = results.join(' ').gsub(/s\b/,'') # combine and singularize results
101
- counts = results.scan(/(\d+) (\w+)/)
102
- counts.inject(Hash.new(0)) do |sum, (number, word)|
103
- sum[word] += number.to_i
104
- sum
100
+ def sum_up_results(results)
101
+ results = results.join(' ').gsub(/s\b/,'') # combine and singularize results
102
+ counts = results.scan(/(\d+) (\w+)/)
103
+ counts.inject(Hash.new(0)) do |sum, (number, word)|
104
+ sum[word] += number.to_i
105
+ sum
106
+ end
105
107
  end
106
- end
107
108
 
108
- # read output of the process and print it in chunks
109
- def self.capture_output(out, silence)
110
- result = ""
111
- loop do
112
- begin
113
- read = out.readpartial(1000000) # read whatever chunk we can get
114
- result << read
115
- unless silence
116
- $stdout.print read
117
- $stdout.flush
109
+ # read output of the process and print it in chunks
110
+ def capture_output(out, silence)
111
+ result = ""
112
+ loop do
113
+ begin
114
+ read = out.readpartial(1000000) # read whatever chunk we can get
115
+ result << read
116
+ unless silence
117
+ $stdout.print read
118
+ $stdout.flush
119
+ end
118
120
  end
119
- end
120
- end rescue EOFError
121
- result
122
- end
121
+ end rescue EOFError
122
+ result
123
+ end
123
124
 
124
- def self.with_runtime_info(tests)
125
- lines = File.read(runtime_log).split("\n") rescue []
126
-
127
- # use recorded test runtime if we got enough data
128
- if lines.size * 1.5 > tests.size
129
- puts "Using recorded test runtime"
130
- times = Hash.new(1)
131
- lines.each do |line|
132
- test, time = line.split(":")
133
- next unless test and time
134
- times[File.expand_path(test)] = time.to_f
125
+ def with_runtime_info(tests)
126
+ lines = File.read(runtime_log).split("\n") rescue []
127
+
128
+ # use recorded test runtime if we got enough data
129
+ if lines.size * 1.5 > tests.size
130
+ puts "Using recorded test runtime"
131
+ times = Hash.new(1)
132
+ lines.each do |line|
133
+ test, time = line.split(":")
134
+ next unless test and time
135
+ times[File.expand_path(test)] = time.to_f
136
+ end
137
+ tests.sort.map{|test| [test, times[File.expand_path(test)]] }
138
+ else # use file sizes
139
+ tests.sort.map{|test| [test, File.stat(test).size] }
135
140
  end
136
- tests.sort.map{|test| [test, times[File.expand_path(test)]] }
137
- else # use file sizes
138
- tests.sort.map{|test| [test, File.stat(test).size] }
139
141
  end
140
- end
141
142
 
142
- def self.find_tests(tests, options = {})
143
- (tests || []).map do |file_or_folder|
144
- if File.directory?(file_or_folder)
145
- files = files_in_folder(file_or_folder, options)
146
- files.grep(/#{Regexp.escape test_suffix}$/).grep(options[:pattern]||//)
143
+ def find_tests(tests, options = {})
144
+ (tests || []).map do |file_or_folder|
145
+ if File.directory?(file_or_folder)
146
+ files = files_in_folder(file_or_folder, options)
147
+ files.grep(/#{Regexp.escape test_suffix}$/).grep(options[:pattern]||//)
148
+ else
149
+ file_or_folder
150
+ end
151
+ end.flatten.uniq
152
+ end
153
+
154
+ def files_in_folder(folder, options={})
155
+ pattern = if options[:symlinks] == false # not nil or true
156
+ "**/*"
147
157
  else
148
- file_or_folder
158
+ # follow one symlink and direct children
159
+ # http://stackoverflow.com/questions/357754/can-i-traverse-symlinked-directories-in-ruby-with-a-glob
160
+ "**{,/*/**}/*"
149
161
  end
150
- end.flatten.uniq
151
- end
152
-
153
- def self.files_in_folder(folder, options={})
154
- pattern = if options[:symlinks] == false # not nil or true
155
- "**/*"
156
- else
157
- # follow one symlink and direct children
158
- # http://stackoverflow.com/questions/357754/can-i-traverse-symlinked-directories-in-ruby-with-a-glob
159
- "**{,/*/**}/*"
162
+ Dir[File.join(folder, pattern)].uniq
160
163
  end
161
- Dir[File.join(folder, pattern)].uniq
162
164
  end
163
165
  end
164
166
  end