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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d378dd684c7e7a13876bc30b017c2d7d38a65238
4
- data.tar.gz: 15a61d401253bcff02d591f5d73345beb3244fec
3
+ metadata.gz: d9936778844bea92756c550ca9ca6c224406ce07
4
+ data.tar.gz: 2e410f1e0d4b8e76b5eb0558bfc5fa01117650ee
5
5
  SHA512:
6
- metadata.gz: 3a743c635bd8a247d26536bb163f1b5383edc75c5e4cc9ca923d774a81df3656cdac6cfd15ebab70ead6293e07b7b71633d90d71e8c687f9968b36ebfab6a0ad
7
- data.tar.gz: 788fd6fb129d348952fe7ed52314d19d573c59f420fbbe06a9a2b08937ca7dd50a454f267154a80325f4845b1c18d563382909b4ebf34985336f7ae3d6e05689
6
+ metadata.gz: eff8a7aa09428d7ac292d767b4ab79ff5a590c03e11d87e57764da3809bc933084d6ab34a23a848d519021e22fa220e33ff660f0f6eb681eb62c59c18153314a
7
+ data.tar.gz: aaf9d2d50339745e6c5f6a80d70000fd2b6f26f4ed36196a8dbc1e97b8e781e373195669cf127377d7264054d4988797076720c13e18a75f67e5ef165c279240
@@ -0,0 +1,207 @@
1
+ require 'optparse'
2
+ require 'tempfile'
3
+ require 'parallel_tests'
4
+
5
+ module ParallelTests
6
+ class CLI
7
+ def run(argv)
8
+ options = parse_options!(argv)
9
+
10
+ num_processes = ParallelTests.determine_number_of_processes(options[:count])
11
+ num_processes = num_processes * (options[:multiply] || 1)
12
+
13
+ if options[:execute]
14
+ execute_shell_command_in_parallel(options[:execute], num_processes, options)
15
+ else
16
+ run_tests_in_parallel(num_processes, options)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def execute_in_parallel(items, num_processes, options)
23
+ Tempfile.open 'parallel_tests-lock' do |lock|
24
+ return Parallel.map(items, :in_threads => num_processes) do |item|
25
+ result = yield(item)
26
+ report_output(result, lock) if options[:serialize_stdout]
27
+ result
28
+ end
29
+ end
30
+ end
31
+
32
+ def run_tests_in_parallel(num_processes, options)
33
+ test_results = nil
34
+
35
+ report_time_taken do
36
+ groups = @runner.tests_in_groups(options[:files], num_processes, options)
37
+
38
+ test_results = if options[:only_group]
39
+ groups_to_run = options[:only_group].collect{|i| groups[i - 1]}
40
+ report_number_of_tests(groups_to_run)
41
+ execute_in_parallel(groups_to_run, groups_to_run.size, options) do |group|
42
+ run_tests(group, groups_to_run.index(group), 1, options)
43
+ end
44
+ else
45
+ report_number_of_tests(groups)
46
+
47
+ execute_in_parallel(groups, groups.size, options) do |group|
48
+ run_tests(group, groups.index(group), num_processes, options)
49
+ end
50
+ end
51
+
52
+ report_results(test_results)
53
+ end
54
+
55
+ abort final_fail_message if any_test_failed?(test_results)
56
+ end
57
+
58
+ def run_tests(group, process_number, num_processes, options)
59
+ if group.empty?
60
+ {:stdout => '', :exit_status => 0}
61
+ else
62
+ @runner.run_tests(group, process_number, num_processes, options)
63
+ end
64
+ end
65
+
66
+ def report_output(result, lock)
67
+ lock.flock File::LOCK_EX
68
+ $stdout.puts result[:stdout]
69
+ $stdout.flush
70
+ ensure
71
+ lock.flock File::LOCK_UN
72
+ end
73
+
74
+ def report_results(test_results)
75
+ results = @runner.find_results(test_results.map { |result| result[:stdout] }*"")
76
+ puts ""
77
+ puts @runner.summarize_results(results)
78
+ end
79
+
80
+ def report_number_of_tests(groups)
81
+ name = @runner.test_file_name
82
+ num_processes = groups.size
83
+ num_tests = groups.map(&:size).inject(:+)
84
+ puts "#{num_processes} processes for #{num_tests} #{name}s, ~ #{num_tests / groups.size} #{name}s per process"
85
+ end
86
+
87
+ #exit with correct status code so rake parallel:test && echo 123 works
88
+ def any_test_failed?(test_results)
89
+ test_results.any? { |result| result[:exit_status] != 0 }
90
+ end
91
+
92
+ def parse_options!(argv)
93
+ options = {}
94
+ OptionParser.new do |opts|
95
+ opts.banner = <<-BANNER.gsub(/^ /, '')
96
+ Run all tests in parallel, giving each process ENV['TEST_ENV_NUMBER'] ('', '2', '3', ...)
97
+
98
+ [optional] Only run selected files & folders:
99
+ parallel_test test/bar test/baz/xxx_text.rb
100
+
101
+ Options are:
102
+ BANNER
103
+ opts.on("-n [PROCESSES]", Integer, "How many processes to use, default: available CPUs") { |n| options[:count] = n }
104
+ opts.on("-p", "--pattern [PATTERN]", "run tests matching this pattern") { |pattern| options[:pattern] = /#{pattern}/ }
105
+ opts.on("--group-by [TYPE]", <<-TEXT.gsub(/^ /, '')
106
+ group tests by:
107
+ found - order of finding files
108
+ steps - number of cucumber/spinach steps
109
+ scenarios - individual cucumber scenarios
110
+ filesize - by size of the file
111
+ default - runtime or filesize
112
+ TEXT
113
+ ) { |type| options[:group_by] = type.to_sym }
114
+ opts.on("-m [FLOAT]", "--multiply-processes [FLOAT]", Float, "use given number as a multiplier of processes to run") { |multiply| options[:multiply] = multiply }
115
+
116
+ opts.on("-s [PATTERN]", "--single [PATTERN]",
117
+ "Run all matching files in the same process") do |pattern|
118
+
119
+ options[:single_process] ||= []
120
+ options[:single_process] << /#{pattern}/
121
+ end
122
+
123
+ opts.on("-i", "--isolate",
124
+ "Do not run any other tests in the group used by --single(-s)") do |pattern|
125
+
126
+ options[:isolate] = true
127
+ end
128
+
129
+ opts.on("--only-group INT[, INT]", Array) { |groups| options[:only_group] = groups.map(&:to_i) }
130
+
131
+ opts.on("-e", "--exec [COMMAND]", "execute this code parallel and with ENV['TEST_ENV_NUM']") { |path| options[:execute] = path }
132
+ opts.on("-o", "--test-options '[OPTIONS]'", "execute test commands with those options") { |arg| options[:test_options] = arg }
133
+ opts.on("-t", "--type [TYPE]", "test(default) / rspec / cucumber / spinach") do |type|
134
+ begin
135
+ @runner = load_runner(type)
136
+ rescue NameError, LoadError => e
137
+ puts "Runner for `#{type}` type has not been found! (#{e})"
138
+ abort
139
+ end
140
+ end
141
+ opts.on("--serialize-stdout", "Serialize stdout output, nothing will be written until everything is done") { options[:serialize_stdout] = true }
142
+ opts.on("--combine-stderr", "Combine stderr into stdout, useful in conjunction with --serialize-stdout") { options[:combine_stderr] = true }
143
+ opts.on("--non-parallel", "execute same commands but do not in parallel, needs --exec") { options[:non_parallel] = true }
144
+ opts.on("--no-symlinks", "Do not traverse symbolic links to find test files") { options[:symlinks] = false }
145
+ opts.on('--ignore-tags [PATTERN]', 'When counting steps ignore scenarios with tags that match this pattern') { |arg| options[:ignore_tag_pattern] = arg }
146
+ opts.on("--nice", "execute test commands with low priority.") { options[:nice] = true }
147
+ opts.on("--runtime-log [PATH]", "Location of previously recorded test runtimes") { |path| options[:runtime_log] = path }
148
+ opts.on("--verbose", "Print more output") { options[:verbose] = true }
149
+ opts.on("-v", "--version", "Show Version") { puts ParallelTests::VERSION; exit }
150
+ opts.on("-h", "--help", "Show this.") { puts opts; exit }
151
+ end.parse!(argv)
152
+
153
+ if options[:count] == 0
154
+ options.delete(:count)
155
+ options[:non_parallel] = true
156
+ end
157
+
158
+ options[:files] = argv
159
+
160
+ options[:group_by] ||= :filesize if options[:only_group]
161
+
162
+ raise "--group-by found and --single-process are not supported" if options[:group_by] == :found and options[:single_process]
163
+ raise "--group-by filesize is required for --only-group" if options[:group_by] != :filesize and options[:only_group]
164
+
165
+ options
166
+ end
167
+
168
+ def load_runner(type)
169
+ require "parallel_tests/#{type}/runner"
170
+ runner_classname = type.split("_").map(&:capitalize).join.sub("Rspec", "RSpec")
171
+ klass_name = "ParallelTests::#{runner_classname}::Runner"
172
+ klass_name.split('::').inject(Object) { |x, y| x.const_get(y) }
173
+ end
174
+
175
+ def execute_shell_command_in_parallel(command, num_processes, options)
176
+ runs = (0...num_processes).to_a
177
+ results = if options[:non_parallel]
178
+ runs.map do |i|
179
+ ParallelTests::Test::Runner.execute_command(command, i, num_processes, options)
180
+ end
181
+ else
182
+ execute_in_parallel(runs, num_processes, options) do |i|
183
+ ParallelTests::Test::Runner.execute_command(command, i, num_processes, options)
184
+ end
185
+ end.flatten
186
+
187
+ abort if results.any? { |r| r[:exit_status] != 0 }
188
+ end
189
+
190
+ def report_time_taken
191
+ start = Time.now
192
+ yield
193
+ puts "\nTook #{Time.now - start} seconds"
194
+ end
195
+
196
+ def final_fail_message
197
+ fail_message = "#{@runner.name}s Failed"
198
+ fail_message = "\e[31m#{fail_message}\e[0m" if use_colors?
199
+
200
+ fail_message
201
+ end
202
+
203
+ def use_colors?
204
+ $stdout.tty?
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,25 @@
1
+ require 'cucumber/formatter/rerun'
2
+ require 'parallel_tests/gherkin/io'
3
+
4
+ module ParallelTests
5
+ module Cucumber
6
+ class FailuresLogger < ::Cucumber::Formatter::Rerun
7
+ include ParallelTests::Gherkin::Io
8
+
9
+ def initialize(runtime, path_or_io, options)
10
+ @io = prepare_io(path_or_io)
11
+ end
12
+
13
+ def after_feature(feature)
14
+ unless @lines.empty?
15
+ lock_output do
16
+ @lines.each do |line|
17
+ @io.puts "#{feature.file}:#{line}"
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,37 @@
1
+ require "parallel_tests/gherkin/runner"
2
+
3
+ module ParallelTests
4
+ module Cucumber
5
+ class Runner < ParallelTests::Gherkin::Runner
6
+ class << self
7
+ def name
8
+ 'cucumber'
9
+ end
10
+
11
+ def line_is_result?(line)
12
+ super or line =~ failing_scenario_regex
13
+ end
14
+
15
+ def summarize_results(results)
16
+ output = []
17
+
18
+ failing_scenarios = results.grep(failing_scenario_regex)
19
+ if failing_scenarios.any?
20
+ failing_scenarios.unshift("Failing Scenarios:")
21
+ output << failing_scenarios.join("\n")
22
+ end
23
+
24
+ output << super
25
+
26
+ output.join("\n\n")
27
+ end
28
+
29
+ private
30
+
31
+ def failing_scenario_regex
32
+ /^cucumber features\/.+:\d+/
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,51 @@
1
+ require 'gherkin/tag_expression'
2
+
3
+ module ParallelTests
4
+ module Cucumber
5
+ module Formatters
6
+ class ScenarioLineLogger
7
+ attr_reader :scenarios
8
+
9
+ def initialize(tag_expression = ::Gherkin::TagExpression.new([]))
10
+ @scenarios = []
11
+ @tag_expression = tag_expression
12
+ end
13
+
14
+ def visit_feature_element(feature_element)
15
+ return unless @tag_expression.evaluate(feature_element.source_tags)
16
+
17
+ case feature_element
18
+ when ::Cucumber::Ast::Scenario
19
+ line = if feature_element.respond_to?(:line)
20
+ feature_element.line
21
+ else
22
+ feature_element.instance_variable_get(:@line)
23
+ end
24
+ @scenarios << [feature_element.feature.file, line].join(":")
25
+ when ::Cucumber::Ast::ScenarioOutline
26
+ sections = feature_element.instance_variable_get(:@example_sections)
27
+ sections.each { |section|
28
+ rows = if section[1].respond_to?(:rows)
29
+ section[1].rows
30
+ else
31
+ section[1].instance_variable_get(:@rows)
32
+ end
33
+ rows.each_with_index { |row, index|
34
+ next if index == 0 # slices didn't work with jruby data structure
35
+ line = if row.respond_to?(:line)
36
+ row.line
37
+ else
38
+ row.instance_variable_get(:@line)
39
+ end
40
+ @scenarios << [feature_element.feature.file, line].join(":")
41
+ }
42
+ }
43
+ end
44
+ end
45
+
46
+ def method_missing(*args)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,34 @@
1
+ require 'gherkin/tag_expression'
2
+ require 'cucumber/runtime'
3
+ require 'cucumber'
4
+ require 'parallel_tests/cucumber/scenario_line_logger'
5
+ require 'parallel_tests/gherkin/listener'
6
+
7
+ module ParallelTests
8
+ module Cucumber
9
+ class Scenarios
10
+ class << self
11
+ def all(files, options={})
12
+ tags = []
13
+ tags.concat options[:ignore_tag_pattern].to_s.split(/\s*,\s*/).map {|tag| "~#{tag}" }
14
+ tags.concat options[:test_options].to_s.scan(/(?:-t|--tags) (~?@[\w,~@]+)/).flatten
15
+ split_into_scenarios files, tags.uniq
16
+ end
17
+
18
+ private
19
+
20
+ def split_into_scenarios(files, tags=[])
21
+ tag_expression = ::Gherkin::TagExpression.new(tags)
22
+ scenario_line_logger = ParallelTests::Cucumber::Formatters::ScenarioLineLogger.new(tag_expression)
23
+ loader = ::Cucumber::Runtime::FeaturesLoader.new(files, [], tag_expression)
24
+
25
+ loader.features.each do |feature|
26
+ feature.accept(scenario_line_logger)
27
+ end
28
+
29
+ scenario_line_logger.scenarios
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,41 @@
1
+ require 'parallel_tests'
2
+
3
+ module ParallelTests
4
+ module Gherkin
5
+ module Io
6
+
7
+ def prepare_io(path_or_io)
8
+ if path_or_io.respond_to?(:write)
9
+ path_or_io
10
+ else # its a path
11
+ File.open(path_or_io, 'w').close # clean out the file
12
+ file = File.open(path_or_io, 'a')
13
+
14
+ at_exit do
15
+ unless file.closed?
16
+ file.flush
17
+ file.close
18
+ end
19
+ end
20
+
21
+ file
22
+ end
23
+ end
24
+
25
+ # do not let multiple processes get in each others way
26
+ def lock_output
27
+ if File === @io
28
+ begin
29
+ @io.flock File::LOCK_EX
30
+ yield
31
+ ensure
32
+ @io.flock File::LOCK_UN
33
+ end
34
+ else
35
+ yield
36
+ end
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,87 @@
1
+ require 'gherkin'
2
+
3
+ module ParallelTests
4
+ module Gherkin
5
+ class Listener
6
+ attr_reader :collect
7
+
8
+ attr_writer :ignore_tag_pattern
9
+
10
+ def initialize
11
+ @steps, @uris = [], []
12
+ @collect = {}
13
+ reset_counters!
14
+ end
15
+
16
+ def feature(feature)
17
+ @feature = feature
18
+ end
19
+
20
+ def background(*args)
21
+ @background = 1
22
+ end
23
+
24
+ def scenario(scenario)
25
+ @outline = @background = 0
26
+ return if should_ignore(scenario)
27
+ @scenarios += 1
28
+ end
29
+
30
+ def scenario_outline(outline)
31
+ return if should_ignore(outline)
32
+ @outline = 1
33
+ end
34
+
35
+ def step(*args)
36
+ return if @ignoring
37
+ if @background == 1
38
+ @background_steps += 1
39
+ elsif @outline > 0
40
+ @outline_steps += 1
41
+ else
42
+ @collect[@uri] += 1
43
+ end
44
+ end
45
+
46
+ def uri(path)
47
+ @uri = path
48
+ @collect[@uri] = 0
49
+ end
50
+
51
+ #
52
+ # @param [Gherkin::Formatter::Model::Examples] examples
53
+ #
54
+ def examples(examples)
55
+ if examples.rows.size > 0
56
+ @collect[@uri] += (@outline_steps * examples.rows.size)
57
+ end
58
+ end
59
+
60
+ def eof(*args)
61
+ @collect[@uri] += (@background_steps * @scenarios)
62
+ reset_counters!
63
+ end
64
+
65
+ def reset_counters!
66
+ @outline = @outline_steps = @background = @background_steps = @scenarios = 0
67
+ @ignoring = nil
68
+ end
69
+
70
+ # ignore lots of other possible callbacks ...
71
+ def method_missing(*args)
72
+ end
73
+
74
+ private
75
+
76
+ # Return a combination of tags declared on this scenario/outline and the feature it belongs to
77
+ def all_tags(scenario)
78
+ (scenario.tags || []) + ((@feature && @feature.tags) || [])
79
+ end
80
+
81
+ # Set @ignoring if we should ignore this scenario/outline based on its tags
82
+ def should_ignore(scenario)
83
+ @ignoring = @ignore_tag_pattern && all_tags(scenario).find{ |tag| @ignore_tag_pattern === tag.name }
84
+ end
85
+ end
86
+ end
87
+ end