parallel_cucumber 0.1.22 → 0.2.0.pre.36

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: 0712cec043620fc781e38673adc438452b993e59
4
- data.tar.gz: a8c396bf23c857e9f269e36ea24b018b304de8c4
3
+ metadata.gz: 9a409cddb7ad8882664b62866dd1d9ffb39caa9d
4
+ data.tar.gz: 0224ea5dd38f661176160e0534f1b5c673f9d147
5
5
  SHA512:
6
- metadata.gz: 2561568ca2d8397944b67171a49839832b422d16135c21b749ca27965706c99165358f520f79f4464724e58978598b07901cc95c298a1a207661fd5fdbc325aa
7
- data.tar.gz: 024e7fe7d1c6427080e94a7f572b3b45c3c940224dea48b956085b5be876107408970004953116f00f92d9b884ab2e2f6920d00df81fe003f6a8ecbe181dd91d
6
+ metadata.gz: 155b9bd2377687099c769731f2cf576d4e8984883745adadc08a48ac6403ea0f6e779714c0ade86b2dc816f469733ce78cb22c7826b195c6aed74df7fb3cca33
7
+ data.tar.gz: bd51ffc599151b873599bff76353b3c565f878d18262b6d1c981b364eb708ac3fa6e3e89e611987124a56d110337873d0a2b498d07a57a59944033a3bcf9c8e5
data/README.md CHANGED
@@ -1,15 +1,35 @@
1
1
  # Parallel Cucumber
2
2
 
3
+ ### Usage
4
+
3
5
  ```
4
6
  Usage: parallel_cucumber [options] [ [FILE|DIR|URL][:LINE[:LINE]*] ]
5
7
  Example: parallel_cucumber -n 4 -o "-f pretty -f html -o report.html" examples/i18n/en/features
6
- -n [PROCESSES] How many processes to use
7
- -o "[OPTIONS]", Run cucumber with these options
8
- --cucumber-options
9
- -e, --env-variables [JSON] Set additional environment variables to processes
10
- -s, --setup-script [SCRIPT] Execute SCRIPT before each process
11
- -t, --teardown-script [SCRIPT] Execute SCRIPT after each process
12
- --thread-delay [SECONDS] Delay before next thread starting
8
+ -n WORKERS How many workers to use. Default is 1 or longest list in -e
9
+ -o, --cucumber-options "OPTIONS" Run cucumber with these options
10
+ --test-command COMMAND Command to run for test phase, default cucumber
11
+ --pre-batch-check COMMAND Command causing worker to quit on exit failure
12
+ -e, --env-variables JSON Set additional environment variables to processes
13
+ --batch-size SIZE How many tests each worker takes from queue at once. Default is 1
14
+ -q ARRAY, `url,name` Url for TCP connection: `redis://[password]@[hostname]:[port]/[db]` (password, port and database are optional), for unix socket connection: `unix://[path to Redis socket]`. Default is redis://127.0.0.1:6379 and name is `queue`
15
+ --queue-connection-params
16
+ --setup-worker SCRIPT Execute SCRIPT before each worker
17
+ --teardown-worker SCRIPT Execute SCRIPT after each worker
18
+ --worker-delay SECONDS Delay before next worker starting. Could be used for avoiding 'spikes' in CPU and RAM usage Default is 0
19
+ --batch-timeout SECONDS Timeout for each batch of tests. Default is 600
20
+ --debug Print more debug information
13
21
  -v, --version Show version
14
22
  -h, --help Show this
