parallel_tests 1.1.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
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