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 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