Commit 775ae9f9 authored by Dan Davison's avatar Dan Davison

Merge branch 'qa-ml-feature-flag-command-line' into 'master'

[QA] Toggle Gitaly N+1 detector via feature flag and run tests

Closes gitlab-qa#374

See merge request gitlab-org/gitlab-ce!26060
parents 743c43e4 67c38a65
...@@ -55,16 +55,19 @@ You can also supply specific tests to run as another parameter. For example, to ...@@ -55,16 +55,19 @@ You can also supply specific tests to run as another parameter. For example, to
run the repository-related specs, you can execute: run the repository-related specs, you can execute:
``` ```
bin/qa Test::Instance::All http://localhost qa/specs/features/repository/ bin/qa Test::Instance::All http://localhost -- qa/specs/features/browser_ui/3_create/repository
``` ```
Since the arguments would be passed to `rspec`, you could use all `rspec` Since the arguments would be passed to `rspec`, you could use all `rspec`
options there. For example, passing `--backtrace` and also line number: options there. For example, passing `--backtrace` and also line number:
``` ```
bin/qa Test::Instance::All http://localhost qa/specs/features/project/create_spec.rb:3 --backtrace bin/qa Test::Instance::All http://localhost -- qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb:6 --backtrace
``` ```
Note that the separator `--` is required; all subsequent options will be
ignored by the QA framework and passed to `rspec`.
### Overriding the authenticated user ### Overriding the authenticated user
Unless told otherwise, the QA tests will run as the default `root` user seeded Unless told otherwise, the QA tests will run as the default `root` user seeded
...@@ -117,7 +120,7 @@ tests that are expected to fail while a fix is in progress (similar to how ...@@ -117,7 +120,7 @@ tests that are expected to fail while a fix is in progress (similar to how
can be used). can be used).
``` ```
bin/qa Test::Instance::All http://localhost --tag quarantine bin/qa Test::Instance::All http://localhost -- --tag quarantine
``` ```
If `quarantine` is used with other tags, tests will only be run if they have at If `quarantine` is used with other tags, tests will only be run if they have at
...@@ -128,3 +131,25 @@ For example, suppose one test has `:smoke` and `:quarantine` metadata, and ...@@ -128,3 +131,25 @@ For example, suppose one test has `:smoke` and `:quarantine` metadata, and
another test has `:ldap` and `:quarantine` metadata. If the tests are run with another test has `:ldap` and `:quarantine` metadata. If the tests are run with
`--tag smoke --tag quarantine`, only the first test will run. The test with `--tag smoke --tag quarantine`, only the first test will run. The test with
`:ldap` will not run even though it also has `:quarantine`. `:ldap` will not run even though it also has `:quarantine`.
### Running tests with a feature flag enabled
Tests can be run with with a feature flag enabled by using the command-line
option `--enable-feature FEATURE_FLAG`. For example, to enable the feature flag
that enforces Gitaly request limits, you would use the command:
```
bin/qa Test::Instance::All http://localhost --enable-feature gitaly_enforce_requests_limits
```
This will instruct the QA framework to enable the `gitaly_enforce_requests_limits`
feature flag ([via the API](https://docs.gitlab.com/ee/api/features.html)), run
all the tests in the `Test::Instance::All` scenario, and then disable the
feature flag again.
Note: the QA framework doesn't currently allow you to easily toggle a feature
flag during a single test, [as you can in unit tests](https://docs.gitlab.com/ee/development/feature_flags.html#specs),
but [that capability is planned](https://gitlab.com/gitlab-org/quality/team-tasks/issues/77).
Note also that the `--` separator isn't used because `--enable-feature` is a QA
framework option, not an `rspec` option.
\ No newline at end of file
...@@ -17,6 +17,7 @@ module QA ...@@ -17,6 +17,7 @@ module QA
autoload :Env, 'qa/runtime/env' autoload :Env, 'qa/runtime/env'
autoload :Address, 'qa/runtime/address' autoload :Address, 'qa/runtime/address'
autoload :Path, 'qa/runtime/path' autoload :Path, 'qa/runtime/path'
autoload :Feature, 'qa/runtime/feature'
autoload :Fixtures, 'qa/runtime/fixtures' autoload :Fixtures, 'qa/runtime/fixtures'
autoload :Logger, 'qa/runtime/logger' autoload :Logger, 'qa/runtime/logger'
...@@ -89,6 +90,7 @@ module QA ...@@ -89,6 +90,7 @@ module QA
autoload :Bootable, 'qa/scenario/bootable' autoload :Bootable, 'qa/scenario/bootable'
autoload :Actable, 'qa/scenario/actable' autoload :Actable, 'qa/scenario/actable'
autoload :Template, 'qa/scenario/template' autoload :Template, 'qa/scenario/template'
autoload :SharedAttributes, 'qa/scenario/shared_attributes'
## ##
# Test scenario entrypoints. # Test scenario entrypoints.
......
...@@ -8,9 +8,6 @@ module QA ...@@ -8,9 +8,6 @@ module QA
module ApiFabricator module ApiFabricator
include Capybara::DSL include Capybara::DSL
HTTP_STATUS_OK = 200
HTTP_STATUS_CREATED = 201
ResourceNotFoundError = Class.new(RuntimeError) ResourceNotFoundError = Class.new(RuntimeError)
ResourceFabricationFailedError = Class.new(RuntimeError) ResourceFabricationFailedError = Class.new(RuntimeError)
ResourceURLMissingError = Class.new(RuntimeError) ResourceURLMissingError = Class.new(RuntimeError)
......
...@@ -15,6 +15,13 @@ module QA ...@@ -15,6 +15,13 @@ module QA
@instance.to_s @instance.to_s
end end
end end
def self.valid?(value)
uri = URI.parse(value)
uri.is_a?(URI::HTTP) && !uri.host.nil?
rescue URI::InvalidURIError
false
end
end end
end end
end end
# frozen_string_literal: true
module QA
module Runtime
module Feature
extend self
extend Support::Api
SetFeatureError = Class.new(RuntimeError)
def enable(key)
QA::Runtime::Logger.info("Enabling feature: #{key}")
set_feature(key, true)
end
def disable(key)
QA::Runtime::Logger.info("Disabling feature: #{key}")
set_feature(key, false)
end
private
def api_client
@api_client ||= Runtime::API::Client.new(:gitlab)
end
def set_feature(key, value)
request = Runtime::API::Request.new(api_client, "/features/#{key}")
response = post(request.url, { value: value })
unless response.code == QA::Support::Api::HTTP_STATUS_CREATED
raise SetFeatureError, "Setting feature flag #{key} to #{value} failed with `#{response}`."
end
end
end
end
end
...@@ -23,7 +23,7 @@ module QA ...@@ -23,7 +23,7 @@ module QA
arguments.parse!(argv) arguments.parse!(argv)
self.perform(Runtime::Scenario.attributes, *arguments.default_argv) self.perform(Runtime::Scenario.attributes, *argv)
end end
private private
...@@ -33,7 +33,13 @@ module QA ...@@ -33,7 +33,13 @@ module QA
end end
def options def options
@options ||= [] # Scenario options/attributes are global. There's only ever one
# scenario at a time, but they can be inherited and we want scenarios
# to share the attributes of their ancestors. For example, `Mattermost`
# inherits from `Test::Instance::All` but if this were an instance
# variable then `Mattermost` wouldn't have access to the attributes
# in `All`
@@options ||= [] # rubocop:disable Style/ClassVars
end end
def has_attributes? def has_attributes?
......
# frozen_string_literal: true
module QA
module Scenario
module SharedAttributes
include Bootable
attribute :gitlab_address, '--address URL', 'Address of the instance to test'
attribute :enable_feature, '--enable-feature FEATURE_FLAG', 'Enable a feature before running tests'
end
end
end
...@@ -18,19 +18,44 @@ module QA ...@@ -18,19 +18,44 @@ module QA
end end
end end
def perform(address, *rspec_options) def perform(options, *args)
Runtime::Scenario.define(:gitlab_address, address) extract_address(:gitlab_address, options, args)
## ##
# Perform before hooks, which are different for CE and EE # Perform before hooks, which are different for CE and EE
# #
Runtime::Release.perform_before_hooks Runtime::Release.perform_before_hooks
Runtime::Feature.enable(options[:enable_feature]) if options.key?(:enable_feature)
Specs::Runner.perform do |specs| Specs::Runner.perform do |specs|
specs.tty = true specs.tty = true
specs.tags = self.class.focus specs.tags = self.class.focus
specs.options = rspec_options if rspec_options.any? specs.options = args if args.any?
end
ensure
Runtime::Feature.disable(options[:enable_feature]) if options.key?(:enable_feature)
end end
def extract_option(name, options, args)
option = if options.key?(name)
options[name]
else
args.shift
end
Runtime::Scenario.define(name, option)
option
end
# For backwards-compatibility, if the gitlab instance address is not
# specified as an option parsed by OptionParser, it can be specified as
# the first argument
def extract_address(name, options, args)
address = extract_option(name, options, args)
raise ::ArgumentError, "The address provided for `#{name}` is not valid: #{address}" unless Runtime::Address.valid?(address)
end end
end end
end end
......
...@@ -8,6 +8,7 @@ module QA ...@@ -8,6 +8,7 @@ module QA
module Instance module Instance
class All < Template class All < Template
include Bootable include Bootable
include SharedAttributes
end end
end end
end end
......
...@@ -8,6 +8,7 @@ module QA ...@@ -8,6 +8,7 @@ module QA
# #
class Smoke < Template class Smoke < Template
include Bootable include Bootable
include SharedAttributes
tags :smoke tags :smoke
end end
......
...@@ -9,10 +9,13 @@ module QA ...@@ -9,10 +9,13 @@ module QA
class Mattermost < Test::Instance::All class Mattermost < Test::Instance::All
tags :mattermost tags :mattermost
def perform(address, mattermost, *rspec_options) attribute :mattermost_address, '--mattermost-address URL', 'Address of the Mattermost server'
Runtime::Scenario.define(:mattermost_address, mattermost)
super(address, *rspec_options) def perform(options, *args)
extract_address(:gitlab_address, options, args)
extract_address(:mattermost_address, options, args)
super(options, *args)
end end
end end
end end
......
module QA module QA
module Support module Support
module Api module Api
HTTP_STATUS_OK = 200
HTTP_STATUS_CREATED = 201
def post(url, payload) def post(url, payload)
RestClient::Request.execute( RestClient::Request.execute(
method: :post, method: :post,
......
# frozen_string_literal: true
describe QA::Runtime::Feature do
let(:api_client) { double('QA::Runtime::API::Client') }
let(:request) { Struct.new(:url).new('http://api') }
let(:response) { Struct.new(:code).new(201) }
before do
allow(described_class).to receive(:api_client).and_return(api_client)
end
describe '.enable' do
it 'enables a feature flag' do
expect(QA::Runtime::API::Request)
.to receive(:new)
.with(api_client, "/features/a-flag")
.and_return(request)
expect(described_class)
.to receive(:post)
.with(request.url, { value: true })
.and_return(response)
subject.enable('a-flag')
end
end
describe '.disable' do
it 'disables a feature flag' do
expect(QA::Runtime::API::Request)
.to receive(:new)
.with(api_client, "/features/a-flag")
.and_return(request)
expect(described_class)
.to receive(:post)
.with(request.url, { value: false })
.and_return(response)
subject.disable('a-flag')
end
end
end
...@@ -13,6 +13,14 @@ describe QA::Runtime::Scenario do ...@@ -13,6 +13,14 @@ describe QA::Runtime::Scenario do
.to eq(my_attribute: 'some-value', another_attribute: 'another-value') .to eq(my_attribute: 'some-value', another_attribute: 'another-value')
end end
it 'replaces an existing attribute' do
subject.define(:my_attribute, 'some-value')
subject.define(:my_attribute, 'another-value')
expect(subject.my_attribute).to eq 'another-value'
expect(subject.attributes).to eq(my_attribute: 'another-value')
end
it 'raises error when attribute is not known' do it 'raises error when attribute is not known' do
expect { subject.invalid_accessor } expect { subject.invalid_accessor }
.to raise_error ArgumentError, /invalid_accessor/ .to raise_error ArgumentError, /invalid_accessor/
......
...@@ -4,14 +4,21 @@ describe QA::Scenario::Bootable do ...@@ -4,14 +4,21 @@ describe QA::Scenario::Bootable do
.include(described_class) .include(described_class)
end end
before do
allow(subject).to receive(:options).and_return([])
allow(QA::Runtime::Scenario).to receive(:attributes).and_return({})
end
it 'makes it possible to define the scenario attribute' do it 'makes it possible to define the scenario attribute' do
subject.class_eval do subject.class_eval do
attribute :something, '--something SOMETHING', 'Some attribute' attribute :something, '--something SOMETHING', 'Some attribute'
attribute :another, '--another ANOTHER', 'Some other attribute' attribute :another, '--another ANOTHER', 'Some other attribute'
end end
# If we run just this test from the command line it fails unless
# we include the command line args that we use to select this test.
expect(subject).to receive(:perform) expect(subject).to receive(:perform)
.with(something: 'test', another: 'other') .with({ something: 'test', another: 'other' })
subject.launch!(%w[--another other --something test]) subject.launch!(%w[--another other --something test])
end end
......
# frozen_string_literal: true
describe QA::Scenario::Template do
let(:feature) { spy('Runtime::Feature') }
let(:release) { spy('Runtime::Release') }
before do
stub_const('QA::Runtime::Release', release)
stub_const('QA::Runtime::Feature', feature)
allow(QA::Specs::Runner).to receive(:perform)
allow(QA::Runtime::Address).to receive(:valid?).and_return(true)
end
it 'allows a feature to be enabled' do
subject.perform({ enable_feature: 'a-feature' })
expect(feature).to have_received(:enable).with('a-feature')
end
it 'ensures an enabled feature is disabled afterwards' do
allow(QA::Specs::Runner).to receive(:perform).and_raise('failed test')
expect { subject.perform({ enable_feature: 'a-feature' }) }.to raise_error('failed test')
expect(feature).to have_received(:enable).with('a-feature')
expect(feature).to have_received(:disable).with('a-feature')
end
end
...@@ -12,7 +12,7 @@ describe QA::Scenario::Test::Integration::Github do ...@@ -12,7 +12,7 @@ describe QA::Scenario::Test::Integration::Github do
let(:tags) { [:github] } let(:tags) { [:github] }
it 'requires a GitHub access token' do it 'requires a GitHub access token' do
subject.perform('gitlab_address') subject.perform(args)
expect(env).to have_received(:require_github_access_token!) expect(env).to have_received(:require_github_access_token!)
end end
......
...@@ -4,14 +4,21 @@ describe QA::Scenario::Test::Integration::Mattermost do ...@@ -4,14 +4,21 @@ describe QA::Scenario::Test::Integration::Mattermost do
context '#perform' do context '#perform' do
it_behaves_like 'a QA scenario class' do it_behaves_like 'a QA scenario class' do
let(:args) { %w[gitlab_address mattermost_address] } let(:args) { %w[gitlab_address mattermost_address] }
let(:args) do
{
gitlab_address: 'http://gitlab_address',
mattermost_address: 'http://mattermost_address'
}
end
let(:named_options) { %w[--address http://gitlab_address --mattermost-address http://mattermost_address] }
let(:tags) { [:mattermost] } let(:tags) { [:mattermost] }
let(:options) { ['path1']} let(:options) { ['path1']}
it 'requires a GitHub access token' do it 'requires a GitHub access token' do
subject.perform(*args) subject.perform(args)
expect(attributes).to have_received(:define) expect(attributes).to have_received(:define)
.with(:mattermost_address, 'mattermost_address') .with(:mattermost_address, 'http://mattermost_address')
end end
end end
end end
......
...@@ -2,19 +2,23 @@ ...@@ -2,19 +2,23 @@
shared_examples 'a QA scenario class' do shared_examples 'a QA scenario class' do
let(:attributes) { spy('Runtime::Scenario') } let(:attributes) { spy('Runtime::Scenario') }
let(:release) { spy('Runtime::Release') }
let(:runner) { spy('Specs::Runner') } let(:runner) { spy('Specs::Runner') }
let(:release) { spy('Runtime::Release') }
let(:feature) { spy('Runtime::Feature') }
let(:args) { ['gitlab_address'] } let(:args) { { gitlab_address: 'http://gitlab_address' } }
let(:named_options) { %w[--address http://gitlab_address] }
let(:tags) { [] } let(:tags) { [] }
let(:options) { %w[path1 path2] } let(:options) { %w[path1 path2] }
before do before do
stub_const('QA::Specs::Runner', runner)
stub_const('QA::Runtime::Release', release) stub_const('QA::Runtime::Release', release)
stub_const('QA::Runtime::Scenario', attributes) stub_const('QA::Runtime::Scenario', attributes)
stub_const('QA::Specs::Runner', runner) stub_const('QA::Runtime::Feature', feature)
allow(runner).to receive(:perform).and_yield(runner) allow(runner).to receive(:perform).and_yield(runner)
allow(QA::Runtime::Address).to receive(:valid?).and_return(true)
end end
it 'responds to perform' do it 'responds to perform' do
...@@ -22,28 +26,48 @@ shared_examples 'a QA scenario class' do ...@@ -22,28 +26,48 @@ shared_examples 'a QA scenario class' do
end end
it 'sets an address of the subject' do it 'sets an address of the subject' do
subject.perform(*args) subject.perform(args)
expect(attributes).to have_received(:define).with(:gitlab_address, 'gitlab_address') expect(attributes).to have_received(:define).with(:gitlab_address, 'http://gitlab_address').at_least(:once)
end end
it 'performs before hooks' do it 'performs before hooks' do
subject.perform(*args) subject.perform(args)
expect(release).to have_received(:perform_before_hooks) expect(release).to have_received(:perform_before_hooks)
end end
it 'sets tags on runner' do it 'sets tags on runner' do
subject.perform(*args) subject.perform(args)
expect(runner).to have_received(:tags=).with(tags) expect(runner).to have_received(:tags=).with(tags)
end end
context 'specifying RSpec options' do context 'specifying RSpec options' do
it 'sets options on runner' do it 'sets options on runner' do
subject.perform(*args, *options) subject.perform(args, *options)
expect(runner).to have_received(:options=).with(options) expect(runner).to have_received(:options=).with(options)
end end
end end
context 'with named command-line options' do
it 'converts options to attributes' do
described_class.launch!(named_options)
args do |k, v|
expect(attributes).to have_received(:define).with(k, v)
end
end
it 'raises an error if the option is invalid' do
expect { described_class.launch!(['--foo']) }.to raise_error(OptionParser::InvalidOption)
end
it 'passes on options after --' do
expect(described_class).to receive(:perform).with(attributes, *%w[--tag quarantine])
described_class.launch!(named_options.push(*%w[-- --tag quarantine]))
end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment