parallel_cucumber 0.1.22

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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0712cec043620fc781e38673adc438452b993e59
4
+ data.tar.gz: a8c396bf23c857e9f269e36ea24b018b304de8c4
5
+ SHA512:
6
+ metadata.gz: 2561568ca2d8397944b67171a49839832b422d16135c21b749ca27965706c99165358f520f79f4464724e58978598b07901cc95c298a1a207661fd5fdbc325aa
7
+ data.tar.gz: 024e7fe7d1c6427080e94a7f572b3b45c3c940224dea48b956085b5be876107408970004953116f00f92d9b884ab2e2f6920d00df81fe003f6a8ecbe181dd91d
@@ -0,0 +1,15 @@
1
+ # Parallel Cucumber
2
+
3
+ ```
4
+ Usage: parallel_cucumber [options] [ [FILE|DIR|URL][:LINE[:LINE]*] ]
5
+ 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
13
+ -v, --version Show version
14
+ -h, --help Show this
15
+ ```
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Enable local usage from cloned repo
4
+ root = File.expand_path('../..', __FILE__)
5
+ $LOAD_PATH << "#{root}/lib" if File.exist?("#{root}/Gemfile")
6
+
7
+ require 'parallel_cucumber'
8
+
9
+ ParallelCucumber::Cli.run(ARGV)
@@ -0,0 +1,53 @@
1
+ require 'parallel'
2
+
3
+ require 'parallel_cucumber/cli'
4
+ require 'parallel_cucumber/grouper'
5
+ require 'parallel_cucumber/result_formatter'
6
+ require 'parallel_cucumber/runner'
7
+ 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
@@ -0,0 +1,79 @@
1
+ require 'json'
2
+ require 'optparse'
3
+
4
+ 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
19
+
20
+ private
21
+
22
+ def parse_options!(argv)
23
+ options = DEFAULTS.dup
24
+
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)
45
+ 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)
49
+ end
50
+ opts.on('--thread-delay [SECONDS]', Float, 'Delay before next thread starting') do |thread_delay|
51
+ options[:thread_delay] = thread_delay
52
+ end
53
+ opts.on('-v', '--version', 'Show version') do
54
+ puts ParallelCucumber::VERSION
55
+ exit 0
56
+ end
57
+ opts.on('-h', '--help', 'Show this') do
58
+ puts opts
59
+ exit 0
60
+ end
61
+ end
62
+
63
+ option_parser.parse!(argv)
64
+ options[:cucumber_args] = argv
65
+
66
+ options
67
+ rescue OptionParser::InvalidOption => e
68
+ puts "Unknown option #{e}"
69
+ puts option_parser.help
70
+ exit 1
71
+ end
72
+
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)
76
+ end
77
+ end # class
78
+ end # Cli
79
+ end # ParallelCucumber
@@ -0,0 +1,95 @@
1
+ require 'English'
2
+ require 'erb'
3
+ require 'json'
4
+ require 'open3'
5
+ require 'tempfile'
6
+ require 'yaml'
7
+
8
+ module ParallelCucumber
9
+ class Grouper
10
+ class << self
11
+ def feature_groups(options, group_size)
12
+ scenario_groups(group_size, options)
13
+ end
14
+
15
+ private
16
+
17
+ def scenario_groups(group_size, options)
18
+ distribution_data = generate_dry_run_report(options)
19
+ all_runnable_scenarios = distribution_data.map do |feature|
20
+ next if feature['elements'].nil?
21
+ feature['elements'].map do |scenario|
22
+ "#{feature['uri']}:#{scenario['line']}" if ['Scenario', 'Scenario Outline'].include?(scenario['keyword'])
23
+ end
24
+ end.flatten.compact
25
+
26
+ group_creator(group_size, all_runnable_scenarios)
27
+ end
28
+
29
+ def generate_dry_run_report(options)
30
+ cucumber_options = options[:cucumber_options]
31
+ cucumber_options = expand_profiles(cucumber_options) unless cucumber_config_file.nil?
32
+ cucumber_options = cucumber_options.gsub(/(--format|-f|--out|-o)\s+[^\s]+/, '')
33
+ result = nil
34
+
35
+ Tempfile.open(%w(dry-run .json)) do |f|
36
+ dry_run_options = "--dry-run --format json --out #{f.path}"
37
+
38
+ cmd = "cucumber #{cucumber_options} #{dry_run_options} #{options[:cucumber_args].join(' ')}"
39
+ _stdout, stderr, status = Open3.capture3(cmd)
40
+ f.close
41
+
42
+ if status != 0
43
+ cmd = "bundle exec #{cmd}" if ENV['BUNDLE_BIN_PATH']
44
+ fail("Can't generate dry run report, command exited with #{status}:\n\t#{cmd}\n\t#{stderr}")
45
+ end
46
+
47
+ content = File.read(f.path)
48
+
49
+ result = begin
50
+ JSON.parse(content)
51
+ rescue JSON::ParserError
52
+ content = content.length > 1024 ? "#{content[0...1000]} ...[TRUNCATED]..." : content
53
+ raise("Can't parse JSON from dry run:\n#{content}")
54
+ end
55
+ end
56
+ result
57
+ end
58
+
59
+ def cucumber_config_file
60
+ Dir.glob('{,.config/,config/}cucumber{.yml,.yaml}').first
61
+ end
62
+
63
+ def expand_profiles(cucumber_options)
64
+ config = YAML.load(ERB.new(File.read(cucumber_config_file)).result)
65
+ _expand_profiles(cucumber_options, config)
66
+ end
67
+
68
+ def _expand_profiles(options, config)
69
+ expand_next = false
70
+ options.split.map do |option|
71
+ case
72
+ when %w(-p --profile).include?(option)
73
+ expand_next = true
74
+ next
75
+ when expand_next
76
+ expand_next = false
77
+ _expand_profiles(config[option], config)
78
+ else
79
+ option
80
+ end
81
+ end.compact.join(' ')
82
+ end
83
+
84
+ def group_creator(groups_count, tasks)
85
+ groups = Array.new(groups_count) { [] }
86
+
87
+ tasks.each do |task|
88
+ group = groups.min_by(&:size)
89
+ group.push(task)
90
+ end
91
+ groups.reject(&:empty?).map(&:compact)
92
+ end
93
+ end # class
94
+ end # Grouper
95
+ end # ParallelCucumber
@@ -0,0 +1,72 @@
1
+ module ParallelCucumber
2
+ class ResultFormatter
3
+ class << self
4
+ def report_results(test_results)
5
+ results = find_results(test_results.map { |result| result[:stdout] }.join(''))
6
+ puts ''
7
+ puts summarize_results(results)
8
+ end
9
+
10
+ def find_results(test_output)
11
+ test_output.split("\n").map do |line|
12
+ line.gsub!(/\e\[\d+m/, '')
13
+ next unless line_is_result?(line)
14
+ line
15
+ end.compact
16
+ end
17
+
18
+ def line_is_result?(line)
19
+ line =~ scenario_or_step_result_regex || line =~ failing_scenario_regex
20
+ end
21
+
22
+ def summarize_results(results)
23
+ output = ["\n\n************ FINAL SUMMARY ************"]
24
+
25
+ failing_scenarios = results.grep(failing_scenario_regex)
26
+ if failing_scenarios.any?
27
+ failing_scenarios.unshift('Failing Scenarios:')
28
+ output << failing_scenarios.join("\n")
29
+ end
30
+
31
+ output << summary(results)
32
+
33
+ output.join("\n\n")
34
+ end
35
+
36
+ def summary(results)
37
+ sort_order = %w(scenario step failed undefined skipped pending passed)
38
+
39
+ %w(scenario step).map do |group|
40
+ group_results = results.grep(/^\d+ #{group}/)
41
+ next if group_results.empty?
42
+
43
+ sums = sum_up_results(group_results)
44
+ sums = sums.sort_by { |word, _| sort_order.index(word) || 999 }
45
+ sums.map! do |word, number|
46
+ plural = 's' if word == group && number != 1
47
+ "#{number} #{word}#{plural}"
48
+ end
49
+ "#{sums[0]} (#{sums[1..-1].join(', ')})"
50
+ end.compact.join("\n")
51
+ end
52
+
53
+ def sum_up_results(results)
54
+ results = results.join(' ').gsub(/s\b/, '') # combine and singularize results
55
+ counts = results.scan(/(\d+) (\w+)/)
56
+ counts.each_with_object(Hash.new(0)) do |(number, word), sum|
57
+ sum[word] += number.to_i
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def scenario_or_step_result_regex
64
+ /^\d+ (steps?|scenarios?)/
65
+ end
66
+
67
+ def failing_scenario_regex
68
+ %r{^cucumber .*features/.+:\d+}
69
+ end
70
+ end # class
71
+ end # ResultFormatter
72
+ end # ParallelCucumber
@@ -0,0 +1,121 @@
1
+ require 'English'
2
+
3
+ module ParallelCucumber
4
+ class Runner
5
+ def initialize(options)
6
+ @options = options
7
+ end
8
+
9
+ def run_tests(process_number, cucumber_args)
10
+ cmd = command_for_test(process_number, cucumber_args)
11
+ execute_command_for_process(process_number, cmd)
12
+ end
13
+
14
+ private
15
+
16
+ def command_for_test(process_number, cucumber_args)
17
+ thread_delay = @options[:thread_delay]
18
+ cucumber_options = @options[:cucumber_options]
19
+ setup_script = @options[:setup_script]
20
+ teardown_script = @options[:teardown_script]
21
+
22
+ delay_cmd = thread_delay > 0 ? "sleep #{thread_delay * process_number}" : nil
23
+
24
+ cucumber_cmd = ['cucumber', cucumber_options, *cucumber_args].compact.join(' ')
25
+
26
+ cmd = [delay_cmd, setup_script, cucumber_cmd].compact.join(' && ')
27
+ teardown_script.nil? ? cmd : "#{cmd}; RET=$?; #{teardown_script}; exit ${RET}"
28
+ end
29
+
30
+ def execute_command_for_process(process_number, cmd)
31
+ env = env_for_process(process_number)
32
+ print_chevron_msg(process_number, "Custom env: #{env.map { |k, v| "#{k}=#{v}" }.join(' ')}; command: #{cmd}")
33
+
34
+ begin
35
+ output = IO.popen(env, "#{cmd} 2>&1") do |io|
36
+ print_chevron_msg(process_number, "Pid: #{io.pid}")
37
+ show_output(io, process_number)
38
+ end
39
+ ensure
40
+ exit_status = -1
41
+ if !$CHILD_STATUS.nil? && $CHILD_STATUS.exited?
42
+ exit_status = $CHILD_STATUS.exitstatus
43
+ print_chevron_msg(process_number, "Exited with status #{exit_status}")
44
+ end
45
+ end
46
+
47
+ { stdout: output, exit_status: exit_status }
48
+ end
49
+
50
+ def env_for_process(process_number)
51
+ env_variables = @options[:env_variables]
52
+ env = env_variables.map do |k, v|
53
+ case v
54
+ when String, Numeric, TrueClass, FalseClass
55
+ [k, v]
56
+ when Array
57
+ [k, v[process_number]]
58
+ when Hash
59
+ value = v[process_number.to_s]
60
+ [k, value] unless value.nil?
61
+ when NilClass
62
+ else
63
+ fail("Don't know how to set '#{v}'(#{v.class}) to the environment variable '#{k}'")
64
+ end
65
+ end.compact.to_h
66
+
67
+ {
68
+ TEST: 1,
69
+ TEST_PROCESS_NUMBER: process_number
70
+ }.merge(env).map { |k, v| [k.to_s, v.to_s] }.to_h
71
+ end
72
+
73
+ def print_chevron_msg(chevron, line, io = $stdout)
74
+ msg = "#{chevron}> #{line}\n"
75
+ io.print(msg)
76
+ io.flush
77
+ end
78
+
79
+ def show_output(io, process_number)
80
+ file = File.open("process_#{process_number}.log", 'w')
81
+ remaining_part = ''
82
+ probable_finish = false
83
+ begin
84
+ loop do
85
+ text_block = remaining_part + io.read_nonblock(32 * 1024)
86
+ lines = text_block.split("\n")
87
+ remaining_part = lines.pop
88
+ probable_finish = last_cucumber_line?(remaining_part)
89
+ lines.each do |line|
90
+ probable_finish = true if last_cucumber_line?(line)
91
+ print_chevron_msg(process_number, line)
92
+ file.write("#{line}\n")
93
+ end
94
+ end
95
+ rescue IO::WaitReadable
96
+ timeout = probable_finish ? 10 : 1800
97
+ result = IO.select([io], [], [], timeout)
98
+ if result.nil?
99
+ if probable_finish
100
+ print_chevron_msg(process_number,
101
+ "Timeout reached in #{timeout}s, but process has probably finished", $stderr)
102
+ else
103
+ raise("Read timeout has reached for process #{process_number}. There is no output in #{timeout}s")
104
+ end
105
+ else
106
+ retry
107
+ end
108
+ rescue EOFError # rubocop:disable Lint/HandleExceptions
109
+ ensure
110
+ print_chevron_msg(process_number, remaining_part)
111
+ file.write(remaining_part.to_s)
112
+ file.close unless file.nil?
113
+ end
114
+ File.read("process_#{process_number}.log")
115
+ end
116
+
117
+ def last_cucumber_line?(line)
118
+ !(line =~ /\d+m[\d\.]+s/).nil?
119
+ end
120
+ end # Runner
121
+ end # ParallelCucumber
@@ -0,0 +1,3 @@
1
+ module ParallelCucumber
2
+ VERSION = '0.1.22'.freeze
3
+ end # ParallelCucumber
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: parallel_cucumber
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.22
5
+ platform: ruby
6
+ authors:
7
+ - Alexander Bayandin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-01-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: cucumber
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: parallel
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.6'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.36'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.36'
55
+ description:
56
+ email: a.bayandin@gmail.com
57
+ executables:
58
+ - parallel_cucumber
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - README.md
63
+ - bin/parallel_cucumber
64
+ - lib/parallel_cucumber.rb
65
+ - lib/parallel_cucumber/cli.rb
66
+ - lib/parallel_cucumber/grouper.rb
67
+ - lib/parallel_cucumber/result_formatter.rb
68
+ - lib/parallel_cucumber/runner.rb
69
+ - lib/parallel_cucumber/version.rb
70
+ homepage: https://github.com/bayandin/parallel_cucumber
71
+ licenses:
72
+ - MIT
73
+ metadata: {}
74
+ post_install_message:
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubyforge_project:
90
+ rubygems_version: 2.4.8
91
+ signing_key:
92
+ specification_version: 4
93
+ summary: Run cucumber in parallel
94
+ test_files: []
95
+ has_rdoc: