parallel_tests 0.13.3 → 0.14.0

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 (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