15
23
  ```
24
+
25
+ ### Reports
26
+
27
+ ```yaml
28
+ # config/cucumber.yaml
29
+
30
+ <% test_batch_id = "#{ENV['TEST_BATCH_ID']}" %>
31
+
32
+ parallel_reports: >
33
+ --format html --out reports/cukes_<%= test_batch_id %>.html
34
+ --format junit --out reports/junit_<%= test_batch_id %>/
35
+ ```
@@ -6,4 +6,4 @@ $LOAD_PATH << "#{root}/lib" if File.exist?("#{root}/Gemfile")
6
6
 
7
7
  require 'parallel_cucumber'
8
8
 
9
- ParallelCucumber::Cli.run(ARGV)
9
+ ParallelCucumber::Cli.new(ARGV).run
@@ -1,53 +1,12 @@
1
- require 'parallel'
2
-
3
1
  require 'parallel_cucumber/cli'
4
- require 'parallel_cucumber/grouper'
5
- require 'parallel_cucumber/result_formatter'
6
- require 'parallel_cucumber/runner'
2
+ require 'parallel_cucumber/helper/command'
3
+ require 'parallel_cucumber/helper/cucumber'
4
+ require 'parallel_cucumber/helper/multi_delegator'
5
+ require 'parallel_cucumber/helper/processes'
6
+ require 'parallel_cucumber/helper/queue'
7
+ require 'parallel_cucumber/helper/utils'
8
+ require 'parallel_cucumber/logger'
9
+ require 'parallel_cucumber/main'
10
+ require 'parallel_cucumber/status'
7
11
  require 'parallel_cucumber/version'
8
-
9
- module ParallelCucumber
10
- class << self
11
- def run_tests_in_parallel(options)
12
- number_of_processes = options[:n]
13
- test_results = nil
14
-
15
- report_time_taken do
16
- groups = Grouper.feature_groups(options, number_of_processes)
17
- threads = groups.size
18
- completed = []
19
-
20
- on_finish = lambda do |_item, index, _result|
21
- completed.push(index)
22
- remaining_threads = ((0...threads).to_a - completed).sort
23
- puts "Thread #{index} has finished. Remaining(#{remaining_threads.count}): #{remaining_threads.join(', ')}"
24
- end
25
-
26
- test_results = Parallel.map_with_index(
27
- groups,
28
- in_threads: threads,
29
- finish: on_finish
30
- ) do |group, index|
31
- Runner.new(options).run_tests(index, group)
32
- end
33
- puts 'All threads are complete'
34
- ResultFormatter.report_results(test_results)
35
- end
36
- exit(1) if any_test_failed?(test_results)
37
- end
38
-
39
- private
40
-
41
- def any_test_failed?(test_results)
42
- test_results.any? { |result| result[:exit_status] != 0 }
43
- end
44
-
45
- def report_time_taken
46
- start = Time.now
47
- yield
48
- time_in_sec = Time.now - start
49
- mm, ss = time_in_sec.divmod(60)
50
- puts "\nTook #{mm} Minutes, #{ss.round(2)} Seconds"
51
- end
52
- end # class
53
- end # ParallelCucumber
12
+ require 'parallel_cucumber/worker'
@@ -1,79 +1,186 @@
1
1
  require 'json'
2
2
  require 'optparse'
3
+ require 'date'
3
4
 
4
5
  module ParallelCucumber
5
- module Cli
6
- class << self
7
- DEFAULTS = {
8
- env_variables: {},
9
- thread_delay: 0,
10
- cucumber_options: '',
11
- n: 1
12
- }.freeze
13
-
14
- def run(argv)
15
- options = parse_options!(argv)
16
-
17
- ParallelCucumber.run_tests_in_parallel(options)
18
- end
6
+ class Cli
7
+ DEFAULTS = {
8
+ batch_size: 1,
9
+ batch_timeout: 600,
10
+ setup_timeout: 30,
11
+ cucumber_options: '',
12
+ debug: false,
13
+ log_dir: '.',
14
+ log_decoration: {},
15
+ env_variables: {},
16
+ n: 0, # Default: computed from longest list in json parameters, minimum 1.
17
+ queue_connection_params: ['redis://127.0.0.1:6379', DateTime.now.strftime('queue-%Y%m%d%H%M%S')],
18
+ worker_delay: 0,
19
+ test_command: 'cucumber'
20
+ }.freeze
19
21
 
20
- private
22
+ def initialize(argv)
23
+ @argv = argv
24
+ @logger = ParallelCucumber::CustomLogger.new(STDOUT)
25
+ @logger.progname = 'CLI'
26
+ @logger.level = if @argv.include?('--debug')
27
+ ParallelCucumber::CustomLogger::DEBUG
28
+ else
29
+ ParallelCucumber::CustomLogger::INFO
30
+ end
31
+ end
21
32
 
22
- def parse_options!(argv)
23
- options = DEFAULTS.dup
33
+ def run
34
+ options = parse_options!(@argv)
35
+ message = <<-LOG
36
+ Running parallel_cucumber with options: #{options.map { |k, v| "#{k}=#{v}" }.join(', ')}
37
+ LOG
38
+ @logger.debug(message)
39
+ ParallelCucumber::Main.new(options).run
40
+ end
24
41
 
25
- option_parser = OptionParser.new do |opts|
26
- opts.banner = [
27
- 'Usage: parallel_cucumber [options] [ [FILE|DIR|URL][:LINE[:LINE]*] ]',
28
- 'Example: parallel_cucumber -n 4 -o "-f pretty -f html -o report.html" examples/i18n/en/features'
29
- ].join("\n")
30
- opts.on('-n [PROCESSES]', Integer, 'How many processes to use') { |n| options[:n] = n }
31
- opts.on('-o', '--cucumber-options "[OPTIONS]"', 'Run cucumber with these options') do |cucumber_options|
32
- options[:cucumber_options] = cucumber_options
33
- end
34
- opts.on('-e', '--env-variables [JSON]', 'Set additional environment variables to processes') do |env_vars|
35
- options[:env_variables] = begin
36
- JSON.parse(env_vars)
37
- rescue JSON::ParserError
38
- puts 'Additional environment variables not in JSON format. And do not forget to escape quotes'
39
- exit 1
40
- end
41
- end
42
- opts.on('-s', '--setup-script [SCRIPT]', 'Execute SCRIPT before each process') do |script|
43
- check_script(script)
44
- options[:setup_script] = File.expand_path(script)
42
+ private
43
+
44
+ def parse_options!(argv)
45
+ options = DEFAULTS.dup
46
+
47
+ option_parser = OptionParser.new do |opts|
48
+ opts.banner = [
49
+ 'Usage: parallel_cucumber [options] [ [FILE|DIR|URL][:LINE[:LINE]*] ]',
50
+ 'Example: parallel_cucumber -n 4 -o "-f pretty -f html -o report.html" examples/i18n/en/features'
51
+ ].join("\n")
52
+
53
+ opts.on('-n WORKERS', Integer, 'How many workers to use. Default is 1 or longest list in -e') do |n|
54
+ if n < 1
55
+ puts "The minimum number of processes is 1 but given: '#{n}'"
56
+ exit 1
45
57
  end
46
- opts.on('-t', '--teardown-script [SCRIPT]', 'Execute SCRIPT after each process') do |script|
47
- check_script(script)
48
- options[:teardown_script] = File.expand_path(script)
58
+ options[:n] = n
59
+ end
60
+
61
+ opts.on('-o', '--cucumber-options "OPTIONS"', 'Run cucumber with these options') do |cucumber_options|
62
+ options[:cucumber_options] = cucumber_options
63
+ end
64
+
65
+ opts.on('--test-command COMMAND',
66
+ "Command to run for test phase, default #{DEFAULTS[:test_command]}") do |test_command|
67
+ options[:test_command] = test_command
68
+ end
69
+
70
+ opts.on('--pre-batch-check COMMAND', 'Command causing worker to quit on exit failure') do |pre_check|
71
+ options[:pre_check] = pre_check
72
+ end
73
+
74
+ options[:pretty] = '--format pretty'
75
+ opts.on('--no-pretty', "Suppress the default 'pretty' formatter directed at stdout") do
76
+ options[:pretty] = ''
77
+ end
78
+
79
+ opts.on('--log-dir DIR', 'Directory for worker logfiles') do |log_dir|
80
+ options[:log_dir] = log_dir
81
+ end
82
+
83
+ opts.on('--log-decoration JSON', 'Block quoting for logs, e.g. {start: "#start %s", end: "#end %s"}') do |json|
84
+ options[:log_decoration] = begin
85
+ JSON.parse(json)
86
+ rescue JSON::ParserError
87
+ puts 'Log block quoting not in JSON format. Did you forget to escape the quotes?'
88
+ raise
49
89
  end
50
- opts.on('--thread-delay [SECONDS]', Float, 'Delay before next thread starting') do |thread_delay|
51
- options[:thread_delay] = thread_delay
90
+ end
91
+
92
+ opts.on('--summary JSON', 'Summary files, e.g. {failed: "./failed.txt", unknown: "./unknown.txt"}') do |json|
93
+ options[:summary] = begin
94
+ JSON.parse(json)
95
+ rescue JSON::ParserError
96
+ puts 'Log block quoting not in JSON format. Did you forget to escape the quotes?'
97
+ raise
52
98
  end
53
- opts.on('-v', '--version', 'Show version') do
54
- puts ParallelCucumber::VERSION
55
- exit 0
99
+ end
100
+
101
+ opts.on('-e', '--env-variables JSON', 'Set additional environment variables to processes') do |env_vars|
102
+ options[:env_variables] = begin
103
+ JSON.parse(env_vars)
104
+ rescue JSON::ParserError
105
+ puts 'Additional environment variables not in JSON format. Did you forget to escape the quotes?'
106
+ raise
56
107
  end
57
- opts.on('-h', '--help', 'Show this') do
58
- puts opts
59
- exit 0
108
+ end
109
+
110
+ help_message = "How many tests each worker takes from queue at once. Default is #{DEFAULTS[:batch_size]}"
111
+ opts.on('--batch-size SIZE', Integer, help_message.gsub(/\s+/, ' ').strip) do |batch_size|
112
+ if batch_size < 1
113
+ puts "The minimum batch size is 1 but given: '#{batch_size}'"
114
+ exit 1
60
115
  end
116
+ options[:batch_size] = batch_size
61
117
  end
62
118
 
63
- option_parser.parse!(argv)
64
- options[:cucumber_args] = argv
119
+ help_message = <<-TEXT
120
+ `url,name`
121
+ Url for TCP connection:
122
+ `redis://[password]@[hostname]:[port]/[db]` (password, port and database are optional),
123
+ for unix socket connection: `unix://[path to Redis socket]`.
124
+ Default is redis://127.0.0.1:6379 and name is `queue`
125
+ TEXT
126
+ opts.on('-q', '--queue-connection-params ARRAY', Array, help_message.gsub(/\s+/, ' ').strip) do |params|
127
+ options[:queue_connection_params] = params
128
+ end
65
129
 
