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.
@@ -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,13 @@
1
+ module ParallelCucumber
2
+ module Helper
3
+ module Utils
4
+ def time_it
5
+ t1 = Time.now
6
+ yield
7
+ t2 = Time.now
8
+ mm, ss = (t2 - t1).divmod(60)
9
+ [mm, ss.round(1)]
10
+ end
11
+ end
12
+ end
13
+ 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
@@ -0,0 +1,8 @@
1
+ module ParallelCucumber
2
+ module Status
3
+ PASSED = :passed
4
+ FAILED = :failed
5
+ SKIPPED = :skipped
6
+ UNKNOWN = :unknown
7
+ end
8
+ end
@@ -1,3 +1,3 @@
1
1
  module ParallelCucumber
2
- VERSION = '0.1.22'.freeze
3
- end # ParallelCucumber
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