parallel_cucumber 0.1.22 → 0.2.0.pre.36
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 +4 -4
- data/README.md +27 -7
- data/bin/parallel_cucumber +1 -1
- data/lib/parallel_cucumber.rb +10 -51
- data/lib/parallel_cucumber/cli.rb +169 -62
- data/lib/parallel_cucumber/helper/command.rb +87 -0
- data/lib/parallel_cucumber/helper/cucumber.rb +104 -0
- data/lib/parallel_cucumber/helper/multi_delegator.rb +24 -0
- data/lib/parallel_cucumber/helper/processes.rb +42 -0
- data/lib/parallel_cucumber/helper/queue.rb +38 -0
- data/lib/parallel_cucumber/helper/unittest/cucumber_test.rb +10 -0
- data/lib/parallel_cucumber/helper/utils.rb +13 -0
- data/lib/parallel_cucumber/logger.rb +15 -0
- data/lib/parallel_cucumber/main.rb +134 -0
- data/lib/parallel_cucumber/status.rb +8 -0
- data/lib/parallel_cucumber/version.rb +2 -2
- data/lib/parallel_cucumber/worker.rb +239 -0
- metadata +50 -14
- data/lib/parallel_cucumber/grouper.rb +0 -95
- data/lib/parallel_cucumber/result_formatter.rb +0 -72
- data/lib/parallel_cucumber/runner.rb +0 -121
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'English'
|
2
|
+
require 'erb'
|
3
|
+
require 'json'
|
4
|
+
require 'open3'
|
5
|
+
require 'tempfile'
|
6
|
+
require 'yaml'
|
7
|
+
|
8
|
+
module ParallelCucumber
|
9
|
+
module Helper
|
10
|
+
module Cucumber
|
11
|
+
class << self
|
12
|
+
def dry_run_report(options, args)
|
13
|
+
options = options.dup
|
14
|
+
options = expand_profiles(options) unless config_file.nil?
|
15
|
+
options = remove_formatters(options)
|
16
|
+
content = nil
|
17
|
+
|
18
|
+
Tempfile.open(%w(dry-run .json)) do |f|
|
19
|
+
dry_run_options = "--dry-run --format json --out #{f.path}"
|
20
|
+
|
21
|
+
cmd = "cucumber #{options} #{dry_run_options} #{args.join(' ')}"
|
22
|
+
_stdout, stderr, status = Open3.capture3(cmd)
|
23
|
+
f.close
|
24
|
+
|
25
|
+
unless status == 0
|
26
|
+
cmd = "bundle exec #{cmd}" if ENV['BUNDLE_BIN_PATH']
|
27
|
+
raise("Can't generate dry run report: #{status}:\n\t#{cmd}\n\t#{stderr}")
|
28
|
+
end
|
29
|
+
|
30
|
+
content = File.read(f.path)
|
31
|
+
end
|
32
|
+
content
|
33
|
+
end
|
34
|
+
|
35
|
+
def batch_mapped_files(options, batch, env)
|
36
|
+
options = options.dup
|
37
|
+
options = expand_profiles(options, env) unless config_file.nil?
|
38
|
+
file_map = {}
|
39
|
+
options.gsub!(/(?:\s|^)--dry-run\s+/, '')
|
40
|
+
options.gsub!(%r{((?:\s|^)(?:--out|-o))\s+((?:\S+\/)?(\S+))}) { "#{$1} #{file_map[$2] = "#{batch}/#{$3}"}" } # rubocop:disable Style/PerlBackrefs, Metrics/LineLength
|
41
|
+
[options, file_map]
|
42
|
+
end
|
43
|
+
|
44
|
+
def parse_json_report(json_report)
|
45
|
+
report = JSON.parse(json_report)
|
46
|
+
report.map do |feature|
|
47
|
+
next if feature['elements'].nil?
|
48
|
+
background = {}
|
49
|
+
feature['elements'].map do |scenario|
|
50
|
+
if scenario['type'] == 'background'
|
51
|
+
background = scenario
|
52
|
+
next
|
53
|
+
end
|
54
|
+
steps = [background['steps'], scenario['steps']].flatten.compact
|
55
|
+
status = case
|
56
|
+
when steps.map { |step| step['result'] }.all? { |result| result['status'] == 'skipped' }
|
57
|
+
Status::SKIPPED
|
58
|
+
when steps.map { |step| step['result'] }.any? { |result| result['status'] == 'failed' }
|
59
|
+
Status::FAILED
|
60
|
+
when steps.map { |step| step['result'] }.all? { |result| result['status'] == 'passed' }
|
61
|
+
Status::PASSED
|
62
|
+
when steps.map { |step| step['result'] }.any? { |result| result['status'] == 'undefined' }
|
63
|
+
Status::UNKNOWN
|
64
|
+
else
|
65
|
+
Status::UNKNOWN
|
66
|
+
end
|
67
|
+
{ "#{feature['uri']}:#{scenario['line']}".to_sym => status }
|
68
|
+
end
|
69
|
+
end.flatten.compact.inject(&:merge) || {}
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def expand_profiles(options, env = {})
|
75
|
+
e = ENV.to_h
|
76
|
+
ENV.replace(e.merge(env))
|
77
|
+
begin
|
78
|
+
config = YAML.load(ERB.new(File.read(config_file)).result)
|
79
|
+
_expand_profiles(options, config)
|
80
|
+
ensure
|
81
|
+
ENV.replace(e)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def config_file
|
86
|
+
Dir.glob('{,.config/,config/}cucumber{.yml,.yaml}').first
|
87
|
+
end
|
88
|
+
|
89
|
+
def _expand_profiles(options, config)
|
90
|
+
profiles = options.scan(/(?:^|\s)((?:--profile|-p)\s+[\S]+)/)
|
91
|
+
profiles.map(&:first).each do |profile|
|
92
|
+
option = profile.gsub(/(--profile|-p)\s+/, '')
|
93
|
+
options.gsub!(profile, _expand_profiles(config.fetch(option), config))
|
94
|
+
end
|
95
|
+
options.strip
|
96
|
+
end
|
97
|
+
|
98
|
+
def remove_formatters(options)
|
99
|
+
options.gsub(/(^|\s)(--format|-f|--out|-o)\s+[\S]+/, '\\1').gsub(/(\s|^)--dry-run\s+/, '\\1')
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# http://stackoverflow.com/questions/6407141/how-can-i-have-ruby-logger-log-output-to-stdout-as-well-as-file
|
2
|
+
# answered Jun 20 '11 at 11:03 jonas054
|
3
|
+
|
4
|
+
class MultiDelegator
|
5
|
+
def initialize(*targets)
|
6
|
+
@targets = targets
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.delegate(*methods)
|
10
|
+
methods.each do |m|
|
11
|
+
define_method(m) do |*args|
|
12
|
+
@targets.map { |t| t.send(m, *args) }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
class <<self
|
19
|
+
alias to new
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# log_file = File.open("debug.log", "a")
|
24
|
+
# log = Logger.new MultiDelegator.delegate(:write, :close).to(STDOUT, log_file)
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module ParallelCucumber
|
2
|
+
module Helper
|
3
|
+
module Processes
|
4
|
+
class << self
|
5
|
+
def ps_tree
|
6
|
+
` ps -ax -o ppid= -o pid= -o lstart= -o command= `
|
7
|
+
.each_line.map { |l| l.strip.split(/ +/, 3) }.to_a
|
8
|
+
.each_with_object({}) do |(ppid, pid, signature), tree|
|
9
|
+
(tree[pid] ||= { children: [] })[:signature] = signature
|
10
|
+
(tree[ppid] ||= { children: [] })[:children] << pid
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def kill_tree(sig, root, tree = nil, old_tree = nil)
|
15
|
+
descendants(root, tree, old_tree) do |pid|
|
16
|
+
begin
|
17
|
+
Process.kill(sig, pid.to_i)
|
18
|
+
rescue Errno::ESRCH
|
19
|
+
nil # It's gone already? Hurrah!
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def all_pids_dead?(root, tree = nil, old_tree = nil)
|
25
|
+
# Note: returns from THIS function as well as descendants: short-circuit evaluation.
|
26
|
+
descendants(root, tree, old_tree) { return false }
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
# Walks old_tree, and yields all processes (alive or dead) that match the pid, start time, and command in
|
31
|
+
# the new tree. Note that this will fumble children created since old_tree was created, but this thing is
|
32
|
+
# riddled with race conditions anyway.
|
33
|
+
def descendants(pid, tree = nil, old_tree = nil, &block)
|
34
|
+
tree ||= ps_tree
|
35
|
+
old_tree ||= tree
|
36
|
+
old_tree[pid][:children].each { |c| descendants(c, tree, old_tree, &block) }
|
37
|
+
yield(pid) if tree[pid] && (tree[pid][:signature] == old_tree[pid][:signature])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
module ParallelCucumber
|
4
|
+
module Helper
|
5
|
+
class Queue
|
6
|
+
attr_reader :name
|
7
|
+
|
8
|
+
def initialize(queue_connection_params)
|
9
|
+
# queue_connection_params:
|
10
|
+
# `url--[name]`
|
11
|
+
# url:
|
12
|
+
# TCP connection: `redis://[password]@[hostname]:[port]/[db]` (password, port and database are optional),
|
13
|
+
# unix socket connection: `unix://[path to Redis socket]`.
|
14
|
+
# name:
|
15
|
+
# queue name, default is `queue`
|
16
|
+
url, name = queue_connection_params
|
17
|
+
@redis = Redis.new(url: url)
|
18
|
+
@name = name
|
19
|
+
end
|
20
|
+
|
21
|
+
def enqueue(elements)
|
22
|
+
@redis.lpush(@name, elements)
|
23
|
+
end
|
24
|
+
|
25
|
+
def dequeue
|
26
|
+
@redis.rpop(@name)
|
27
|
+
end
|
28
|
+
|
29
|
+
def length
|
30
|
+
@redis.llen(@name)
|
31
|
+
end
|
32
|
+
|
33
|
+
def empty?
|
34
|
+
length.zero?
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require_relative '../cucumber'
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
class CucumberTest < Test::Unit::TestCase
|
5
|
+
def test_argument_mapping
|
6
|
+
s, m = ParallelCucumber::Helper::Cucumber.batch_mapped_files('--out foo/bar -o wib/ble', 'ARGH', {})
|
7
|
+
assert_equal('--out ARGH/bar -o ARGH/ble', s)
|
8
|
+
assert_equal({ 'foo/bar' => 'ARGH/bar', 'wib/ble' => 'ARGH/ble' }, m)
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module ParallelCucumber
|
4
|
+
class CustomLogger < Logger
|
5
|
+
private
|
6
|
+
|
7
|
+
def format_message(severity, datetime, progname, msg)
|
8
|
+
if @level == DEBUG
|
9
|
+
"[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}]\t#{progname}\t#{severity}\t#{msg.gsub(/\s+/, ' ').strip}\n"
|
10
|
+
else
|
11
|
+
"#{progname}\t#{msg.gsub(/\s+/, ' ').strip}\n"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'parallel'
|
2
|
+
|
3
|
+
module ParallelCucumber
|
4
|
+
class Main
|
5
|
+
include ParallelCucumber::Helper::Utils
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
@options = options
|
9
|
+
|
10
|
+
@logger = ParallelCucumber::CustomLogger.new(STDOUT)
|
11
|
+
@logger.progname = 'Primary' # Longer than 'Main', to make the log file pretty.
|
12
|
+
@logger.level = options[:debug] ? ParallelCucumber::CustomLogger::DEBUG : ParallelCucumber::CustomLogger::INFO
|
13
|
+
end
|
14
|
+
|
15
|
+
def run
|
16
|
+
queue = Helper::Queue.new(@options[:queue_connection_params])
|
17
|
+
@logger.debug("Connecting to Queue: #{@options[:queue_connection_params]}")
|
18
|
+
|
19
|
+
unless queue.empty?
|
20
|
+
@logger.error("Queue '#{queue.name}' is not empty")
|
21
|
+
exit(1)
|
22
|
+
end
|
23
|
+
|
24
|
+
tests = []
|
25
|
+
mm, ss = time_it do
|
26
|
+
dry_run_report = Helper::Cucumber.dry_run_report(@options[:cucumber_options], @options[:cucumber_args])
|
27
|
+
tests = Helper::Cucumber.parse_json_report(dry_run_report).keys
|
28
|
+
end
|
29
|
+
tests.shuffle!
|
30
|
+
@logger.debug("Generating all tests took #{mm} minutes #{ss} seconds")
|
31
|
+
|
32
|
+
if tests.empty?
|
33
|
+
@logger.error('There are no tests to run')
|
34
|
+
exit(1)
|
35
|
+
end
|
36
|
+
|
37
|
+
@logger.info("Adding #{tests.count} tests to Queue")
|
38
|
+
queue.enqueue(tests)
|
39
|
+
|
40
|
+
if @options[:n] == 0
|
41
|
+
@options[:n] = [1, @options[:env_variables].map { |_k, v| v.is_a?(Array) ? v.count : 0 }].flatten.max
|
42
|
+
@logger.info("Inferred worker count #{@options[:n]} from env_variables option")
|
43
|
+
end
|
44
|
+
|
45
|
+
number_of_workers = [@options[:n], tests.count].min
|
46
|
+
unless number_of_workers == @options[:n]
|
47
|
+
@logger.info(<<-LOG)
|
48
|
+
Number of workers was overridden to #{number_of_workers}.
|
49
|
+
Was requested more workers (#{@options[:n]}) than tests (#{tests.count})".
|
50
|
+
LOG
|
51
|
+
end
|
52
|
+
|
53
|
+
if (@options[:batch_size] - 1) * number_of_workers >= tests.count
|
54
|
+
original_batch_size = @options[:batch_size]
|
55
|
+
@options[:batch_size] = [(tests.count.to_f / number_of_workers).floor, 1].max
|
56
|
+
@logger.info(<<-LOG)
|
57
|
+
Batch size was overridden to #{@options[:batch_size]}.
|
58
|
+
Presumably it will be more optimal for #{tests.count} tests and #{number_of_workers} workers
|
59
|
+
than #{original_batch_size}
|
60
|
+
LOG
|
61
|
+
end
|
62
|
+
|
63
|
+
diff = []
|
64
|
+
info = {}
|
65
|
+
total_mm, total_ss = time_it do
|
66
|
+
results = Helper::Command.wrap_block(@options[:log_decoration],
|
67
|
+
@options[:log_decoration]['worker_block'] || 'workers',
|
68
|
+
@logger) do
|
69
|
+
finished = []
|
70
|
+
Parallel.map(
|
71
|
+
0...number_of_workers,
|
72
|
+
in_processes: number_of_workers,
|
73
|
+
finish: -> (_, index, _) { @logger.info("Finished: #{finished[index] = index} #{finished - [nil]}") }
|
74
|
+
) do |index|
|
75
|
+
Worker.new(@options, index).start(env_for_worker(@options[:env_variables], index))
|
76
|
+
end.inject(:merge) # Returns hash of file:line to statuses + :worker-index to summary.
|
77
|
+
end
|
78
|
+
results ||= {}
|
79
|
+
unrun = tests - results.keys
|
80
|
+
@logger.error("Tests #{unrun.join(' ')} were not run") unless diff.empty?
|
81
|
+
@logger.error("Queue #{queue.name} is not empty") unless queue.empty?
|
82
|
+
|
83
|
+
Helper::Command.wrap_block(
|
84
|
+
@options[:log_decoration],
|
85
|
+
'Worker summary',
|
86
|
+
@logger
|
87
|
+
) { results.find_all { |w| @logger.info("#{w.first} #{w.last.sort}") if w.first =~ /^:worker-/ } }
|
88
|
+
|
89
|
+
info = Status.constants.map do |status|
|
90
|
+
status = Status.const_get(status)
|
91
|
+
tests_with_status = results.select { |_t, s| s == status }.keys
|
92
|
+
[status, tests_with_status]
|
93
|
+
end.to_h
|
94
|
+
end
|
95
|
+
|
96
|
+
@logger.debug("SUMMARY=#{@options[:summary]}") if @options[:summary]
|
97
|
+
info.each do |s, tt|
|
98
|
+
next if tt.empty?
|
99
|
+
@logger.info("Total: #{s.to_s.upcase} tests (#{tt.count}): #{tt.join(' ')}")
|
100
|
+
filename = @options[:summary] && @options[:summary][s.to_s.downcase]
|
101
|
+
open(filename, 'w') { |f| f << tt.join("\n") } if filename
|
102
|
+
end
|
103
|
+
|
104
|
+
@logger.info("\nTook #{total_mm} minutes #{total_ss} seconds")
|
105
|
+
|
106
|
+
exit((diff + info[Status::FAILED] + info[Status::UNKNOWN]).empty? ? 0 : 1)
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def env_for_worker(env_variables, worker_number)
|
112
|
+
env = env_variables.map do |k, v|
|
113
|
+
case v
|
114
|
+
when String, Numeric, TrueClass, FalseClass
|
115
|
+
[k, v]
|
116
|
+
when Array
|
117
|
+
[k, v[worker_number]]
|
118
|
+
when Hash
|
119
|
+
value = v[worker_number.to_s]
|
120
|
+
[k, value] unless value.nil?
|
121
|
+
when NilClass
|
122
|
+
else
|
123
|
+
raise("Don't know how to set '#{v}'<#{v.class}> to the environment variable '#{k}'")
|
124
|
+
end
|
125
|
+
end.compact.to_h
|
126
|
+
|
127
|
+
# Defaults, if absent in env. Shame 'merge' isn't something non-commutative like 'adopts/defaults'.
|
128
|
+
env = { TEST: 1, TEST_PROCESS_NUMBER: worker_number, WORKER_INDEX: worker_number }.merge(env)
|
129
|
+
|
130
|
+
# Overwrite this if it exists in env.
|
131
|
+
env.merge(PARALLEL_CUCUMBER_EXPORTS: env.keys.join(',')).map { |k, v| [k.to_s, v.to_s] }.to_h
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -1,3 +1,3 @@
|
|
1
1
|
module ParallelCucumber
|
2
|
-
VERSION = '0.
|
3
|
-
end
|
2
|
+
VERSION = '0.2.0.pre.36'.freeze
|
3
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
require 'English'
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
module ParallelCucumber
|
5
|
+
class Tracker
|
6
|
+
def initialize(queue)
|
7
|
+
@queue = queue
|
8
|
+
end
|
9
|
+
|
10
|
+
def status
|
11
|
+
queue_length = @queue.length
|
12
|
+
now = Time.now
|
13
|
+
@full ||= queue_length
|
14
|
+
@start ||= now
|
15
|
+
completed = @full - queue_length
|
16
|
+
elapsed = now - @start
|
17
|
+
estimate = (completed == 0) ? '' : " #{(elapsed * @full / completed).to_i}s est"
|
18
|
+
"#{queue_length}/#{@full} left #{elapsed.to_i}s worker#{estimate}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Worker
|
23
|
+
include ParallelCucumber::Helper::Utils
|
24
|
+
|
25
|
+
def initialize(options, index)
|
26
|
+
@batch_size = options[:batch_size]
|
27
|
+
@batch_timeout = options[:batch_timeout]
|
28
|
+
@setup_timeout = options[:setup_timeout]
|
29
|
+
@cucumber_options = options[:cucumber_options]
|
30
|
+
@test_command = options[:test_command]
|
31
|
+
@pre_check = options[:pre_check]
|
32
|
+
@pretty = options[:pretty]
|
33
|
+
@env_variables = options[:env_variables]
|
34
|
+
@index = index
|
35
|
+
@queue_connection_params = options[:queue_connection_params]
|
36
|
+
@setup_worker = options[:setup_worker]
|
37
|
+
@teardown_worker = options[:teardown_worker]
|
38
|
+
@worker_delay = options[:worker_delay]
|
39
|
+
@debug = options[:debug]
|
40
|
+
@log_decoration = options[:log_decoration]
|
41
|
+
@log_dir = options[:log_dir]
|
42
|
+
@log_file = "#{@log_dir}/worker_#{index}.log"
|
43
|
+
end
|
44
|
+
|
45
|
+
def start(env)
|
46
|
+
env = env.dup.merge!('WORKER_LOG' => @log_file)
|
47
|
+
|
48
|
+
File.delete(@log_file) if File.exist?(@log_file)
|
49
|
+
File.open(@log_file, 'a') do |file_handle|
|
50
|
+
file_handle.sync = true
|
51
|
+
@logger = ParallelCucumber::CustomLogger.new(MultiDelegator.delegate(:write, :close).to(STDOUT, file_handle))
|
52
|
+
@logger.progname = "Worker-#{@index}"
|
53
|
+
@logger.level = @debug ? ParallelCucumber::CustomLogger::DEBUG : ParallelCucumber::CustomLogger::INFO
|
54
|
+
|
55
|
+
@logger.info("Starting, also logging to #{@log_file}")
|
56
|
+
|
57
|
+
unless @worker_delay.zero?
|
58
|
+
@logger.info("Waiting #{@worker_delay * @index} seconds before start")
|
59
|
+
sleep(@worker_delay * @index)
|
60
|
+
end
|
61
|
+
|
62
|
+
@logger.debug(<<-LOG)
|
63
|
+
Additional environment variables: #{env.map { |k, v| "#{k}=#{v}" }.join(' ')}
|
64
|
+
LOG
|
65
|
+
|
66
|
+
results = {}
|
67
|
+
running_total = Hash.new(0)
|
68
|
+
begin
|
69
|
+
setup(env)
|
70
|
+
|
71
|
+
queue = ParallelCucumber::Helper::Queue.new(@queue_connection_params)
|
72
|
+
queue_tracker = Tracker.new(queue)
|
73
|
+
|
74
|
+
loop_mm, loop_ss = time_it do
|
75
|
+
loop do
|
76
|
+
break if queue.empty?
|
77
|
+
batch = []
|
78
|
+
precheck(env)
|
79
|
+
@batch_size.times do
|
80
|
+
# TODO: Handle recovery of dequeued tests, if a worker dies mid-processing.
|
81
|
+
batch << queue.dequeue
|
82
|
+
end
|
83
|
+
batch.compact!
|
84
|
+
batch.sort!
|
85
|
+
break if batch.empty?
|
86
|
+
|
87
|
+
run_batch(env, queue_tracker, results, running_total, batch)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
@logger.debug("Loop took #{loop_mm} minutes #{loop_ss} seconds")
|
91
|
+
ensure
|
92
|
+
teardown(env)
|
93
|
+
|
94
|
+
results[":worker-#{@index}"] = running_total
|
95
|
+
results
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def run_batch(env, queue_tracker, results, running_total, tests)
|
101
|
+
batch_id = "#{Time.now.to_i}-#{@index}"
|
102
|
+
@logger.debug("Batch ID is #{batch_id}")
|
103
|
+
@logger.info("Took #{tests.count} from the queue (#{queue_tracker.status}): #{tests.join(' ')}")
|
104
|
+
|
105
|
+
batch_mm, batch_ss = time_it do
|
106
|
+
batch_results = test_batch(batch_id, env, running_total, tests)
|
107
|
+
|
108
|
+
process_results(batch_results, tests)
|
109
|
+
|
110
|
+
running_totals(batch_results, running_total)
|
111
|
+
results.merge!(batch_results)
|
112
|
+
end
|
113
|
+
|
114
|
+
@logger.debug("Batch #{batch_id} took #{batch_mm} minutes #{batch_ss} seconds")
|
115
|
+
end
|
116
|
+
|
117
|
+
def precheck(env)
|
118
|
+
return unless @pre_check
|
119
|
+
continue = Helper::Command.exec_command(
|
120
|
+
env, 'precheck', @pre_check, @log_file, @logger, @log_decoration, @batch_timeout
|
121
|
+
)
|
122
|
+
return if continue
|
123
|
+
@logger.error('Pre-check failed: quitting immediately')
|
124
|
+
raise :prechek_failed
|
125
|
+
end
|
126
|
+
|
127
|
+
def running_totals(batch_results, running_total)
|
128
|
+
batch_info = Status.constants.map do |status|
|
129
|
+
status = Status.const_get(status)
|
130
|
+
[status, batch_results.select { |_t, s| s == status }.keys]
|
131
|
+
end.to_h
|
132
|
+
batch_info.each do |s, tt|
|
133
|
+
@logger.info("#{s.to_s.upcase} #{tt.count} tests: #{tt.join(' ')}") unless tt.empty?
|
134
|
+
running_total[s] += tt.count unless tt.empty?
|
135
|
+
end
|
136
|
+
running_total[:batches] += 1
|
137
|
+
@logger.info(running_total.sort.to_s)
|
138
|
+
end
|
139
|
+
|
140
|
+
def process_results(batch_results, tests)
|
141
|
+
batch_keys = batch_results.keys
|
142
|
+
test_syms = tests.map(&:to_sym)
|
143
|
+
unrun = test_syms - batch_keys
|
144
|
+
surfeit = batch_keys - test_syms
|
145
|
+
unrun.each { |test| batch_results[test] = Status::UNKNOWN }
|
146
|
+
surfeit.each { |test| batch_results.delete(test) }
|
147
|
+
@logger.error("Did not run #{unrun.count}/#{tests.count}: #{unrun.join(' ')}") unless unrun.empty?
|
148
|
+
@logger.error("Extraneous runs (#{surfeit.count}): #{surfeit.join(' ')}") unless surfeit.empty?
|
149
|
+
return if surfeit.empty?
|
150
|
+
# Don't see how this can happen, but...
|
151
|
+
@logger.error("Tests/result mismatch: #{tests.count}!=#{batch_results.count}: #{tests}/#{batch_keys}")
|
152
|
+
end
|
153
|
+
|
154
|
+
def test_batch(batch_id, env, running_total, tests)
|
155
|
+
test_batch_dir = "/tmp/w-#{batch_id}"
|
156
|
+
FileUtils.rm_rf(test_batch_dir)
|
157
|
+
FileUtils.mkpath(test_batch_dir)
|
158
|
+
|
159
|
+
test_state = "#{test_batch_dir}/test_state.json"
|
160
|
+
cmd = "#{@test_command} #{@pretty} --format json --out #{test_state} #{@cucumber_options} "
|
161
|
+
batch_env = {
|
162
|
+
:TEST_BATCH_ID.to_s => batch_id,
|
163
|
+
:TEST_BATCH_DIR.to_s => test_batch_dir,
|
164
|
+
:BATCH_NUMBER.to_s => running_total[:batches].to_s
|
165
|
+
}.merge(env)
|
166
|
+
mapped_batch_cmd, file_map = Helper::Cucumber.batch_mapped_files(cmd, test_batch_dir, batch_env)
|
167
|
+
file_map.each { |_user, worker| FileUtils.mkpath(worker) if worker =~ %r{\/$} }
|
168
|
+
mapped_batch_cmd += ' ' + tests.join(' ')
|
169
|
+
res = ParallelCucumber::Helper::Command.exec_command(
|
170
|
+
batch_env, 'batch', mapped_batch_cmd, @log_file, @logger, @log_decoration, @batch_timeout
|
171
|
+
)
|
172
|
+
batch_results = if res.nil?
|
173
|
+
{}
|
174
|
+
else
|
175
|
+
Helper::Command.wrap_block(@log_decoration, 'file copy', @logger) do
|
176
|
+
# Use system cp -r because Ruby's has crap diagnostics in weird situations.
|
177
|
+
# Copy files we might have renamed or moved
|
178
|
+
file_map.each do |user, worker|
|
179
|
+
unless worker == user
|
180
|
+
cp_out = `cp -Rv #{worker} #{user} 2>&1`
|
181
|
+
@logger.debug("Copy of #{worker} to #{user} said: #{cp_out}")
|
182
|
+
end
|
183
|
+
end
|
184
|
+
# Copy everything else too, in case it's interesting.
|
185
|
+
cp_out = `cp -Rv #{test_batch_dir}/* #{@log_dir} 2>&1`
|
186
|
+
@logger.debug("Copy of #{test_batch_dir}/* to #{@log_dir} said: #{cp_out}")
|
187
|
+
parse_results(test_state)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
ensure
|
191
|
+
FileUtils.rm_rf(test_batch_dir)
|
192
|
+
batch_results
|
193
|
+
end
|
194
|
+
|
195
|
+
def teardown(env)
|
196
|
+
return unless @teardown_worker
|
197
|
+
mm, ss = time_it do
|
198
|
+
@logger.info('Teardown running')
|
199
|
+
success = Helper::Command.exec_command(
|
200
|
+
env, 'teardown', @teardown_worker, @log_file, @logger, @log_decoration
|
201
|
+
)
|
202
|
+
@logger.warn('Teardown finished with error') unless success
|
203
|
+
end
|
204
|
+
@logger.debug("Teardown took #{mm} minutes #{ss} seconds")
|
205
|
+
end
|
206
|
+
|
207
|
+
def setup(env)
|
208
|
+
return unless @setup_worker
|
209
|
+
mm, ss = time_it do
|
210
|
+
@logger.info('Setup running')
|
211
|
+
success = Helper::Command.exec_command(
|
212
|
+
env, 'setup', @setup_worker, @log_file, @logger, @log_decoration, @setup_timeout
|
213
|
+
)
|
214
|
+
unless success
|
215
|
+
@logger.warn('Setup failed: quitting immediately')
|
216
|
+
raise :setup_failed
|
217
|
+
end
|
218
|
+
end
|
219
|
+
@logger.debug("Setup took #{mm} minutes #{ss} seconds")
|
220
|
+
end
|
221
|
+
|
222
|
+
def parse_results(f)
|
223
|
+
unless File.file?(f)
|
224
|
+
@logger.error("Results file does not exist: #{f}")
|
225
|
+
return {}
|
226
|
+
end
|
227
|
+
json_report = File.read(f)
|
228
|
+
if json_report.empty?
|
229
|
+
@logger.error("Results file is empty: #{f}")
|
230
|
+
return {}
|
231
|
+
end
|
232
|
+
Helper::Cucumber.parse_json_report(json_report)
|
233
|
+
rescue => e
|
234
|
+
trace = e.backtrace.join("\n\t").sub("\n\t", ": #{$ERROR_INFO}#{e.class ? " (#{e.class})" : ''}\n\t")
|
235
|
+
@logger.error("Threw: JSON parse of results caused #{trace}")
|
236
|
+
{}
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|