Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for additional parallel configuration #85

Merged
merged 1 commit into from
Mar 1, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion .config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,31 @@ features_folder: 'cukes'
# egligible if you only have a few). However your scenarios must be independent,
# and the console output will be 'number of processes' * cucumber output. So
# this is best used if you are just after a simple pass/fail result.
parrellel: true
parrellel:
enable: true
# The available options are based on those the ParallelTests gem supports
#
# - found - order of finding files
# - steps - number of cucumber/spinach steps
# - scenarios - individual cucumber scenarios
# - filesize - by size of the file
# - runtime - info from runtime log
# - default - runtime when runtime log is filled otherwise filesize
#
# N.B. For cucumber the default actually seems to be number of feature files.
#
# The reason this is available is to allow you to fine tune your test runs.
# If you have a test suite which involves features that have 1 scenario, and
# others that have many, you may wish to group by scenario instead of the
# default features in order to balance what gets assigned to what processes
# https://github.com/grosser/parallel_tests#setup-for-non-rails
group_by: scenarios
# By default ParallelTests determines the number of processes to create
# based on the number of logical cores on a machine. You can experiment
# though with increasing this value to see if you can reduce the time it takes
# to complete the tests. Note, with each increase the reduction in time will
# decrease to a point where it might become detrimental
processes: 4

# Normally Capybara expects to be testing an in-process Rack application, but
# we're using it to talk to a remote host. Users of Quke can set what this
Expand Down
1 change: 1 addition & 0 deletions lib/quke.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
require "quke/cuke_runner"
require "quke/driver_registration"
require "quke/driver_configuration"
require "quke/parallel_configuration"

module Quke #:nodoc:

Expand Down
27 changes: 11 additions & 16 deletions lib/quke/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ class Configuration
# returning the config for setting up Quke to use browserstack.
attr_reader :browserstack

# Instance of +Quke::ParallelConfiguration+ which manages reading and
# returning the config for setting up Quke to use parallel tests.
#
# The instance will be populated based on what was set in the config.yml
# merged with default values.
#
# These values will then tell Quke whether to run tests in parallel, and if
# so how to setup the runs.
attr_reader :parallel

class << self
# Class level setter for the location of the config file.
#
Expand Down Expand Up @@ -45,6 +55,7 @@ def self.file_name
def initialize
@data = load_data
@browserstack = ::Quke::BrowserstackConfiguration.new(self)
@parallel = ::Quke::ParallelConfiguration.new(@data["parallel"] || {})
end

# Returns the value set for +features_folder+.
Expand Down Expand Up @@ -87,21 +98,6 @@ def headless
@data["headless"]
end

# Returns the value set for +parallel+.
#
# Tells Quke whether run the features in parallel. Depending on the number
# of cores on the host machine, it will split the features across a given
# number of processes and run them in parallel.
#
# This is great if you have a large test suite (the performance improvement
# is negligible if you only have a few). However your scenarios must be
# independent, and the console output will be 'number of processes' *
# cucumber output. This is best used if you are just after a simple
# pass/fail result.
def parallel
@data["parallel"]
end

# Return the value set for +pause+.
#
# Add a pause (in seconds) between steps so you can visually track how the
Expand Down Expand Up @@ -212,7 +208,6 @@ def default_data!(data)
"app_host" => (data["app_host"] || "").downcase.strip,
"driver" => (data["driver"] || "phantomjs").downcase.strip,
"headless" => (data["headless"].to_s.downcase.strip == "true"),
"parallel" => (data["parallel"].to_s.downcase.strip == "true"),
"pause" => (data["pause"] || "0").to_s.downcase.strip.to_i,
"stop_on_error" => (data["stop_on_error"] || "false").to_s.downcase.strip,
"max_wait_time" => (data["max_wait_time"] || Capybara.default_max_wait_time).to_s.downcase.strip.to_i,
Expand Down
37 changes: 1 addition & 36 deletions lib/quke/cuke_runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ module Quke #:nodoc:
# Handles executing Cucumber, including sorting the arguments we pass to it
class CukeRunner

# Access the arguments used by Quke when it was executed
attr_reader :args

# When an instance of CukeRunner is initialized it will take the arguments
# passed in and combine them with its own default args. Those args are a mix
# of ones specific to ParallelTests, and ones for Cucumber.
Expand All @@ -36,7 +33,7 @@ class CukeRunner
# parallel.
def initialize(passed_in_args = [])
Quke.config = Configuration.new
@args = parallel_args + test_options_args(passed_in_args)
@args = Quke.config.parallel.command_args(Quke.config.features_folder, passed_in_args)
end

# Executes ParallelTests, which in turn executes Cucumber passing in the
Expand All @@ -48,38 +45,6 @@ def run
raise StandardError, "Cucumber exited in a failed state" unless e.success?
end

private

def parallel_args
args = [
Quke.config.features_folder,
"--type", "cucumber",
"--serialize-stdout",
"--combine-stderr"
]
args += ["--single", "--quiet"] unless Quke.config.parallel
args
end

def test_options_args(passed_in_args)
# Because cucumber is called in the context of the executing project and
# not Quke it will take its arguments in the context of that location, and
# not from where the Quke currently sits. This means to Cucumber
# 'lib/features' doesn't exist, which means our env.rb never gets loaded.
# Instead we first have to determine where this file is running from when
# called, then we simply replace the last part of that result (which we
# know will be lib/quke) with lib/features. For example __dir__ returns
# '/Users/acruikshanks/projects/defra/quke/lib/quke' but we need Cucumber
# to load '/Users/acruikshanks/projects/defra/quke/lib/features'
# We then pass this full path to Cucumber so it can correctly find the
# folder holding our predefined env.rb file.
env_folder = __dir__.sub!("lib/quke", "lib/features")
[
"--test-options",
"--format pretty -r #{env_folder} -r #{Quke.config.features_folder} #{passed_in_args.join(' ')}"
]
end

