lib/gitlab/qa/component/specs.rb



# frozen_string_literal: true
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/AbcSize

require 'securerandom'

module Gitlab
  module QA
    module Component
      ##
      # This class represents GitLab QA specs image that is implemented in
      # the `qa/` directory located in GitLab CE / EE repositories.
      #
      class Specs < Scenario::Template
        attr_accessor :suite, :release, :network, :args, :volumes, :env, :runner_network, :hostname, :additional_hosts

        def initialize
          @docker = Docker::Engine.new(stream_output: true) # stream test output directly instead of through logger
          @env = {}
          @volumes = {}
          @additional_hosts = []
          @default_volumes = { '/var/run/docker.sock' => '/var/run/docker.sock' }
          return if Runtime::Env.qa_knapsack_report_path.blank?

          @default_volumes[Runtime::Env.qa_knapsack_report_path] = File.join(
            Docker::Volumes::QA_CONTAINER_WORKDIR, 'knapsack'
          )
        end

        def perform
          if Runtime::Env.use_selenoid?
            Component::Selenoid.perform do |selenoid|
              selenoid.network = network
              selenoid.instance do
                internal_perform
              end
            end
          else
            internal_perform
          end
        end

        def internal_perform
          return Runtime::Logger.info("Skipping tests.") if skip_tests?

          raise ArgumentError unless [suite, release].all?

          docker_pull_qa_image_if_needed

          Runtime::Logger.info("Running test suite `#{suite}` for #{release.project_name}")

          name = "#{release.project_name}-qa-#{SecureRandom.hex(4)}"

          feature_flag_sets = []

          # When `args` includes `[..., "--disable-feature", "a", "--enable-feature", "b", "--set-feature-flags", "c=enable", ...]`
          # `feature_flag_sets` will be set to `[["--disable-feature", "a"], ["--enable-feature", "b"], ["--set-feature-flags", "c=enable"]]`
          # This will result in tests running three times, once with each feature flag option.
          while (index = args&.index { |x| x =~ /--.*-feature/ })
            feature_flag_sets << args.slice!(index, 2)
          end

          # When `args` do not have any feature flag options, we add [] so that test is run exactly once.
          feature_flag_sets << [] unless feature_flag_sets.any?

          feature_flag_sets.each do |feature_flag_set|
            @docker.run(image: qa_image, args: [suite, *args_with_flags(args, feature_flag_set)]) do |command|
              command << "-t --rm --net=#{network || 'bridge'}"

              unless hostname.nil?
                command << "--hostname #{hostname}"
                command.env('QA_HOSTNAME', hostname)
              end

              if Runtime::Env.docker_add_hosts.present? || additional_hosts.present?
                hosts = Runtime::Env.docker_add_hosts.concat(additional_hosts).map { |host| "--add-host=#{host} " }.join
                command << hosts # override /etc/hosts in docker container when test runs
              end

              Runtime::Env.variables.merge(env).each do |key, value|
                command.env(key, value)
              end

              command.volume(
                File.join(Runtime::Env.host_artifacts_dir, name),
                File.join(Docker::Volumes::QA_CONTAINER_WORKDIR, 'tmp')
              )

              volumes.to_h.merge(default_volumes).each { |to, from| command.volume(to, from) }

              command.name(name)
            end
          end
        end

        private

        attr_reader :default_volumes

        def docker_pull_qa_image_if_needed
          @docker.login(**release.login_params) if release.login_params

          @docker.pull(image: qa_image) unless Runtime::Env.skip_pull?
        end

        def args_with_flags(args, feature_flag_set)
          return args if feature_flag_set.empty?

          Runtime::Logger.info("Running with feature flag: #{feature_flag_set.join(' ')}")

          args_with_f = args.dup
          args_with_f.insert(1, *feature_flag_set)
        end

        def skip_tests?
          Runtime::Scenario.attributes.include?(:run_tests) && !Runtime::Scenario.run_tests
        end

        def qa_image
          if Runtime::Scenario.attributes.include?(:qa_image)
            Runtime::Scenario.qa_image
          else
            "#{release.qa_image}:#{release.qa_tag}"
          end
        end
      end
    end
  end
end
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/AbcSize