66
- options
67
- rescue OptionParser::InvalidOption => e
68
- puts "Unknown option #{e}"
69
- puts option_parser.help
70
- exit 1
71
- end
130
+ opts.on('--setup-worker SCRIPT', 'Execute SCRIPT before each worker') do |script|
131
+ options[:setup_worker] = script
132
+ end
72
133
 
73
- def check_script(path)
74
- fail("File '#{path}' does not exist") unless File.exist?(path)
75
- fail("File '#{path}' is not executable") unless File.executable?(path)
134
+ opts.on('--teardown-worker SCRIPT', 'Execute SCRIPT after each worker') do |script|
135
+ options[:teardown_worker] = script
136
+ end
137
+
138
+ help_message = <<-TEXT
139
+ Delay before next worker starting.
140
+ Could be used for avoiding 'spikes' in CPU and RAM usage
141
+ Default is #{DEFAULTS[:worker_delay]}
142
+ TEXT
143
+ opts.on('--worker-delay SECONDS', Float, help_message.gsub(/\s+/, ' ').strip) do |worker_delay|
144
+ options[:worker_delay] = worker_delay
145
+ end
146
+
147
+ help_message = <<-TEXT
148
+ Timeout for each batch of tests. Default is #{DEFAULTS[:batch_timeout]}
149
+ TEXT
150
+ opts.on('--batch-timeout SECONDS', Float, help_message.gsub(/\s+/, ' ').strip) do |batch_timeout|
151
+ options[:batch_timeout] = batch_timeout
152
+ end
153
+
154
+ help_message = <<-TEXT
155
+ Timeout for each worker's set-up phase. Default is #{DEFAULTS[:setup_timeout]}
156
+ TEXT
157
+ opts.on('--setup-timeout SECONDS', Float, help_message.gsub(/\s+/, ' ').strip) do |setup_timeout|
158
+ options[:setup_timeout] = setup_timeout
159
+ end
160
+
161
+ opts.on('--debug', 'Print more debug information') do |debug|
162
+ options[:debug] = debug
163
+ end
164
+
165
+ opts.on('-v', '--version', 'Show version') do
166
+ puts ParallelCucumber::VERSION
167
+ exit 0
168
+ end
169
+
170
+ opts.on('-h', '--help', 'Show this') do
171
+ puts opts
172
+ exit 0
173
+ end
76
174
  end