end

end
56 changes: 56 additions & 0 deletions lib/quke/parallel_configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

module Quke #:nodoc:

# Manages all parallel configuration for Quke.
class ParallelConfiguration

attr_reader :enabled, :group_by, :processes

def initialize(data = {})
@enabled = (data["enable"].to_s.downcase.strip == "true")
@group_by = (data["group_by"] || "default").downcase.strip
@processes = (data["processes"] || "0").to_s.downcase.strip.to_i
end

def command_args(features_folder, additional_args = [])
args = standard_args(features_folder)
args += ["--single", "--quiet"] unless @enabled
args += ["--group-by", @group_by] unless @group_by == "default"
args += ["-n", @processes.to_s] if @processes.positive?
args + test_options_args(features_folder, additional_args)
end

private

def standard_args(features_folder)
[
features_folder,
"--type", "cucumber",
"--serialize-stdout",
"--combine-stderr"
]
end

def test_options_args(features_folder, additional_args)
# Because cucumber is called in the context of the executing project and
# not Quke it will take its arguments in the context of that location, and
# not from where the Quke currently sits. This means to Cucumber
# 'lib/features' doesn't exist, which means our env.rb never gets loaded.
# Instead we first have to determine where this file is running from when
# called, then we simply replace the last part of that result (which we
# know will be lib/quke) with lib/features. For example __dir__ returns
# '/Users/acruikshanks/projects/defra/quke/lib/quke' but we need Cucumber
# to load '/Users/acruikshanks/projects/defra/quke/lib/features'
# We then pass this full path to Cucumber so it can correctly find the
# folder holding our predefined env.rb file.
env_folder = __dir__.sub!("lib/quke", "lib/features")
[
"--test-options",
"--format pretty -r #{env_folder} -r #{features_folder} #{additional_args.join(' ')}".strip
]
end

end

end
5 changes: 5 additions & 0 deletions spec/data/.simple.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ pause: 1
stop_on_error: true
max_wait_time: 3

parallel:
enable: true
group_by: "scenarios"
processes: 4

browserstack:
username: jdoe
auth_key: 123456789ABCDE
Expand Down
23 changes: 0 additions & 23 deletions spec/quke/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,29 +74,6 @@
end
end

describe "#parallel" do
context "when NOT specified in the config file" do
it "defaults to false" do
Quke::Configuration.file_location = data_path(".no_file.yml")
expect(subject.parallel).to eq(false)
end
end

context "when specified in the config file" do
it "matches the config file" do
Quke::Configuration.file_location = data_path(".parallel.yml")
expect(subject.parallel).to eq(true)
end
end

context "when in the config file as a string" do
it "matches the config file" do
Quke::Configuration.file_location = data_path(".as_string.yml")
expect(subject.parallel).to eq(true)
end
end
end

describe "#pause" do
context "when NOT specified in the config file" do
it "defaults to 0" do
Expand Down
27 changes: 0 additions & 27 deletions spec/quke/cuke_runner_spec.rb

This file was deleted.

101 changes: 101 additions & 0 deletions spec/quke/parallel_configuration_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe Quke::ParallelConfiguration do
describe "instantiating" do
context "when `.config.yml` is blank or contains no parallel section" do
subject do
Quke::Configuration.file_location = data_path(".no_file.yml")
Quke::Configuration.new.parallel
end

it "returns an instance defaulted to blank values" do
expect(subject.enabled).to eq(false)
expect(subject.group_by).to eq("default")
expect(subject.processes).to eq(0)
end

end

context "when `.config.yml` contains a parallel section" do
subject do
Quke::Configuration.file_location = data_path(".simple.yml")
Quke::Configuration.new.parallel
end

it "returns an instance with properties that match the input" do
expect(subject.enabled).to eq(true)
expect(subject.group_by).to eq("scenarios")
expect(subject.processes).to eq(4)
end

end
end

describe "#command_args" do
let(:feature_folder) { "features" }
let(:additional_args) { ["--tags", "@wip"] }

context "when the instance has been instantiated with no data" do
subject { Quke::ParallelConfiguration.new }

it "returns an array of default args for ParallelTests" do
expect(subject.command_args(feature_folder)).to match_array(
[
feature_folder,
"--type",
"cucumber",
"--serialize-stdout",
"--combine-stderr",
"--single",
"--quiet",
"--test-options",
"--format pretty -r #{File.join(Dir.pwd, 'lib', 'features')} -r #{feature_folder}"
]
)
end

end

context "when the instance has been instantiated with parallel enabled" do
subject { Quke::ParallelConfiguration.new("enable" => "true") }

it "returns an array without the args '--single' and '--quiet'" do
args = subject.command_args(feature_folder)
expect(args).not_to include(["--single", "--quiet"])
end

end

context "when the instance has been instantiated with group_by set" do
subject { Quke::ParallelConfiguration.new("group_by" => "scenarios") }

it "returns an array with the args '--group-by' and 'scenarios'" do
args = subject.command_args(feature_folder)
expect(args).to include("--group-by", "scenarios")
end

end

context "when the instance has been instantiated with processes set" do
subject { Quke::ParallelConfiguration.new("processes" => "4") }

it "returns an array with the args '-n' and '4'" do
args = subject.command_args(feature_folder)
expect(args).to include("-n", "4")
end

end

context "when additional arguments are passed in" do
subject { Quke::ParallelConfiguration.new }

it "the last argument contains those values" do
args = subject.command_args(feature_folder, additional_args)
expect(args.last).to include(additional_args.join(" "))
end

end
end
end