parallel_cucumber 0.1.22

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: