Showing content from https://gitlab.com/gitlab-org/gitlab-qa/-/merge_requests/136.diff below:
diff --git a/.rubocop.yml b/.rubocop.yml index c4839d51a29eac213550ec77ac12d720d439fb1c..bad20ee4188e8f449fa6317133896f302e047217 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -28,3 +28,6 @@ Style/ModuleFunction: Style/SignalException: Enabled: false + +GitlabSecurity/PublicSend: + Enabled: false diff --git a/bin/qa b/bin/qa index 158290481cfc1214feb0ce9a9d0b86160fb6aaae..90297cd67c68432e5b33414c23a2f2eeaf4e256c 100755 --- a/bin/qa +++ b/bin/qa @@ -5,4 +5,4 @@ require 'gitlab/qa' Gitlab::QA::Scenario .const_get(ARGV.shift) - .perform(*ARGV) + .launch!(ARGV) diff --git a/gitlab-qa.gemspec b/gitlab-qa.gemspec index ffae94576336e48f779835fcf29a7ab2de7e5964..ab2725624439cc02c98d7ff1108ea64056e5bd04 100644 --- a/gitlab-qa.gemspec +++ b/gitlab-qa.gemspec @@ -1,5 +1,4 @@ -lib = File.expand_path('../lib', __FILE__) -$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +$LOAD_PATH.unshift(File.expand_path('../lib', __FILE__)).uniq! require 'gitlab/qa/version' Gem::Specification.new do |spec| @@ -18,6 +17,10 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ['lib'] + spec.add_runtime_dependency 'capybara', '~> 2.16' + spec.add_runtime_dependency 'capybara-screenshot', '~> 1.0.18' + spec.add_runtime_dependency 'selenium-webdriver', '~> 3.8.0' + # Some dependencies are pinned, to prevent new cops from breaking the CI pipelines spec.add_development_dependency 'gitlab-styles', '2.2.0' spec.add_development_dependency 'pry', '~> 0.11' diff --git a/lib/gitlab/qa.rb b/lib/gitlab/qa.rb index c15630f2ef4c7e0b72a9c15beac60e5964e0b789..b1a26d631c300ca25608260d5574fea308f78221 100644 --- a/lib/gitlab/qa.rb +++ b/lib/gitlab/qa.rb @@ -1,18 +1,23 @@ -$LOAD_PATH << File.expand_path(__dir__) +$LOAD_PATH.unshift(File.expand_path(__dir__)).uniq! module Gitlab module QA - autoload :Release, 'qa/release' + module Component + autoload :Gitlab, 'qa/component/gitlab' + autoload :LDAP, 'qa/component/ldap' + autoload :Specs, 'qa/component/specs' + autoload :Staging, 'qa/component/staging' + end module Runtime - autoload :Env, 'qa/runtime/env' + autoload :Scenario, 'qa/runtime/scenario' + autoload :Settings, 'qa/runtime/settings' end module Scenario - autoload :Actable, 'qa/scenario/actable' - autoload :Template, 'qa/scenario/template' - module Test + autoload :Template, 'qa/scenario/test/template' + module Instance autoload :Any, 'qa/scenario/test/instance/any' autoload :Image, 'qa/scenario/test/instance/image' @@ -37,18 +42,8 @@ module Gitlab end end - module Component - autoload :Gitlab, 'qa/component/gitlab' - autoload :LDAP, 'qa/component/ldap' - autoload :Specs, 'qa/component/specs' - autoload :Staging, 'qa/component/staging' - end - - module Docker - autoload :Command, 'qa/docker/command' - autoload :Engine, 'qa/docker/engine' - autoload :Shellout, 'qa/docker/shellout' - autoload :Volumes, 'qa/docker/volumes' - end + autoload :Release, 'qa/release' end end + +require 'qa/framework' diff --git a/lib/gitlab/qa/component/gitlab.rb b/lib/gitlab/qa/component/gitlab.rb index f0b36ddd46d3b7e8d5aa77a4a5c0a1b3495f1476..065149544d0bd6c1751bc74df3e5dd07c220fcdb 100644 --- a/lib/gitlab/qa/component/gitlab.rb +++ b/lib/gitlab/qa/component/gitlab.rb @@ -8,16 +8,21 @@ module Gitlab module Component class Gitlab extend Forwardable - include Scenario::Actable + include Framework::Scenario::Actable attr_reader :release, :docker attr_accessor :volumes, :network, :environment attr_writer :name + VOLUMES = { + 'config' => '/etc/gitlab', + 'data' => '/var/opt/gitlab' + }.freeze + def_delegators :release, :tag, :image, :edition def initialize - @docker = Docker::Engine.new + @docker = Framework::Docker::Engine.new @environment = {} @volumes = {} @network_aliases = [] @@ -83,7 +88,7 @@ module Gitlab command.volume(to, from, 'Z') end - File.join(Runtime::Env.logs_dir, name).tap do |logs_dir| + File.join(Runtime::Settings.logs_dir, name).tap do |logs_dir| command.volume(logs_dir, '/var/log/gitlab', 'Z') end @@ -148,7 +153,7 @@ module Gitlab class Availability def initialize(name) - @docker = Docker::Engine.new + @docker = Framework::Docker::Engine.new host = @docker.hostname port = @docker.port(name, 80).split(':').last diff --git a/lib/gitlab/qa/component/ldap.rb b/lib/gitlab/qa/component/ldap.rb index 942cfefecbd12765f5fac7f939e59e938d1acbdc..0c85400ddae54759a12025f62a2a6a5f79bcb93e 100644 --- a/lib/gitlab/qa/component/ldap.rb +++ b/lib/gitlab/qa/component/ldap.rb @@ -16,7 +16,7 @@ module Gitlab module QA module Component class LDAP - include Scenario::Actable + include Framework::Scenario::Actable LDAP_IMAGE = 'osixia/openldap'.freeze LDAP_IMAGE_TAG = 'latest'.freeze @@ -38,7 +38,7 @@ module Gitlab attr_writer :name def initialize - @docker = Docker::Engine.new + @docker = Framework::Docker::Engine.new @environment = {} @volumes = {} @network_aliases = [] @@ -150,8 +150,8 @@ module Gitlab end def set_gitlab_credentials - ::Gitlab::QA::Runtime::Env.ldap_username = username - ::Gitlab::QA::Runtime::Env.ldap_password = password + ::Gitlab::QA::Runtime::Settings.ldap_username = username + ::Gitlab::QA::Runtime::Settings.ldap_password = password end end end diff --git a/lib/gitlab/qa/component/specs.rb b/lib/gitlab/qa/component/specs.rb index f331deb5cdcefa321b72f6e2707c6e5d0d8f38b2..5b4817d77d1fd7ef94a6c8d18a3a5cbec469d37b 100644 --- a/lib/gitlab/qa/component/specs.rb +++ b/lib/gitlab/qa/component/specs.rb @@ -5,11 +5,13 @@ module Gitlab # This class represents GitLab QA specs image that is implemented in # the `qa/` directory located in GitLab CE / EE repositories. # - class Specs < Scenario::Template + class Specs + include Framework::Scenario::Actable + attr_accessor :suite, :release, :network, :args def initialize - @docker = Docker::Engine.new + @docker = Framework::Docker::Engine.new end def perform # rubocop:disable Metrics/AbcSize @@ -20,13 +22,13 @@ module Gitlab @docker.run(release.qa_image, release.tag, suite, *args) do |command| command << "-t --rm --net=#{network || 'bridge'}" - variables = Runtime::Env.variables + variables = Runtime::Settings.variables variables.each do |key, value| command.env(key, value) end command.volume('/var/run/docker.sock', '/var/run/docker.sock') - command.volume(Runtime::Env.screenshots_dir, '/home/qa/tmp') + command.volume(Runtime::Settings.screenshots_dir, '/home/qa/tmp') command.name("gitlab-specs-#{Time.now.to_i}") end end diff --git a/lib/gitlab/qa/component/staging.rb b/lib/gitlab/qa/component/staging.rb index fe3a7ff14066461f1f7b8ff005251843e9a799e8..8107d0da1b79ec02b22911ab54c3ef921852520f 100644 --- a/lib/gitlab/qa/component/staging.rb +++ b/lib/gitlab/qa/component/staging.rb @@ -50,10 +50,10 @@ module Gitlab private def request - Runtime::Env.require_qa_access_token! + Runtime::Settings.require_qa_access_token! @request ||= Net::HTTP::Get.new(@uri.path).tap do |req| - req['PRIVATE-TOKEN'] = Runtime::Env.qa_access_token + req['PRIVATE-TOKEN'] = Runtime::Settings.qa_access_token end end end diff --git a/lib/gitlab/qa/docker/command.rb b/lib/gitlab/qa/docker/command.rb deleted file mode 100644 index a3f2a105903f5efbfd7564f7789d98bdb137d06b..0000000000000000000000000000000000000000 --- a/lib/gitlab/qa/docker/command.rb +++ /dev/null @@ -1,45 +0,0 @@ -module Gitlab - module QA - module Docker - class Command - attr_reader :args - - def initialize(cmd = nil) - @args = Array(cmd) - end - - def <<(*args) - tap { @args.concat(args) } - end - - def volume(from, to, opt = :z) - tap { @args.push("--volume #{from}:#{to}:#{opt}") } - end - - def name(identity) - tap { @args.push("--name #{identity}") } - end - - def env(name, value) - tap { @args.push(%(--env #{name}="#{value}")) } - end - - def to_s - "docker #{@args.join(' ')}" - end - - def ==(other) - to_s == other.to_s - end - - def execute!(&block) - Docker::Shellout.new(self).execute!(&block) - end - - def self.execute(cmd, &block) - new(cmd).execute!(&block) - end - end - end - end -end diff --git a/lib/gitlab/qa/docker/engine.rb b/lib/gitlab/qa/docker/engine.rb deleted file mode 100644 index 7635195511902e2332a8cdc5019253af86493a15..0000000000000000000000000000000000000000 --- a/lib/gitlab/qa/docker/engine.rb +++ /dev/null @@ -1,65 +0,0 @@ -module Gitlab - module QA - module Docker - class Engine - DOCKER_HOST = ENV['DOCKER_HOST'] || 'http://localhost' - - def hostname - URI(DOCKER_HOST).host - end - - def pull(image, tag) - Docker::Command.execute("pull #{image}:#{tag}") - end - - def run(image, tag, *args) - Docker::Command.new('run').tap do |command| - yield command if block_given? - - command << "#{image}:#{tag}" - command << args if args.any? - - command.execute! - end - end - - def read_file(image, tag, path, &block) - cat_file = "run --rm --entrypoint /bin/cat #{image}:#{tag} #{path}" - Docker::Command.execute(cat_file, &block) - end - - def attach(name, &block) - Docker::Command.execute("attach --sig-proxy=false #{name}", &block) - end - - def restart(name) - Docker::Command.execute("restart #{name}") - end - - def stop(name) - Docker::Command.execute("stop #{name}") - end - - def remove(name) - Docker::Command.execute("rm -f #{name}") - end - - def network_exists?(name) - Docker::Command.execute("network inspect #{name}") - rescue Docker::Shellout::StatusError - false - else - true - end - - def network_create(name) - Docker::Command.execute("network create #{name}") - end - - def port(name, port) - Docker::Command.execute("port #{name} #{port}/tcp") - end - end - end - end -end diff --git a/lib/gitlab/qa/docker/shellout.rb b/lib/gitlab/qa/docker/shellout.rb deleted file mode 100644 index fa623fe1bc5caad0f5148cf283e9da1cb06ef872..0000000000000000000000000000000000000000 --- a/lib/gitlab/qa/docker/shellout.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'open3' - -module Gitlab - module QA - module Docker - class Shellout - StatusError = Class.new(StandardError) - - def initialize(command) - @command = command - @output = [] - - puts "Docker shell command: `#{@command}`" - end - - def execute! - raise StatusError, 'Command already executed' if @output.any? - - Open3.popen2e(@command.to_s) do |_in, out, wait| - out.each do |line| - @output.push(line) - - if block_given? - yield line, wait - else - puts line - end - end - - if wait.value.exited? && wait.value.exitstatus.nonzero? - raise StatusError, "Docker command `#{@command}` failed!" - end - end - - @output.join.chomp - end - end - end - end -end diff --git a/lib/gitlab/qa/docker/volumes.rb b/lib/gitlab/qa/docker/volumes.rb deleted file mode 100644 index be9e411dffdebc62b356ce3d6b4b3f1c8c44d80a..0000000000000000000000000000000000000000 --- a/lib/gitlab/qa/docker/volumes.rb +++ /dev/null @@ -1,25 +0,0 @@ -require 'tmpdir' - -module Gitlab - module QA - module Docker - class Volumes - VOLUMES = { 'config' => '/etc/gitlab', - 'data' => '/var/opt/gitlab' }.freeze - - def initialize(volumes = VOLUMES) - @volumes = volumes - end - - def with_temporary_volumes - # macOS's tmpdir is a symlink /var/folders -> /private/var/folders - # but Docker on macOS exposes /private and disallow exposing /var/ - # so we need to get the real tmpdir path - Dir.mktmpdir('gitlab-qa-', File.realpath(Dir.tmpdir)).tap do |dir| - yield Hash[@volumes.map { |k, v| ["#{dir}/#{k}", v] }] - end - end - end - end - end -end diff --git a/lib/gitlab/qa/framework.rb b/lib/gitlab/qa/framework.rb new file mode 100644 index 0000000000000000000000000000000000000000..56bfdf0a442b59f845833a8df6985dc9f655685b --- /dev/null +++ b/lib/gitlab/qa/framework.rb @@ -0,0 +1,46 @@ +$LOAD_PATH.unshift(File.expand_path('../', __dir__)).uniq! + +module Gitlab + module QA + module Framework + module Docker + autoload :Command, 'qa/framework/docker/command' + autoload :Engine, 'qa/framework/docker/engine' + autoload :Volumes, 'qa/framework/docker/volumes' + end + + module Factory + autoload :Base, 'qa/framework/factory/base' + autoload :Dependency, 'qa/framework/factory/dependency' + autoload :Product, 'qa/framework/factory/product' + end + + module Page + autoload :Base, 'qa/framework/page/base' + autoload :Element, 'qa/framework/page/element' + autoload :Validator, 'qa/framework/page/validator' + autoload :View, 'qa/framework/page/view' + end + + module Runtime + autoload :Address, 'qa/framework/runtime/address' + autoload :Browser, 'qa/framework/runtime/browser' + autoload :Env, 'qa/framework/runtime/env' + autoload :Scenario, 'qa/framework/runtime/scenario' + autoload :Session, 'qa/framework/runtime/session' + end + + module Scenario + autoload :Actable, 'qa/framework/scenario/actable' + autoload :Bootable, 'qa/framework/scenario/bootable' + autoload :Runner, 'qa/framework/scenario/runner' + autoload :Taggable, 'qa/framework/scenario/taggable' + autoload :Template, 'qa/framework/scenario/template' + end + + module Utils + autoload :Shellout, 'qa/framework/utils/shellout' + end + end + end +end diff --git a/lib/gitlab/qa/framework/docker/command.rb b/lib/gitlab/qa/framework/docker/command.rb new file mode 100644 index 0000000000000000000000000000000000000000..471fb02c584f99450ea63f601876966e2b529a5f --- /dev/null +++ b/lib/gitlab/qa/framework/docker/command.rb @@ -0,0 +1,47 @@ +module Gitlab + module QA + module Framework + module Docker + class Command + attr_reader :args + + def initialize(cmd = nil) + @args = Array(cmd) + end + + def <<(*args) + tap { @args.concat(args) } + end + + def volume(from, to, opt = :z) + tap { @args.push("--volume #{from}:#{to}:#{opt}") } + end + + def name(identity) + tap { @args.push("--name #{identity}") } + end + + def env(name, value) + tap { @args.push(%(--env #{name}="#{value}")) } + end + + def to_s + "docker #{@args.join(' ')}" + end + + def ==(other) + to_s == other.to_s + end + + def execute!(&block) + Framework::Utils::Shellout.new(self).execute!(&block) + end + + def self.execute(cmd, &block) + new(cmd).execute!(&block) + end + end + end + end + end +end diff --git a/lib/gitlab/qa/framework/docker/engine.rb b/lib/gitlab/qa/framework/docker/engine.rb new file mode 100644 index 0000000000000000000000000000000000000000..22d3e1b0b22b5bf131b0375c47946cfa01f8a1bc --- /dev/null +++ b/lib/gitlab/qa/framework/docker/engine.rb @@ -0,0 +1,67 @@ +module Gitlab + module QA + module Framework + module Docker + class Engine + DOCKER_HOST = ENV['DOCKER_HOST'] || 'http://localhost' + + def hostname + URI(DOCKER_HOST).host + end + + def pull(image, tag) + Command.execute("pull #{image}:#{tag}") + end + + def run(image, tag, *args) + Command.new('run').tap do |command| + yield command if block_given? + + command << "#{image}:#{tag}" + command << args if args.any? + + command.execute! + end + end + + def read_file(image, tag, path, &block) + cat_file = "run --rm --entrypoint /bin/cat #{image}:#{tag} #{path}" + Command.execute(cat_file, &block) + end + + def attach(name, &block) + Command.execute("attach --sig-proxy=false #{name}", &block) + end + + def restart(name) + Command.execute("restart #{name}") + end + + def stop(name) + Command.execute("stop #{name}") + end + + def remove(name) + Command.execute("rm -f #{name}") + end + + def network_exists?(name) + Command.execute("network inspect #{name}") + rescue Shellout::StatusError + false + else + true + end + + def network_create(name) + Command.execute("network create #{name}") + end + + def port(name, port) + Command.execute("port #{name} #{port}/tcp") + end + end + end + end + end +end diff --git a/lib/gitlab/qa/framework/docker/volumes.rb b/lib/gitlab/qa/framework/docker/volumes.rb new file mode 100644 index 0000000000000000000000000000000000000000..491e774204c527207c2ca0cc8d66d610eaf79b61 --- /dev/null +++ b/lib/gitlab/qa/framework/docker/volumes.rb @@ -0,0 +1,24 @@ +require 'tmpdir' + +module Gitlab + module QA + module Framework + module Docker + class Volumes + def initialize(volumes) + @volumes = volumes + end + + def with_temporary_volumes + # macOS's tmpdir is a symlink /var/folders -> /private/var/folders + # but Docker on macOS exposes /private and disallow exposing /var/ + # so we need to get the real tmpdir path + Dir.mktmpdir('qa-framework-', File.realpath(Dir.tmpdir)).tap do |dir| + yield Hash[@volumes.map { |k, v| ["#{dir}/#{k}", v] }] + end + end + end + end + end + end +end diff --git a/lib/gitlab/qa/framework/factory/base.rb b/lib/gitlab/qa/framework/factory/base.rb new file mode 100644 index 0000000000000000000000000000000000000000..8207128e6906f854f9fc0e5b0236d00eda9d1815 --- /dev/null +++ b/lib/gitlab/qa/framework/factory/base.rb @@ -0,0 +1,64 @@ +require 'forwardable' + +module Gitlab + module QA + module Framework + module Factory + class Base + extend SingleForwardable + + def_delegators :evaluator, :dependency, :dependencies + def_delegators :evaluator, :product, :attributes + + def fabricate!(*_args) + raise NotImplementedError + end + + def self.fabricate!(*args) + new.tap do |factory| + yield factory if block_given? + + dependencies.each do |name, signature| + Factory::Dependency.new(name, factory, signature).build! + end + + factory.fabricate!(*args) + + break Factory::Product.populate!(factory) + end + end + + def self.evaluator + @evaluator ||= Factory::Base::DSL.new(self) + end + + class DSL + attr_reader :dependencies, :attributes + + def initialize(base) + @base = base + @dependencies = {} + @attributes = {} + end + + def dependency(factory, as:, &block) + as.tap do |name| + @base.class_eval { attr_accessor name } + + Dependency::Signature.new(factory, block).tap do |signature| + @dependencies.store(name, signature) + end + end + end + + def product(attribute, &block) + Product::Attribute.new(attribute, block).tap do |signature| + @attributes.store(attribute, signature) + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/qa/framework/factory/dependency.rb b/lib/gitlab/qa/framework/factory/dependency.rb new file mode 100644 index 0000000000000000000000000000000000000000..31cc41307334f85c77b8c727762a7cb688e0cd1d --- /dev/null +++ b/lib/gitlab/qa/framework/factory/dependency.rb @@ -0,0 +1,43 @@ +module Gitlab + module QA + module Framework + module Factory + class Dependency + Signature = Struct.new(:factory, :block) + + def initialize(name, factory, signature) + @name = name + @factory = factory + @signature = signature + end + + def overridden? + !!@factory.public_send(@name) + end + + def build! + return if overridden? + + Builder.new(@signature, @factory).fabricate!.tap do |product| + @factory.public_send("#{@name}=", product) + end + end + + class Builder + def initialize(signature, caller_factory) + @factory = signature.factory + @block = signature.block + @caller_factory = caller_factory + end + + def fabricate! + @factory.fabricate! do |factory| + @block&.call(factory, @caller_factory) + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/qa/framework/factory/product.rb b/lib/gitlab/qa/framework/factory/product.rb new file mode 100644 index 0000000000000000000000000000000000000000..88ebb04407ecb661b37bc48f04b8bba790c165d0 --- /dev/null +++ b/lib/gitlab/qa/framework/factory/product.rb @@ -0,0 +1,34 @@ +require 'capybara/dsl' + +module Gitlab + module QA + module Framework + module Factory + class Product + include Capybara::DSL + + Attribute = Struct.new(:name, :block) + + def initialize + @location = current_url + end + + def visit! + visit @location + end + + def self.populate!(factory) + new.tap do |product| + factory.class.attributes.each_value do |attribute| + product.instance_exec(factory, attribute.block) do |factory, block| + value = block.call(factory) + product.define_singleton_method(attribute.name) { value } + end + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/qa/framework/page/base.rb b/lib/gitlab/qa/framework/page/base.rb new file mode 100644 index 0000000000000000000000000000000000000000..a9e31329cb7d5d31608d2c67f7d9968559a7924d --- /dev/null +++ b/lib/gitlab/qa/framework/page/base.rb @@ -0,0 +1,128 @@ +require 'capybara/dsl' + +module Gitlab + module QA + module Framework + module Page + class Base + include Capybara::DSL + include Gitlab::QA::Framework::Scenario::Actable + extend SingleForwardable + + def_delegators :evaluator, :view, :views + + def refresh + visit current_url + end + + def wait(max: 60, time: 1, reload: true) + start = Time.now + + while Time.now - start < max + result = yield + return result if result + + sleep(time) + + refresh if reload + end + + false + end + + def scroll_to(selector, text: nil) + page.execute_script <<~JS + var elements = Array.from(document.querySelectorAll('#{selector}')); + var text = '#{text}'; + + if (text.length > 0) { + elements.find(e => e.textContent === text).scrollIntoView(); + } else { + elements[0].scrollIntoView(); + } + JS + + page.within(selector) { yield } if block_given? + end + + # Returns true if successfully GETs the given URL + # Useful because `page.status_code` is unsupported by our driver, and + # we don't have access to the `response` to use `have_http_status`. + def asset_exists?(url) + page.execute_script <<~JS + xhr = new XMLHttpRequest(); + xhr.open('GET', '#{url}', true); + xhr.send(); + JS + + return false unless wait(time: 0.5, max: 60, reload: false) do + page.evaluate_script('xhr.readyState == XMLHttpRequest.DONE') + end + + page.evaluate_script('xhr.status') == 200 + end + + def find_element(name) + find(element_selector_css(name)) + end + + def all_elements(name) + all(element_selector_css(name)) + end + + def click_element(name) + find_element(name).click + end + + def fill_element(name, content) + find_element(name).set(content) + end + + def within_element(name) + page.within(element_selector_css(name)) do + yield + end + end + + def element_selector_css(name) + Page::Element.new(name).selector_css + end + + def self.path + raise NotImplementedError + end + + def self.evaluator + @evaluator ||= Page::Base::DSL.new + end + + def self.errors + if views.empty? + return ["Page class does not have views / elements defined!"] + end + + views.map(&:errors).flatten + end + + def self.elements + views.map(&:elements).flatten + end + + class DSL + attr_reader :views + + def initialize + @views = [] + end + + def view(path, &block) + Page::View.evaluate(&block).tap do |view| + @views.push(Page::View.new(path, view.elements)) + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/qa/framework/page/element.rb b/lib/gitlab/qa/framework/page/element.rb new file mode 100644 index 0000000000000000000000000000000000000000..8457dc202b6face3b23303fd245adefe8c699adb --- /dev/null +++ b/lib/gitlab/qa/framework/page/element.rb @@ -0,0 +1,36 @@ +module Gitlab + module QA + module Framework + module Page + class Element + attr_reader :name + + def initialize(name, pattern = nil) + @name = name + @pattern = pattern || selector + end + + def selector + "qa-#{@name.to_s.tr('_', '-')}" + end + + def selector_css + ".#{selector}" + end + + def expression + if @pattern.is_a?(String) + @_regexp ||= Regexp.new(Regexp.escape(@pattern)) + else + @pattern + end + end + + def matches?(line) + !!(line =~ expression) + end + end + end + end + end +end diff --git a/lib/gitlab/qa/framework/page/validator.rb b/lib/gitlab/qa/framework/page/validator.rb new file mode 100644 index 0000000000000000000000000000000000000000..8d69f5a4464635bd2a895bdcb3aa61ac89c8e89e --- /dev/null +++ b/lib/gitlab/qa/framework/page/validator.rb @@ -0,0 +1,56 @@ +module Gitlab + module QA + module Framework + module Page + class Validator + ValidationError = Class.new(StandardError) + + Error = Struct.new(:page, :message) do + def to_s + "Error: #{page} - #{message}" + end + end + + def initialize(constant) + @module = constant + end + + def constants + @consts ||= @module.constants.map do |const| + @module.const_get(const) + end + end + + def descendants + @descendants ||= constants.map do |const| + case const + when Class + const if const < ::Gitlab::QA::Framework::Page::Base + when Module + Page::Validator.new(const).descendants + end + end + + @descendants.flatten.compact + end + + def errors + [].tap do |errors| + descendants.each do |page| + page.errors.each do |message| + errors.push(Error.new(page.name, message)) + end + end + end + end + + def validate! + return if errors.none? + + raise ValidationError, 'Page views / elements validation error!' + end + end + end + end + end +end diff --git a/lib/gitlab/qa/framework/page/view.rb b/lib/gitlab/qa/framework/page/view.rb new file mode 100644 index 0000000000000000000000000000000000000000..29c08ac99c087966995dfabf34607cb9a10ebbfd --- /dev/null +++ b/lib/gitlab/qa/framework/page/view.rb @@ -0,0 +1,59 @@ +module Gitlab + module QA + module Framework + module Page + class View + attr_reader :path, :elements + + def initialize(path, elements) + @path = path + @elements = elements + end + + def pathname + @pathname ||= Pathname.new(File.join(__dir__, '../../../../../', @path)) + .cleanpath.expand_path + end + + def errors + unless pathname.readable? + return ["Missing view partial `#{pathname}`!"] + end + + ## + # Reduce required elements by streaming view and making assertions on + # elements' existence. + # + @missing ||= @elements.dup.tap do |elements| + File.foreach(pathname.to_s) do |line| + elements.reject! { |element| element.matches?(line) } + end + end + + @missing.map do |missing| + "Missing element `#{missing.name}` in `#{pathname}` view partial!" + end + end + + def self.evaluate(&block) + Page::View::DSL.new.tap do |evaluator| + evaluator.instance_exec(&block) if block_given? + end + end + + class DSL + attr_reader :elements + + def initialize + @elements = [] + end + + def element(name, pattern = nil) + @elements.push(Page::Element.new(name, pattern)) + end + end + end + end + end + end +end diff --git a/lib/gitlab/qa/framework/runtime/address.rb b/lib/gitlab/qa/framework/runtime/address.rb new file mode 100644 index 0000000000000000000000000000000000000000..462d9e64f7635c7b7bae714d3209e1742a820e7f --- /dev/null +++ b/lib/gitlab/qa/framework/runtime/address.rb @@ -0,0 +1,24 @@ +module Gitlab + module QA + module Framework + module Runtime + class Address + attr_reader :address + + def initialize(instance, page = nil) + @instance = instance + @address = host + (page.is_a?(String) ? page : page&.path) + end + + def host + if @instance.is_a?(Symbol) + Scenario.send("#{@instance}_address") + else + @instance.to_s + end + end + end + end + end + end +end diff --git a/lib/gitlab/qa/framework/runtime/browser.rb b/lib/gitlab/qa/framework/runtime/browser.rb new file mode 100644 index 0000000000000000000000000000000000000000..e0cc5f70633afb910a0cfa058afc339562c54a8d --- /dev/null +++ b/lib/gitlab/qa/framework/runtime/browser.rb @@ -0,0 +1,110 @@ +require 'rspec/core' +require 'capybara/rspec' +require 'capybara-screenshot/rspec' +require 'selenium-webdriver' + +module Gitlab + module QA + module Framework + module Runtime + class Browser + include Gitlab::QA::Framework::Scenario::Actable + + def initialize + self.class.configure! + end + + ## + # Visit a page that belongs to a GitLab instance under given address. + # + # Example: + # + # visit(:gitlab, Page::Main::Login) + # visit('http://gitlab.example/users/sign_in') + # + # In case of an address that is a symbol we will try to guess address + # based on `Runtime::Scenario#something_address`. + # + def visit(address, page = nil, &block) + Session.new(address, page).perform(&block) + end + + def self.visit(address, page = nil, &block) + new.visit(address, page, &block) + end + + def self.configure! + return if Capybara.drivers.include?(:chrome) + + Capybara.register_driver :chrome do |app| + options = Selenium::WebDriver::Chrome::Options.new + configure_defaults!(options) + configure_headless!(options) + configure_ci!(options) + + Capybara::Selenium::Driver.new( + app, + browser: :chrome, + desired_capabilities: capabilities, + options: options + ) + end + + # Keep only the screenshots generated from the last failing test suite + Capybara::Screenshot.prune_strategy = :keep_last_run + + # From https://github.com/mattheworiordan/capybara-screenshot/issues/84#issuecomment-41219326 + Capybara::Screenshot.register_driver(:chrome) do |driver, path| + driver.browser.save_screenshot(path) + end + + Capybara.configure do |config| + config.default_driver = :chrome + config.javascript_driver = :chrome + config.default_max_wait_time = 10 + # https://github.com/mattheworiordan/capybara-screenshot/issues/164 + config.save_path = File.expand_path('../../tmp', __dir__) + end + end + + def self.capabilities + Selenium::WebDriver::Remote::Capabilities.chrome( + # This enables access to logs with `page.driver.manage.get_log(:browser)` + loggingPrefs: { + browser: "ALL", + client: "ALL", + driver: "ALL", + server: "ALL" + } + ) + end + + def self.configure_defaults!(options) + options.add_argument("window-size=1240,1680") + + # Chrome won't work properly in a Docker container in sandbox mode + options.add_argument("no-sandbox") + end + + def self.configure_headless!(options) + return unless Env.chrome_headless? + + # Run headless by default unless CHROME_HEADLESS is false + options.add_argument("headless") + + # Chrome documentation says this flag is needed for now + # https://developers.google.com/web/updates/2017/04/headless-chrome#cli + options.add_argument("disable-gpu") + end + + def self.configure_ci!(options) + return unless Env.running_in_ci? + + # Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252 + options.add_argument("disable-dev-shm-usage") + end + end + end + end + end +end diff --git a/lib/gitlab/qa/framework/runtime/env.rb b/lib/gitlab/qa/framework/runtime/env.rb new file mode 100644 index 0000000000000000000000000000000000000000..a15a9206a6a534034c58a946b4d91db633912aac --- /dev/null +++ b/lib/gitlab/qa/framework/runtime/env.rb @@ -0,0 +1,20 @@ +module Gitlab + module QA + module Framework + module Runtime + module Env + extend self + + # set to 'false' to have Chrome run visibly instead of headless + def chrome_headless? + (ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i) != 0 + end + + def running_in_ci? + ENV['CI'] || ENV['CI_SERVER'] + end + end + end + end + end +end diff --git a/lib/gitlab/qa/framework/runtime/scenario.rb b/lib/gitlab/qa/framework/runtime/scenario.rb new file mode 100644 index 0000000000000000000000000000000000000000..ac1b2add6e916a0c21ca024504b3a889f75a19b2 --- /dev/null +++ b/lib/gitlab/qa/framework/runtime/scenario.rb @@ -0,0 +1,45 @@ +module Gitlab + module QA + module Framework + module Runtime + ## + # Singleton approach to global test scenario arguments. + # + module Scenario + extend self + + def attributes + @attributes ||= {} + end + + def define(attribute, value, **opts) + attribute_sym = attribute.to_sym + attributes.store(attribute_sym, value) + + return if respond_to?(attribute) + + define_singleton_method(attribute) do + attributes[attribute_sym].tap do |val| + if opts[:type] != :flag && val.to_s.empty? + raise ArgumentError, "Empty `#{attribute}` attribute!" + end + end + end + end + + def clear_attributes + @attributes = {} + end + + def respond_to_missing?(name, *) + super + end + + def method_missing(name, *) # rubocop:disable Style/MethodMissing + raise ArgumentError, "Scenario attribute `#{name}` not defined!" + end + end + end + end + end +end diff --git a/lib/gitlab/qa/framework/runtime/session.rb b/lib/gitlab/qa/framework/runtime/session.rb new file mode 100644 index 0000000000000000000000000000000000000000..6f1b61c056057c7f9934bedffa056b57cd87386d --- /dev/null +++ b/lib/gitlab/qa/framework/runtime/session.rb @@ -0,0 +1,52 @@ +require 'rspec/core' +require 'capybara/rspec' +require 'capybara-screenshot/rspec' +require 'selenium-webdriver' + +module Gitlab + module QA + module Framework + module Runtime + class Session + include Capybara::DSL + + def initialize(instance, page = nil) + @session_address = Address.new(instance, page) + end + + def url + @session_address.address + end + + def perform(&block) + visit(url) + + yield if block_given? + rescue StandardError + raise if block.nil? + + # RSpec examples will take care of screenshots on their own + # + unless block.binding.receiver.is_a?(RSpec::Core::ExampleGroup) + screenshot_and_save_page + end + + raise + ensure + clear! if block_given? + end + + ## + # Selenium allows to reset session cookies for current domain only. + # + # See gitlab-org/gitlab-qa#102 + # + def clear! + visit(url) + reset_session! + end + end + end + end + end +end diff --git a/lib/gitlab/qa/framework/scenario/actable.rb b/lib/gitlab/qa/framework/scenario/actable.rb new file mode 100644 index 0000000000000000000000000000000000000000..88afb39710310bc2b400fc0d4983c4e058aabbec --- /dev/null +++ b/lib/gitlab/qa/framework/scenario/actable.rb @@ -0,0 +1,31 @@ +module Gitlab + module QA + module Framework + module Scenario + module Actable + def self.included(base) + base.extend(ClassMethods) + end + + def act(*args, &block) + instance_exec(*args, &block) + end + + module ClassMethods + def perform(*args) + new.tap do |actor| + block_result = yield actor if block_given? + actor.perform(*args) if actor.respond_to?(:perform) + return block_result + end + end + + def act(*args, &block) + new.act(*args, &block) + end + end + end + end + end + end +end diff --git a/lib/gitlab/qa/framework/scenario/bootable.rb b/lib/gitlab/qa/framework/scenario/bootable.rb new file mode 100644 index 0000000000000000000000000000000000000000..a7d16a09566f8c5f0f44e428613df07a372a8e98 --- /dev/null +++ b/lib/gitlab/qa/framework/scenario/bootable.rb @@ -0,0 +1,55 @@ +require 'optparse' + +module Gitlab + module QA + module Framework + module Scenario + module Bootable + Option = Struct.new(:name, :arg, :type, :default, :desc) + DEFAULT_NOT_PASSED = Object.new.freeze + + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def launch!(argv = []) + return perform(*argv) unless has_attributes? + + options_parser.parse!(argv) + + perform(Runtime::Scenario.attributes, *options_parser.default_argv) + end + + def attribute(name, arg, type: String, default: DEFAULT_NOT_PASSED, desc: '') + options.push(Option.new(name, arg, type, default, desc)) + end + + def options + @options ||= [] + end + + def has_attributes? + options.any? + end + + def options_parser + @options_parser ||= + OptionParser.new do |parser| + options.to_a.each do |opt| + if opt.default != DEFAULT_NOT_PASSED + Runtime::Scenario.define(opt.name, opt.default, type: opt.type) + end + + parser.on(opt.arg, opt.desc) do |value| + Runtime::Scenario.define(opt.name, value, type: opt.type) + end + end + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/qa/framework/scenario/runner.rb b/lib/gitlab/qa/framework/scenario/runner.rb new file mode 100644 index 0000000000000000000000000000000000000000..db2029986a701c663696ff0a9334386da9afb780 --- /dev/null +++ b/lib/gitlab/qa/framework/scenario/runner.rb @@ -0,0 +1,34 @@ +require 'rspec/core' + +module Gitlab + module QA + module Framework + module Scenario + class Runner + include Gitlab::QA::Framework::Scenario::Actable + + attr_accessor :tty, :tags, :options + + def initialize + @tty = false + @tags = [] + @options = [File.expand_path('../../features', __dir__)] + end + + def perform + args = [] + args.push('--tty') if tty + tags.to_a.each { |tag| args.push(['-t', tag.to_s]) } + args.push(options) + + Framework::Runtime::Browser.configure! + + RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status| + abort if status.nonzero? + end + end + end + end + end + end +end diff --git a/lib/gitlab/qa/framework/scenario/taggable.rb b/lib/gitlab/qa/framework/scenario/taggable.rb new file mode 100644 index 0000000000000000000000000000000000000000..56765dd95f8f282bf52962e16721cc6756513a7b --- /dev/null +++ b/lib/gitlab/qa/framework/scenario/taggable.rb @@ -0,0 +1,23 @@ +module Gitlab + module QA + module Framework + module Scenario + module Taggable + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def tags(*tags) + @tags = tags # rubocop:disable Gitlab/ModuleWithInstanceVariables + end + + def focus + @tags.to_a # rubocop:disable Gitlab/ModuleWithInstanceVariables + end + end + end + end + end + end +end diff --git a/lib/gitlab/qa/framework/scenario/template.rb b/lib/gitlab/qa/framework/scenario/template.rb new file mode 100644 index 0000000000000000000000000000000000000000..6c3a70aecb9d21978366cfeb6e1428f447211110 --- /dev/null +++ b/lib/gitlab/qa/framework/scenario/template.rb @@ -0,0 +1,14 @@ +module Gitlab + module QA + module Framework + module Scenario + module Template + def self.included(base) + base.include Gitlab::QA::Framework::Scenario::Actable + base.include Gitlab::QA::Framework::Scenario::Bootable + end + end + end + end + end +end diff --git a/lib/gitlab/qa/framework/utils/shellout.rb b/lib/gitlab/qa/framework/utils/shellout.rb new file mode 100644 index 0000000000000000000000000000000000000000..e0c41254a0e8cd379b2b726fa13f1eebe4290033 --- /dev/null +++ b/lib/gitlab/qa/framework/utils/shellout.rb @@ -0,0 +1,42 @@ +require 'open3' + +module Gitlab + module QA + module Framework + module Utils + class Shellout + StatusError = Class.new(StandardError) + + def initialize(command) + @command = command + @output = [] + + puts "Command: `#{@command}`" + end + + def execute! + raise StatusError, 'Command already executed' if @output.any? + + Open3.popen2e(@command.to_s) do |_in, out, wait| + out.each do |line| + @output.push(line) + + if block_given? + yield line, wait + else + puts line + end + end + + if wait.value.exited? && wait.value.exitstatus.nonzero? + raise StatusError, "Command `#{@command}` failed!" + end + end + + @output.join.chomp + end + end + end + end + end +end diff --git a/lib/gitlab/qa/runtime/env.rb b/lib/gitlab/qa/runtime/settings.rb similarity index 99% rename from lib/gitlab/qa/runtime/env.rb rename to lib/gitlab/qa/runtime/settings.rb index 8ea4ca93c8cb8affb3dcb9e8dbe33cf0d044fae3..de5ae86b0216ad8d8b5039bd4767d38f2f590123 100644 --- a/lib/gitlab/qa/runtime/env.rb +++ b/lib/gitlab/qa/runtime/settings.rb @@ -1,7 +1,7 @@ module Gitlab module QA module Runtime - module Env + module Settings extend self ENV_VARIABLES = { diff --git a/lib/gitlab/qa/scenario/actable.rb b/lib/gitlab/qa/scenario/actable.rb deleted file mode 100644 index 23a67b09ba2a80965ffbaf968be453070241e340..0000000000000000000000000000000000000000 --- a/lib/gitlab/qa/scenario/actable.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Gitlab - module QA - module Scenario - module Actable - def act(*args, &block) - instance_exec(*args, &block) - end - - def self.included(base) - base.extend(ClassMethods) - end - - module ClassMethods - def perform - yield new if block_given? - end - - def act(*args, &block) - new.act(*args, &block) - end - end - end - end - end -end diff --git a/lib/gitlab/qa/scenario/template.rb b/lib/gitlab/qa/scenario/template.rb deleted file mode 100644 index 506004ad13915d2c6178ab44697641955a9ec1e8..0000000000000000000000000000000000000000 --- a/lib/gitlab/qa/scenario/template.rb +++ /dev/null @@ -1,18 +0,0 @@ -module Gitlab - module QA - module Scenario - class Template - def self.perform(*args) - new.tap do |scenario| - yield scenario if block_given? - return scenario.perform(*args) - end - end - - def perform(*_args) - raise NotImplementedError - end - end - end - end -end diff --git a/lib/gitlab/qa/scenario/test/instance/any.rb b/lib/gitlab/qa/scenario/test/instance/any.rb index af7ab3a6d9c7e4c68d5164b0e01d86f83ca9b759..25d420cc8f17600bf6867a87a8e895b4c68f6572 100644 --- a/lib/gitlab/qa/scenario/test/instance/any.rb +++ b/lib/gitlab/qa/scenario/test/instance/any.rb @@ -7,8 +7,10 @@ module Gitlab # Run test suite against any GitLab instance, # including staging and on-premises installation. # - class Any < Scenario::Template - def perform(edition, tag, address) + class Any + include Template + + def perform(options, edition, tag, address) release = Release.new(edition).tap do |r| r.tag = tag end diff --git a/lib/gitlab/qa/scenario/test/instance/image.rb b/lib/gitlab/qa/scenario/test/instance/image.rb index 1ddf1677f13b1e58f9f5472d41c86f76b1b0bbe5..21638b78cff1d93aad7848c2c1604107f37c0531 100644 --- a/lib/gitlab/qa/scenario/test/instance/image.rb +++ b/lib/gitlab/qa/scenario/test/instance/image.rb @@ -3,14 +3,16 @@ module Gitlab module Scenario module Test module Instance - class Image < Scenario::Template + class Image + include Template + attr_writer :volumes def initialize @volumes = {} end - def perform(release) + def perform(options, release) Component::Gitlab.perform do |gitlab| gitlab.release = release gitlab.volumes = @volumes diff --git a/lib/gitlab/qa/scenario/test/instance/staging.rb b/lib/gitlab/qa/scenario/test/instance/staging.rb index 2b57e3b3dd156ceb7ae71b2fd9c0e7502a93324b..2a06cad2d0a5e42a07ba44dd823e132069c35163 100644 --- a/lib/gitlab/qa/scenario/test/instance/staging.rb +++ b/lib/gitlab/qa/scenario/test/instance/staging.rb @@ -6,9 +6,11 @@ module Gitlab ## # Run test suite against staging.gitlab.com # - class Staging < Scenario::Template - def perform(*) - Runtime::Env.require_no_license! + class Staging + include Template + + def perform(options) + Runtime::Settings.require_no_license! release = Component::Staging.release diff --git a/lib/gitlab/qa/scenario/test/integration/geo.rb b/lib/gitlab/qa/scenario/test/integration/geo.rb index 407b9f6497a459008cb735e753b90f0fcb0485f9..2845635775cf62cea8b11e2844474c27d7b87bd1 100644 --- a/lib/gitlab/qa/scenario/test/integration/geo.rb +++ b/lib/gitlab/qa/scenario/test/integration/geo.rb @@ -3,18 +3,20 @@ module Gitlab module Scenario module Test module Integration - class Geo < Scenario::Template + class Geo + include Template + ## # rubocop:disable Lint/MissingCopEnableDirective # rubocop:disable Metrics/MethodLength # rubocop:disable Metrics/AbcSize # - def perform(release) + def perform(options, release) release = Release.new(release) raise ArgumentError, 'Geo is EE only!' unless release.ee? - Runtime::Env.require_license! + Runtime::Settings.require_license! Component::Gitlab.perform do |primary| primary.release = release diff --git a/lib/gitlab/qa/scenario/test/integration/ldap.rb b/lib/gitlab/qa/scenario/test/integration/ldap.rb index 09de8d8ae5eb4bda20f8f5509148963109db1dae..db630e8ab29a73cae715bf007844cdbb2df262ac 100644 --- a/lib/gitlab/qa/scenario/test/integration/ldap.rb +++ b/lib/gitlab/qa/scenario/test/integration/ldap.rb @@ -5,9 +5,11 @@ module Gitlab module Scenario module Test module Integration - class LDAP < Scenario::Template + class LDAP + include Template + # rubocop:disable Metrics/AbcSize - def perform(release) + def perform(options, release) Component::Gitlab.perform do |gitlab| gitlab.release = release gitlab.name = 'gitlab-ldap' diff --git a/lib/gitlab/qa/scenario/test/integration/mattermost.rb b/lib/gitlab/qa/scenario/test/integration/mattermost.rb index b9685c6241fc3d7680487e04a6f34115ac3d8b23..df4ad196ec91890ec336d2a1437f124e9954fd61 100644 --- a/lib/gitlab/qa/scenario/test/integration/mattermost.rb +++ b/lib/gitlab/qa/scenario/test/integration/mattermost.rb @@ -3,8 +3,10 @@ module Gitlab module Scenario module Test module Integration - class Mattermost < Scenario::Template - def perform(release) + class Mattermost + include Template + + def perform(options, release) Component::Gitlab.perform do |gitlab| gitlab.release = release gitlab.network = 'test' diff --git a/lib/gitlab/qa/scenario/test/omnibus/image.rb b/lib/gitlab/qa/scenario/test/omnibus/image.rb index ccc7a1b707c6a0d012a0d4dcd06efafaabd7ca39..b1c41b61e7e857c2c73421f9a7a0b3f3614f572a 100644 --- a/lib/gitlab/qa/scenario/test/omnibus/image.rb +++ b/lib/gitlab/qa/scenario/test/omnibus/image.rb @@ -3,8 +3,10 @@ module Gitlab module Scenario module Test module Omnibus - class Image < Scenario::Template - def perform(release) + class Image + include Template + + def perform(options, release) Component::Gitlab.perform do |gitlab| gitlab.release = release gitlab.network = 'bridge' diff --git a/lib/gitlab/qa/scenario/test/omnibus/update.rb b/lib/gitlab/qa/scenario/test/omnibus/update.rb index 938bb14cf1da1a6cdb7788713291b53a86d5425c..6dd81862b718c24f71d6691fedf321f40268a6f1 100644 --- a/lib/gitlab/qa/scenario/test/omnibus/update.rb +++ b/lib/gitlab/qa/scenario/test/omnibus/update.rb @@ -6,11 +6,14 @@ module Gitlab module Scenario module Test module Omnibus - class Update < Scenario::Template - def perform(next_release) + class Update + include Template + + def perform(options, next_release) next_release = Release.new(next_release) + volumes = Framework::Docker::Volumes.new(Component::Gitlab::VOLUMES) - Docker::Volumes.new.with_temporary_volumes do |volumes| + volumes.with_temporary_volumes do |volumes| Scenario::Test::Instance::Image .perform(next_release.previous_stable) do |scenario| scenario.volumes = volumes diff --git a/lib/gitlab/qa/scenario/test/omnibus/upgrade.rb b/lib/gitlab/qa/scenario/test/omnibus/upgrade.rb index e1a0ceeb87bee6ad5d979c8e157609fc7ae65404..d6b2fb1c8b47825ce29d34de1642e1369ec959bc 100644 --- a/lib/gitlab/qa/scenario/test/omnibus/upgrade.rb +++ b/lib/gitlab/qa/scenario/test/omnibus/upgrade.rb @@ -6,15 +6,19 @@ module Gitlab module Scenario module Test module Omnibus - class Upgrade < Scenario::Template - def perform(image = 'CE') + class Upgrade + include Template + + def perform(options, image = 'CE') ce_release = Release.new(image) if ce_release.ee? raise ArgumentError, 'Only CE can be upgraded to EE!' end - Docker::Volumes.new.with_temporary_volumes do |volumes| + volumes = Framework::Docker::Volumes.new(Component::Gitlab::VOLUMES) + + volumes.with_temporary_volumes do |volumes| Scenario::Test::Instance::Image .perform(ce_release) do |scenario| scenario.volumes = volumes diff --git a/lib/gitlab/qa/scenario/test/sanity/version.rb b/lib/gitlab/qa/scenario/test/sanity/version.rb index d4a08ff24a363acd74cd75ae7928d52a84bb2300..32f17ef3372f657590856d7b32a34c531dc919e3 100644 --- a/lib/gitlab/qa/scenario/test/sanity/version.rb +++ b/lib/gitlab/qa/scenario/test/sanity/version.rb @@ -11,11 +11,14 @@ module Gitlab # the window defined by `HOURS_AGO`. We perform a single API call, # so `COMMITS` needs to be a large enough value that we expect all # the commits in the time window will fit. - class Version < Scenario::Template + class Version + include Gitlab::QA::Framework::Scenario::Template + include Scenario::CommonOptions + HOURS_AGO = 24 COMMITS = 10_000 - def perform(release) + def perform(options, release) version = Component::Gitlab.perform do |gitlab| gitlab.release = release gitlab.act do diff --git a/lib/gitlab/qa/scenario/test/template.rb b/lib/gitlab/qa/scenario/test/template.rb new file mode 100644 index 0000000000000000000000000000000000000000..3425cd213aaf0c954d5f485115ed96f1f056c3fe --- /dev/null +++ b/lib/gitlab/qa/scenario/test/template.rb @@ -0,0 +1,19 @@ +require 'json' +require 'net/http' +require 'cgi' + +module Gitlab + module QA + module Scenario + module Test + module Template + def self.included(base) + base.include Gitlab::QA::Framework::Scenario::Actable + base.include Gitlab::QA::Framework::Scenario::Bootable + base.attribute :skip_pull?, '--skip-pull', type: :flag, default: false + end + end + end + end + end +end diff --git a/spec/gitlab/qa/component/gitlab_spec.rb b/spec/gitlab/qa/component/gitlab_spec.rb index 09f6ea94eaa2047e5ea67e54f7cac6d655065521..aa1097f32d50ddf08485e119536c9887b4c5f634 100644 --- a/spec/gitlab/qa/component/gitlab_spec.rb +++ b/spec/gitlab/qa/component/gitlab_spec.rb @@ -88,7 +88,7 @@ describe Gitlab::QA::Component::Gitlab do let(:docker) { spy('docker command') } before do - stub_const('Gitlab::QA::Docker::Command', docker) + stub_const('Gitlab::QA::Framework::Docker::Command', docker) allow(subject).to receive(:ensure_configured!) end @@ -120,7 +120,7 @@ describe Gitlab::QA::Component::Gitlab do end it 'bind-mounds volume with logs in an appropriate directory' do - allow(Gitlab::QA::Runtime::Env) + allow(Gitlab::QA::Runtime::Settings) .to receive(:logs_dir) .and_return('/tmp/gitlab-qa/logs') diff --git a/spec/gitlab/qa/component/specs_spec.rb b/spec/gitlab/qa/component/specs_spec.rb index 9e80b8105336a891c6630764d213c53d12683ffe..66a9c100f4a915e1c1c43c15ed95a2aab53ac510 100644 --- a/spec/gitlab/qa/component/specs_spec.rb +++ b/spec/gitlab/qa/component/specs_spec.rb @@ -2,7 +2,7 @@ describe Gitlab::QA::Component::Specs do let(:docker) { spy('docker command') } before do - stub_const('Gitlab::QA::Docker::Command', docker) + stub_const('Gitlab::QA::Framework::Docker::Command', docker) end describe '#perform' do diff --git a/spec/gitlab/qa/docker/command_spec.rb b/spec/gitlab/qa/framework/docker/command_spec.rb similarity index 90% rename from spec/gitlab/qa/docker/command_spec.rb rename to spec/gitlab/qa/framework/docker/command_spec.rb index 1698897ee5b9a54f21a33eaea5b7bd8ed0e5d5ef..a8c8c13f94e8ec79f98c2ed142237a094511f279 100644 --- a/spec/gitlab/qa/docker/command_spec.rb +++ b/spec/gitlab/qa/framework/docker/command_spec.rb @@ -1,8 +1,8 @@ -describe Gitlab::QA::Docker::Command do +describe Gitlab::QA::Framework::Docker::Command do let(:docker) { spy('docker') } before do - stub_const('Gitlab::QA::Docker::Shellout', docker) + stub_const('Gitlab::QA::Framework::Utils::Shellout', docker) end describe '#<<' do diff --git a/spec/gitlab/qa/docker/engine_spec.rb b/spec/gitlab/qa/framework/docker/engine_spec.rb similarity index 88% rename from spec/gitlab/qa/docker/engine_spec.rb rename to spec/gitlab/qa/framework/docker/engine_spec.rb index df2d40ccb78e58ec008306410ec97cdc1c357c77..abe6ed0fc4b7e984eeebc623c4e05061aab51e27 100644 --- a/spec/gitlab/qa/docker/engine_spec.rb +++ b/spec/gitlab/qa/framework/docker/engine_spec.rb @@ -1,8 +1,8 @@ -describe Gitlab::QA::Docker::Engine do +describe Gitlab::QA::Framework::Docker::Engine do let(:docker) { spy('docker') } before do - stub_const('Gitlab::QA::Docker::Shellout', docker) + stub_const('Gitlab::QA::Framework::Utils::Shellout', docker) end describe '#pull' do diff --git a/spec/gitlab/qa/framework/factory/base_spec.rb b/spec/gitlab/qa/framework/factory/base_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..e1a37aca1711cc1efa3f826722970333ee656097 --- /dev/null +++ b/spec/gitlab/qa/framework/factory/base_spec.rb @@ -0,0 +1,127 @@ +describe Gitlab::QA::Framework::Factory::Base do + let(:factory) { spy('factory') } + let(:product) { spy('product') } + + describe '.fabricate!' do + subject { Class.new(described_class) } + + before do + allow(Gitlab::QA::Framework::Factory::Product).to receive(:new).and_return(product) + allow(Gitlab::QA::Framework::Factory::Product).to receive(:populate!).and_return(product) + end + + it 'instantiates the factory and calls factory method' do + expect(subject).to receive(:new).and_return(factory) + + subject.fabricate!('something') + + expect(factory).to have_received(:fabricate!).with('something') + end + + it 'returns fabrication product' do + allow(subject).to receive(:new).and_return(factory) + + result = subject.fabricate!('something') + + expect(result).to eq product + end + + it 'yields factory before calling factory method' do + allow(subject).to receive(:new).and_return(factory) + + subject.fabricate!(&:something!) + + expect(factory).to have_received(:something!).ordered + expect(factory).to have_received(:fabricate!).ordered + end + end + + describe '.dependency' do + let(:dependency) { spy('dependency') } + + before do + stub_const('Some::MyDependency', dependency) + end + + subject do + Class.new(described_class) do + dependency(Some::MyDependency, as: :mydep, &:something!) + end + end + + it 'appends a new dependency and accessors' do + expect(subject.dependencies).to be_one + end + + it 'defines dependency accessors' do + expect(subject.new).to respond_to :mydep, :mydep= + end + + describe 'dependencies fabrication' do + let(:dependency) { double('dependency') } + let(:instance) { spy('instance') } + + subject do + Class.new(described_class) do + dependency Some::MyDependency, as: :mydep + end + end + + before do + stub_const('Some::MyDependency', dependency) + + allow(subject).to receive(:new).and_return(instance) + allow(instance).to receive(:mydep).and_return(nil) + allow(Gitlab::QA::Framework::Factory::Product).to receive(:new) + allow(Gitlab::QA::Framework::Factory::Product).to receive(:populate!) + end + + it 'builds all dependencies first' do + expect(dependency).to receive(:fabricate!).once + + subject.fabricate! + end + end + end + + describe '.product' do + subject do + Class.new(described_class) do + def fabricate! + "any" + end + + # Defined only to be stubbed + def self.find_page; end + + product :token do + find_page.do_something_on_page! + 'resulting value' + end + end + end + + it 'appends new product attribute' do + expect(subject.attributes).to be_one + expect(subject.attributes).to have_key(:token) + end + + describe 'populating fabrication product with data' do + let(:page) { spy('page') } + + before do + allow(factory).to receive(:class).and_return(subject) + allow(Gitlab::QA::Framework::Factory::Product).to receive(:new).and_return(product) + allow(product).to receive(:page).and_return(page) + allow(subject).to receive(:find_page).and_return(page) + end + + it 'populates product after fabrication' do + subject.fabricate! + + expect(product.token).to eq 'resulting value' + expect(page).to have_received(:do_something_on_page!) + end + end + end +end diff --git a/spec/gitlab/qa/framework/factory/dependency_spec.rb b/spec/gitlab/qa/framework/factory/dependency_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..b2e59c2928b3600eec8270aaae05b06886850320 --- /dev/null +++ b/spec/gitlab/qa/framework/factory/dependency_spec.rb @@ -0,0 +1,72 @@ +describe Gitlab::QA::Framework::Factory::Dependency do + let(:dependency) { spy('dependency') } + let(:factory) { spy('factory') } + let(:block) { spy('block') } + + let(:signature) do + double('signature', factory: dependency, block: block) + end + + subject do + described_class.new(:mydep, factory, signature) + end + + describe '#overridden?' do + it 'returns true if factory has overridden dependency' do + allow(factory).to receive(:mydep).and_return('something') + + expect(subject).to be_overridden + end + + it 'returns false if dependency has not been overridden' do + allow(factory).to receive(:mydep).and_return(nil) + + expect(subject).not_to be_overridden + end + end + + describe '#build!' do + context 'when dependency has been overridden' do + before do + allow(subject).to receive(:overridden?).and_return(true) + end + + it 'does not fabricate dependency' do + subject.build! + + expect(dependency).not_to have_received(:fabricate!) + end + end + + context 'when dependency has not been overridden' do + before do + allow(subject).to receive(:overridden?).and_return(false) + end + + it 'fabricates dependency' do + subject.build! + + expect(dependency).to have_received(:fabricate!) + end + + it 'sets product in the factory' do + subject.build! + + expect(factory).to have_received(:mydep=).with(dependency) + end + + context 'when receives a caller factory as block argument' do + let(:dependency) { Gitlab::QA::Framework::Factory::Base } + + it 'calls given block with dependency factory and caller factory' do + allow_any_instance_of(Gitlab::QA::Framework::Factory::Base).to receive(:fabricate!).and_return(factory) + allow(Gitlab::QA::Framework::Factory::Product).to receive(:populate!).and_return(spy('any')) + + subject.build! + + expect(block).to have_received(:call).with(an_instance_of(Gitlab::QA::Framework::Factory::Base), factory) + end + end + end + end +end diff --git a/spec/gitlab/qa/framework/factory/product_spec.rb b/spec/gitlab/qa/framework/factory/product_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..8c74fced89b998056853ff0cada2f5115337f615 --- /dev/null +++ b/spec/gitlab/qa/framework/factory/product_spec.rb @@ -0,0 +1,39 @@ +describe Gitlab::QA::Framework::Factory::Product do + let(:factory) do + Gitlab::QA::Framework::Factory::Base.new + end + + let(:attributes) do + { test: Gitlab::QA::Framework::Factory::Product::Attribute.new(:test, proc { 'returned' }) } + end + + let(:product) { spy('product') } + + before do + allow(Gitlab::QA::Framework::Factory::Base).to receive(:attributes).and_return(attributes) + end + + describe '.populate!' do + it 'returns a fabrication product and define factory attributes as its methods' do + expect(described_class).to receive(:new).and_return(product) + + result = described_class.populate!(factory) do |instance| + instance.something = 'string' + end + + expect(result).to be product + expect(result.test).to eq('returned') + end + end + + describe '.visit!' do + it 'makes it possible to visit fabrication product' do + allow_any_instance_of(described_class) + .to receive(:current_url).and_return('some url') + allow_any_instance_of(described_class) + .to receive(:visit).and_return('visited some url') + + expect(subject.visit!).to eq 'visited some url' + end + end +end diff --git a/spec/gitlab/qa/framework/page/base_spec.rb b/spec/gitlab/qa/framework/page/base_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..56933cab81020191f86afdc15733c213bae3f02f --- /dev/null +++ b/spec/gitlab/qa/framework/page/base_spec.rb @@ -0,0 +1,62 @@ +describe Gitlab::QA::Framework::Page::Base do + describe 'page helpers' do + it 'exposes helpful page helpers' do + expect(subject).to respond_to :refresh, :wait, :scroll_to + end + end + + describe '.view', 'DSL for defining view partials' do + subject do + Class.new(described_class) do + view 'path/to/some/view.html.haml' do + element :something, 'string pattern' + element :something_else, /regexp pattern/ + end + + view 'path/to/some/_partial.html.haml' do + element :another_element, 'string pattern' + end + end + end + + it 'makes it possible to define page views' do + expect(subject.views.size).to eq 2 + expect(subject.views).to all(be_an_instance_of(Gitlab::QA::Framework::Page::View)) + end + + it 'populates views objects with data about elements' do + expect(subject.elements.size).to eq 3 + expect(subject.elements).to all(be_an_instance_of(Gitlab::QA::Framework::Page::Element)) + expect(subject.elements.map(&:name)) + .to eq [:something, :something_else, :another_element] + end + end + + describe '.errors' do + let(:view) { double('view') } + + context 'when page has views and elements defined' do + before do + allow(described_class).to receive(:views) + .and_return([view]) + + allow(view).to receive(:errors).and_return(['some error']) + end + + it 'iterates views composite and returns errors' do + expect(described_class.errors).to eq ['some error'] + end + end + + context 'when page has no views and elements defined' do + before do + allow(described_class).to receive(:views).and_return([]) + end + + it 'appends an error about missing views / elements block' do + expect(described_class.errors) + .to include 'Page class does not have views / elements defined!' + end + end + end +end diff --git a/spec/gitlab/qa/framework/page/element_spec.rb b/spec/gitlab/qa/framework/page/element_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..c22e39bc4a740dcc7fe3204cdae28562d53fd776 --- /dev/null +++ b/spec/gitlab/qa/framework/page/element_spec.rb @@ -0,0 +1,51 @@ +describe Gitlab::QA::Framework::Page::Element do + describe '#selector' do + it 'transforms element name into QA-specific selector' do + expect(described_class.new(:sign_in_button).selector) + .to eq 'qa-sign-in-button' + end + end + + describe '#selector_css' do + it 'transforms element name into QA-specific clickable css selector' do + expect(described_class.new(:sign_in_button).selector_css) + .to eq '.qa-sign-in-button' + end + end + + context 'when pattern is an expression' do + subject { described_class.new(:something, /button 'Sign in'/) } + + it 'matches when there is a match' do + expect(subject.matches?("button 'Sign in'")).to be true + end + + it 'does not match if pattern is not present' do + expect(subject.matches?("button 'Sign out'")).to be false + end + end + + context 'when pattern is a string' do + subject { described_class.new(:something, 'button') } + + it 'matches when there is match' do + expect(subject.matches?('some button in the view')).to be true + end + + it 'does not match if pattern is not present' do + expect(subject.matches?('text_field :name')).to be false + end + end + + context 'when pattern is not provided' do + subject { described_class.new(:some_name) } + + it 'matches when QA specific selector is present' do + expect(subject.matches?('some qa-some-name selector')).to be true + end + + it 'does not match if QA selector is not there' do + expect(subject.matches?('some_name selector')).to be false + end + end +end diff --git a/spec/gitlab/qa/framework/page/validator_spec.rb b/spec/gitlab/qa/framework/page/validator_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..1b30990a26f45a74d06c2d1eb7873c3267b80605 --- /dev/null +++ b/spec/gitlab/qa/framework/page/validator_spec.rb @@ -0,0 +1,85 @@ +describe Gitlab::QA::Framework::Page::Validator do + before do + stub_const('Gitlab::QA::Framework::Test', Module.new) + stub_const('Gitlab::QA::Framework::Test::APage', Class.new(Gitlab::QA::Framework::Page::Base) { view('lib/gitlab/qa.rb') { element :button, 'module Gitlab' } }) + stub_const('Gitlab::QA::Framework::Test::AModule', Module.new) + stub_const('Gitlab::QA::Framework::Test::AModule::APage', Class.new(Gitlab::QA::Framework::Page::Base) { view('lib/gitlab/qa.rb') { element :button, 'module Gitlab' } }) + end + describe '#constants' do + subject do + described_class.new(Gitlab::QA::Framework::Test) + end + + it 'returns all constants that are module children' do + expect(subject.constants) + .to include Gitlab::QA::Framework::Test::APage, Gitlab::QA::Framework::Test::AModule + end + end + + describe '#descendants' do + subject do + described_class.new(Gitlab::QA::Framework::Test) + end + + it 'recursively returns all descendants that are page objects' do + expect(subject.descendants) + .to include Gitlab::QA::Framework::Test::APage, Gitlab::QA::Framework::Test::AModule::APage + end + + it 'does not return modules that aggregate page objects' do + expect(subject.descendants) + .not_to include Gitlab::QA::Framework::Test::AModule + end + end + + context 'when checking validation errors' do + let(:view) { spy('view') } + + before do + allow(Gitlab::QA::Framework::Test::AModule::APage) + .to receive(:views).and_return([view]) + end + + subject do + described_class.new(Gitlab::QA::Framework::Test) + end + + context 'when there are no validation errors' do + before do + allow(view).to receive(:errors).and_return([]) + end + + describe '#errors' do + it 'does not return errors' do + expect(subject.errors).to be_empty + end + end + + describe '#validate!' do + it 'does not raise error' do + expect { subject.validate! }.not_to raise_error + end + end + end + + context 'when there are validation errors' do + before do + allow(view).to receive(:errors) + .and_return(['some error', 'another error']) + end + + describe '#errors' do + it 'returns errors' do + expect(subject.errors.count).to eq 2 + end + end + + describe '#validate!' do + it 'raises validation error' do + expect { subject.validate! } + .to raise_error described_class::ValidationError + end + end + end + end +end diff --git a/spec/gitlab/qa/framework/page/view_spec.rb b/spec/gitlab/qa/framework/page/view_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..089fa763e970c65edba13be4cb56cf9b04ff9d9b --- /dev/null +++ b/spec/gitlab/qa/framework/page/view_spec.rb @@ -0,0 +1,70 @@ +describe Gitlab::QA::Framework::Page::View do + let(:element) do + double('element', name: :something, pattern: /some element/) + end + + subject { described_class.new('some/file.html', [element]) } + + describe '.evaluate' do + it 'evaluates a block and returns a DSL object' do + results = described_class.evaluate do + element :something, 'my pattern' + element :something_else, /another pattern/ + end + + expect(results.elements.size).to eq 2 + end + end + + describe '#pathname' do + it 'returns an absolute and clean path to the view' do + expect(subject.pathname.to_s).not_to include 'qa/page/' + expect(subject.pathname.to_s).to include 'some/file.html' + end + end + + describe '#errors' do + context 'when view partial is present' do + before do + allow(subject.pathname).to receive(:readable?) + .and_return(true) + end + + context 'when pattern is found' do + before do + allow(File).to receive(:foreach) + .and_yield('some element').once + allow(element).to receive(:matches?) + .with('some element').and_return(true) + end + + it 'walks through the view and asserts on elements existence' do + expect(subject.errors).to be_empty + end + end + + context 'when pattern has not been found' do + before do + allow(File).to receive(:foreach) + .and_yield('some element').once + allow(element).to receive(:matches?) + .with('some element').and_return(false) + end + + it 'returns an array of errors related to missing elements' do + expect(subject.errors).not_to be_empty + expect(subject.errors.first) + .to match %r{Missing element `.*` in `.*/some/file.html` view} + end + end + end + + context 'when view partial has not been found' do + it 'returns an error when it is not able to find the partial' do + expect(subject.errors).to be_one + expect(subject.errors.first) + .to match %r{Missing view partial `.*/some/file.html`!} + end + end + end +end diff --git a/spec/gitlab/qa/framework/runtime/env_spec.rb b/spec/gitlab/qa/framework/runtime/env_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..4f260ff7de58654c712989e575724b917b28aa03 --- /dev/null +++ b/spec/gitlab/qa/framework/runtime/env_spec.rb @@ -0,0 +1,58 @@ +describe Gitlab::QA::Framework::Runtime::Env do + include Support::StubENV + + describe '.chrome_headless?' do + context 'when there is an env variable set' do + it 'returns false when falsey values specified' do + stub_env('CHROME_HEADLESS', 'false') + expect(described_class).not_to be_chrome_headless + + stub_env('CHROME_HEADLESS', 'no') + expect(described_class).not_to be_chrome_headless + + stub_env('CHROME_HEADLESS', '0') + expect(described_class).not_to be_chrome_headless + end + + it 'returns true when anything else specified' do + stub_env('CHROME_HEADLESS', 'true') + expect(described_class).to be_chrome_headless + + stub_env('CHROME_HEADLESS', '1') + expect(described_class).to be_chrome_headless + + stub_env('CHROME_HEADLESS', 'anything') + expect(described_class).to be_chrome_headless + end + end + + context 'when there is no env variable set' do + it 'returns the default, true' do + stub_env('CHROME_HEADLESS', nil) + expect(described_class).to be_chrome_headless + end + end + end + + describe '.running_in_ci?' do + context 'when there is an env variable set' do + it 'returns true if CI' do + stub_env('CI', 'anything') + expect(described_class).to be_running_in_ci + end + + it 'returns true if CI_SERVER' do + stub_env('CI_SERVER', 'anything') + expect(described_class).to be_running_in_ci + end + end + + context 'when there is no env variable set' do + it 'returns true' do + stub_env('CI', nil) + stub_env('CI_SERVER', nil) + expect(described_class).not_to be_running_in_ci + end + end + end +end diff --git a/spec/gitlab/qa/framework/runtime/scenario_spec.rb b/spec/gitlab/qa/framework/runtime/scenario_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..6d489d88c5d1e771c60cca6eee1ee678755a6228 --- /dev/null +++ b/spec/gitlab/qa/framework/runtime/scenario_spec.rb @@ -0,0 +1,29 @@ +describe Gitlab::QA::Framework::Runtime::Scenario do + subject do + Module.new.extend(described_class) + end + + it 'makes it possible to define global scenario attributes' do + subject.define(:my_attribute, 'some-value') + subject.define(:another_attribute, '42') + subject.define(:last_attribute, true, type: :flag) + + expect(subject.my_attribute).to eq 'some-value' + expect(subject.another_attribute).to eq '42' + expect(subject.last_attribute).to eq(true) + expect(subject.attributes) + .to eq(my_attribute: 'some-value', another_attribute: '42', last_attribute: true) + end + + it 'raises error when attribute is not known' do + expect { subject.invalid_accessor } + .to raise_error ArgumentError, /invalid_accessor/ + end + + it 'raises error when attribute is empty' do + subject.define(:empty_attribute, '') + + expect { subject.empty_attribute } + .to raise_error ArgumentError, /empty_attribute/ + end +end diff --git a/spec/gitlab/qa/scenario/actable_spec.rb b/spec/gitlab/qa/framework/scenario/actable_spec.rb similarity index 53% rename from spec/gitlab/qa/scenario/actable_spec.rb rename to spec/gitlab/qa/framework/scenario/actable_spec.rb index caecfde264e4d063c91b1a86638b4345622dd457..ae00e6c0cce7c6d05040e04437018d5fb6624b67 100644 --- a/spec/gitlab/qa/scenario/actable_spec.rb +++ b/spec/gitlab/qa/framework/scenario/actable_spec.rb @@ -1,7 +1,7 @@ -describe Gitlab::QA::Scenario::Actable do +describe Gitlab::QA::Framework::Scenario::Actable do subject do Class.new do - include Gitlab::QA::Scenario::Actable + include Gitlab::QA::Framework::Scenario::Actable attr_accessor :something @@ -43,5 +43,33 @@ describe Gitlab::QA::Scenario::Actable do expect(result).to eq 'something' end + + context 'when class implements #perform' do + subject do + Class.new do + include Gitlab::QA::Framework::Scenario::Actable + + attr_reader :thing1, :thing2 + + def do_something(arg = nil) + @thing1 = "something" + end + + def perform(arg = nil) + @thing2 = "someone" + end + end + end + + it 'calls #perform' do + result = subject.perform do |object| + object.do_something + object + end + + expect(result.thing1).to eq 'something' + expect(result.thing2).to eq 'someone' + end + end end end diff --git a/spec/gitlab/qa/framework/scenario/bootable_spec.rb b/spec/gitlab/qa/framework/scenario/bootable_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..c3578c0929848fa4aa66ab3bacef1dc244055140 --- /dev/null +++ b/spec/gitlab/qa/framework/scenario/bootable_spec.rb @@ -0,0 +1,27 @@ +describe Gitlab::QA::Framework::Scenario::Bootable do + subject do + Class.new + .include(Gitlab::QA::Framework::Scenario::Actable) + .include(described_class) + end + + it 'makes it possible to define the scenario attribute' do + subject.class_eval do + attribute :something, '--something SOMETHING', desc: 'Some attribute' + attribute :another, '--another ANOTHER', desc: 'Some other attribute' + attribute :bool, '--bool', type: :flag, desc: 'A boolean attribute' + attribute :attr_with_default, '--foo', default: 'hello world' + end + + expect(subject).to receive(:perform) + .with(something: 'test', another: '42', bool: true, attr_with_default: 'hello world') + + subject.launch!(%w[--another 42 --something test --bool]) + end + + it 'does not require attributes to be defined' do + expect(subject).to receive(:perform).with('some', 'argv') + + subject.launch!(%w[some argv]) + end +end diff --git a/spec/gitlab/qa/framework/scenario/template_spec.rb b/spec/gitlab/qa/framework/scenario/template_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..58fddadc06a238a86740f9620f4b7a6ebef5a02f --- /dev/null +++ b/spec/gitlab/qa/framework/scenario/template_spec.rb @@ -0,0 +1,8 @@ +describe Gitlab::QA::Framework::Scenario::Template do + subject do + Class.new.include(described_class) + end + + it { expect(subject).to include(Gitlab::QA::Framework::Scenario::Actable) } + it { expect(subject).to include(Gitlab::QA::Framework::Scenario::Bootable) } +end diff --git a/spec/gitlab/qa/runtime/env_spec.rb b/spec/gitlab/qa/runtime/settings_spec.rb similarity index 98% rename from spec/gitlab/qa/runtime/env_spec.rb rename to spec/gitlab/qa/runtime/settings_spec.rb index d12c9c55d309e71b722c0695e2c87e9858cabcc9..22dd44f0e2b479db754199a36716616d269c91cf 100644 --- a/spec/gitlab/qa/runtime/env_spec.rb +++ b/spec/gitlab/qa/runtime/settings_spec.rb @@ -1,4 +1,4 @@ -describe Gitlab::QA::Runtime::Env do +describe Gitlab::QA::Runtime::Settings do describe '.screenshots_dir' do context 'when there is an env variable set' do before do diff --git a/spec/gitlab/qa/scenario/test/template_spec.rb b/spec/gitlab/qa/scenario/test/template_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..60e5512d86796f6cd75a1beefd9cee311d30f0d5 --- /dev/null +++ b/spec/gitlab/qa/scenario/test/template_spec.rb @@ -0,0 +1,19 @@ +describe Gitlab::QA::Scenario::Test::Template do + subject do + Class.new.include(described_class) + end + + describe '.skip_pull?' do + it 'defaults to false' do + subject.launch! + + expect(Gitlab::QA::Framework::Runtime::Scenario.skip_pull?).to be(false) + end + + it 'can be set to true' do + subject.launch!(['--skip-pull']) + + expect(Gitlab::QA::Framework::Runtime::Scenario.skip_pull?).to be(true) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 50e36e9f6d6be10b05cb3045dd6c3d0156a71848..fa437f787650a14245635b832a0cc1b2c58452a8 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,7 @@ require 'gitlab/qa' +Dir[File.expand_path('./support/**/*.rb', __dir__)].each { |f| require f } + RSpec.configure do |config| config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true @@ -16,4 +18,8 @@ RSpec.configure do |config| config.profile_examples = 10 config.order = :random Kernel.srand config.seed + + config.before do + Gitlab::QA::Framework::Runtime::Scenario.clear_attributes + end end diff --git a/spec/support/stub_env.rb b/spec/support/stub_env.rb new file mode 100644 index 0000000000000000000000000000000000000000..c4bb8df6c913dd9a154d909e5a866a01ede65145 --- /dev/null +++ b/spec/support/stub_env.rb @@ -0,0 +1,38 @@ +# Inspired by https://github.com/ljkbennett/stub_env/blob/master/lib/stub_env/helpers.rb +module Support + module StubENV + def stub_env(key_or_hash, value = nil) + init_stub unless env_stubbed? + + if key_or_hash.is_a? Hash + key_or_hash.each { |k, v| add_stubbed_value(k, v) } + else + add_stubbed_value key_or_hash, value + end + end + + private + + STUBBED_KEY = '__STUBBED__'.freeze + + def add_stubbed_value(key, value) # rubocop:disable Metrics/AbcSize + allow(ENV).to receive(:[]).with(key).and_return(value) + allow(ENV).to receive(:key?).with(key).and_return(true) + allow(ENV).to receive(:fetch).with(key).and_return(value) + allow(ENV).to receive(:fetch).with(key, anything) do |_, default_val| + value || default_val + end + end + + def env_stubbed? + ENV[STUBBED_KEY] + end + + def init_stub + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:key?).and_call_original + allow(ENV).to receive(:fetch).and_call_original + add_stubbed_value(STUBBED_KEY, true) + end + end +end
RetroSearch is an open source project built by @garambo
| Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4