77
- end # class
78
- end # Cli
79
- end # ParallelCucumber
175
+
176
+ option_parser.parse!(argv)
177
+ options[:cucumber_args] = argv
178
+
179
+ options
180
+ rescue OptionParser::InvalidOption => e
181
+ puts "Unknown option #{e}"
182
+ puts option_parser.help
183
+ exit 1
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,87 @@
1
+ module ParallelCucumber
2
+ module Helper
3
+ module Command
4
+ class << self
5
+ def wrap_block(log_decoration, block_name, logger)
6
+ logger << format(log_decoration['start'] + "\n", block_name) if log_decoration['start']
7
+ yield
8
+ ensure
9
+ logger << format(log_decoration['end'] + "\n", block_name) if log_decoration['end']
10
+ end
11
+
12
+ def exec_command(env, desc, script, log_file, logger, log_decoration = {}, timeout = 30) # rubocop:disable Metrics/ParameterLists, Metrics/LineLength
13
+ file = File.open(log_file)
14
+ file.seek(0, File::SEEK_END)
15
+ full_script = "#{script} >>#{log_file} 2>&1"
16
+ env_string = env.map { |k, v| "#{k}=#{v}" }.sort.join(' ')
17
+ message = <<-LOG
18
+ Running command `#{full_script}` with environment variables: #{env_string}
19
+ LOG
20
+ logger.debug(message)
21
+ pstat = nil
22
+ pout = nil
23
+ begin
24
+ completed = Timeout.timeout(timeout) do
25
+ pin, pout, pstat = Open3.popen2e(env, full_script)
26
+ logger.debug("Command has pid #{pstat[:pid]}")
27
+ pin.close
28
+ out = pout.readlines.join("\n") # Not expecting anything in 'out' due to redirection, but...
29
+ pout.close
30
+ pstat.value # N.B. Await process termination
31
+ "Command completed #{pstat.value} and expecting '#{out}' to be empty due to redirects"
32
+ end
33
+ logger.debug(completed)
34
+ # system("lsof #{log_file} >> #{log_file} 2>&1")
35
+ system("ps -axf | grep '#{pstat[:pid]}\\s' >> #{log_file}")
36
+ return pstat.value.success?
37
+ rescue Timeout::Error
38
+ pout.close
39
+ logger.debug("Timeout, so trying SIGINT #{pstat[:pid]}")
40
+ wait_sigint = 15
41
+ logger.error("Timeout #{timeout}s was reached. Sending SIGINT(2), then waiting up to #{wait_sigint}s")
42
+ tree = Helper::Processes.ps_tree
43
+ begin
44
+ Helper::Processes.kill_tree('SIGINT', pstat[:pid].to_s, tree)
45
+ timed_out = wait_sigint.times do |t|
46
+ break if Helper::Processes.all_pids_dead?(pstat[:pid].to_s, nil, tree)
47
+ logger.info("Wait dead #{t}")
48
+ sleep 1
49
+ end
50
+ if timed_out
51
+ logger.error("Process #{pstat[:pid]} lasted #{wait_sigint}s after SIGINT(2), so SIGKILL(9)! Fatality!")
52
+ Helper::Processes.kill_tree('SIGKILL', pstat[:pid].to_s, nil, tree)
53
+ end
54
+ logger.debug("About to reap root #{pstat[:pid]}")
55
+ pstat.value # reap root - everything else should be reaped by init.
56
+ logger.debug("Reaped root #{pstat[:pid]}")
57
+ # system("lsof #{log_file} >> #{log_file}")
58
+ logger.debug("Tried SIGKILL #{pstat[:pid]} - hopefully no processes still have #{log_file}!")
59
+ end
60
+ rescue => e
61
+ logger.debug("Exception #{pstat ? pstat[:pid] : "pstat=#{pstat}=nil"}")
62
+ trace = e.backtrace.join("\n\t").sub("\n\t", ": #{$ERROR_INFO}#{e.class ? " (#{e.class})" : ''}\n\t")
63
+ logger.error("Threw: for #{full_script}, caused #{trace}")
64
+ ensure
65
+ # It's only logging - don't really care if we lose some, though it would be nice if we didn't.
66
+ if log_decoration['worker_block']
67
+ prefix = "#{env['TEST_USER']}-w#{env['WORKER_INDEX']}>"
68
+ block_name = ''
69
+ if log_decoration['start'] || log_decoration['end']
70
+ block_name = "#{prefix} #{desc}"
71
+ prefix = ''
72
+ end
73
+ message = ''
74
+ message << format(log_decoration['start'] + "\n", block_name) if log_decoration['start']
75
+ message << "#{prefix}#{file.readline}" until file.eof
76
+ message << format(log_decoration['end'] + "\n", block_name) if log_decoration['end']
77
+ logger << message
78
+ file.close
79
+ end
80
+ end
81
+ logger.debug("Unusual termination for command: #{script}")
82
+ nil
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end