diff --git a/.gitlab/ci/review-apps/qa.gitlab-ci.yml b/.gitlab/ci/review-apps/qa.gitlab-ci.yml index 77291aac9075a45de86482764b827dca4ecd2302..58ceb744480f5c3433b62445da1d029d04205dc3 100644 --- a/.gitlab/ci/review-apps/qa.gitlab-ci.yml +++ b/.gitlab/ci/review-apps/qa.gitlab-ci.yml @@ -13,8 +13,8 @@ include: .test_variables: variables: - QA_DEBUG: "true" QA_GENERATE_ALLURE_REPORT: "true" + COLORIZED_LOGS: "true" GITLAB_USERNAME: "root" GITLAB_PASSWORD: "${REVIEW_APPS_ROOT_PASSWORD}" GITLAB_ADMIN_USERNAME: "root" diff --git a/qa/.gitignore b/qa/.gitignore index 3c5db4b565e79f826dc4ab150e3534186f8cc646..31ab17eeeaf47aeb5f950efd549528805fb44ac7 100644 --- a/qa/.gitignore +++ b/qa/.gitignore @@ -1,4 +1,3 @@ -tmp/ reports/ no_of_examples/ diff --git a/qa/Gemfile b/qa/Gemfile index b504d6d4e9021edf830bfc04a3db90e56b501a98..28fd1c9394dbe1170a1a6e07d8d317abbe507be1 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -2,7 +2,7 @@ source 'https://rubygems.org' -gem 'gitlab-qa', require: 'gitlab/qa' +gem 'gitlab-qa', '~> 7', require: 'gitlab/qa' gem 'activesupport', '~> 6.1.4.7' # This should stay in sync with the root's Gemfile gem 'allure-rspec', '~> 2.16.0' gem 'capybara', '~> 3.35.0' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 675d163fe9d3c2330e0fcf74c099d12eb9ad2bad..ca23c4ac39d9c6b8e1f7e1b3f630f678cf432eaf 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -121,7 +121,7 @@ GEM gitlab (4.18.0) httparty (~> 0.18) terminal-table (>= 1.5.1) - gitlab-qa (7.24.4) + gitlab-qa (7.29.1) activesupport (~> 6.1) gitlab (~> 4.18.0) http (~> 5.0) @@ -368,7 +368,7 @@ DEPENDENCIES deprecation_toolkit (~> 1.5.1) faker (~> 2.19, >= 2.19.0) fog-google (~> 1.17) - gitlab-qa + gitlab-qa (~> 7) influxdb-client (~> 1.17) knapsack (~> 4.0) octokit (~> 4.21) diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index 248c5f384388cee9690b26e9e31560556366bd35..775a5ead5f783e4b216901c8a0812669e5c7bf59 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -5,10 +5,12 @@ module QA module Page class Base - prepend Support::Page::Logging if Runtime::Env.debug? + prepend Support::Page::Logging + include Capybara::DSL include Scenario::Actable include Support::WaitForRequests + extend Validatable extend SingleForwardable diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb index fc7f8445d4e9591e31c6990c15cd939912458f3e..ba1b581b10070c9dac227e4de808ff70586542ef 100644 --- a/qa/qa/resource/base.rb +++ b/qa/qa/resource/base.rb @@ -99,13 +99,13 @@ def log_and_record_fabrication(fabrication_method, resource, parents, args) fabrication_time: fabrication_time ) - Runtime::Logger.debug do + Runtime::Logger.info do msg = ["==#{'=' * parents.size}>"] msg << "#{fabrication_http_method} a #{Rainbow(name).black.bg(:white)}" msg << resource.identifier msg << "as a dependency of #{parents.last}" if parents.any? msg << "via #{fabrication_method}" - msg << "in #{fabrication_time} seconds" + msg << "in #{fabrication_time.round(2)} seconds" msg.compact.join(' ') end @@ -161,7 +161,7 @@ def fabricate!(*_args) end def visit!(skip_resp_code_check: false) - Runtime::Logger.debug("Visiting #{Rainbow(self.class.name).black.bg(:white)} at #{web_url}") + Runtime::Logger.info("Visiting #{Rainbow(self.class.name).black.bg(:white)} at #{web_url}") # Just in case an async action is not yet complete Support::WaitForRequests.wait_for_requests(skip_resp_code_check: skip_resp_code_check) @@ -224,7 +224,7 @@ def identifier def remove_via_api! super - Runtime::Logger.debug(["Removed a #{self.class.name}", identifier].compact.join(' ')) + Runtime::Logger.info(["Removed a #{self.class.name}", identifier].compact.join(' ')) end protected diff --git a/qa/qa/resource/members.rb b/qa/qa/resource/members.rb index 0061f74cec5d95cf9e845410d7b128ffc0d59ba0..d9300f80f5dd618a325eb7b44c0bc12bd50e295d 100644 --- a/qa/qa/resource/members.rb +++ b/qa/qa/resource/members.rb @@ -9,7 +9,7 @@ module Resource module Members def add_member(user, access_level = AccessLevel::DEVELOPER) Support::Retrier.retry_until do - QA::Runtime::Logger.debug(%Q[Adding user #{user.username} to #{full_path} #{self.class.name}]) + QA::Runtime::Logger.info(%(Adding user #{user.username} to #{full_path} #{self.class.name})) response = post Runtime::API::Request.new(api_client, api_members_path).url, { user_id: user.id, access_level: access_level } break true if response.code == QA::Support::API::HTTP_STATUS_CREATED @@ -18,7 +18,7 @@ def add_member(user, access_level = AccessLevel::DEVELOPER) end def remove_member(user) - QA::Runtime::Logger.debug(%Q[Removing user #{user.username} from #{full_path} #{self.class.name}]) + QA::Runtime::Logger.info(%(Removing user #{user.username} from #{full_path} #{self.class.name})) delete Runtime::API::Request.new(api_client, "#{api_members_path}/#{user.id}").url end @@ -29,7 +29,7 @@ def list_members def invite_group(group, access_level = AccessLevel::GUEST) Support::Retrier.retry_until do - QA::Runtime::Logger.debug(%Q[Sharing #{self.class.name} with #{group.name}]) + QA::Runtime::Logger.info(%(Sharing #{self.class.name} with #{group.name})) response = post Runtime::API::Request.new(api_client, api_share_path).url, { group_id: group.id, group_access: access_level } response.code == QA::Support::API::HTTP_STATUS_CREATED diff --git a/qa/qa/resource/runner.rb b/qa/qa/resource/runner.rb index c014563671dddbce97a66057ec79240d67c812cd..278bdd1cabd35d6e20698dd96e3e1171b73f13b6 100644 --- a/qa/qa/resource/runner.rb +++ b/qa/qa/resource/runner.rb @@ -90,21 +90,19 @@ def api_delete_path "/runners/#{id}" end - def api_get_path - end + def api_get_path; end def api_post_path "/runners" end - def api_post_body - end + def api_post_body; end private def dump_logs if @docker_container.running? - @docker_container.logs { |line| QA::Runtime::Logger.debug(line) } + @docker_container.logs else QA::Runtime::Logger.debug("No runner container found named #{name}") end diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index 0db5314de4d0d24113b9fa1e3ff66cffe93a5ca2..7677a18911685f8cb4ba5d671fa13ff755a615b5 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -73,10 +73,6 @@ def default_branch ENV['QA_DEFAULT_BRANCH'] || 'main' end - def log_destination - ENV['QA_LOG_PATH'] || $stdout - end - def colorized_logs? enabled?(ENV['COLORIZED_LOGS'], default: false) end diff --git a/qa/qa/runtime/logger.rb b/qa/qa/runtime/logger.rb index 1f17146303abd728b631ae4b3c78e947e205118d..fa626eb9d16fdebfa1dfea2173ff041d97f3fa18 100644 --- a/qa/qa/runtime/logger.rb +++ b/qa/qa/runtime/logger.rb @@ -9,10 +9,14 @@ class Logger def_delegators :logger, :debug, :info, :warn, :error, :fatal, :unknown + # Global logger instance + # + # @return [ActiveSupport::Logger] def self.logger @logger ||= Gitlab::QA::TestLogger.logger( - level: Runtime::Env.debug? ? ::Logger::DEBUG : ::Logger::INFO, - source: 'QA Tests' + level: Runtime::Env.debug? ? "DEBUG" : Gitlab::QA::Runtime::Env.log_level, + source: 'QA Tests', + path: File.expand_path('../../tmp', __dir__) ) end end diff --git a/qa/qa/service/docker_run/base.rb b/qa/qa/service/docker_run/base.rb index 85c06e6c307a7edfe8a13d19d8f1d3c1b2c84dcf..53980b8e0516ec42eea72994b0a4b3cbaeb87de0 100644 --- a/qa/qa/service/docker_run/base.rb +++ b/qa/qa/service/docker_run/base.rb @@ -12,9 +12,7 @@ def initialize end def logs - shell "docker logs #{@name}" do |line| - yield " #{line.chomp}" - end + shell "docker logs #{@name}" end def network diff --git a/qa/qa/service/docker_run/gitlab_runner.rb b/qa/qa/service/docker_run/gitlab_runner.rb index 0a8ac39dabd82f3f6c579dd5d3ee304b8b8ce91b..1584b577af13ef2f216f495b0c394717b30abcb5 100644 --- a/qa/qa/service/docker_run/gitlab_runner.rb +++ b/qa/qa/service/docker_run/gitlab_runner.rb @@ -26,13 +26,13 @@ def initialize(name) end def config - @config ||= <<~END + @config ||= <<~CONFIG concurrent = 1 check_interval = 0 [session_server] session_timeout = 1800 - END + CONFIG end def register! @@ -40,15 +40,14 @@ def register! docker run -d --rm --network #{runner_network} --name #{@name} #{'-v /var/run/docker.sock:/var/run/docker.sock' if @executor == :docker} --privileged - #{@image} #{add_gitlab_tls_cert if @address.include? "https"} && docker exec --detach #{@name} sh -c "#{register_command}" + #{@image} #{add_gitlab_tls_cert if @address.include? 'https'} + && docker exec --detach #{@name} sh -c "#{register_command}" CMD wait_until_running_and_configured # Prove airgappedness - if runner_network == 'airgapped' - shell("docker exec #{@name} sh -c '#{prove_airgap}'") - end + shell("docker exec #{@name} sh -c '#{prove_airgap}'") if runner_network == 'airgapped' end def tags=(tags) @@ -66,7 +65,7 @@ def register_command args << "--registration-token #{@token}" args << if run_untagged - raise CONFLICTING_VARIABLES_MESSAGE % [:tags=, :run_untagged, run_untagged] if @tags&.any? + raise format(CONFLICTING_VARIABLES_MESSAGE, :tags=, :run_untagged, run_untagged) if @tags&.any? '--run-untagged=true' else @@ -86,7 +85,7 @@ def register_command end <<~CMD.strip - printf '#{config.chomp.gsub(/\n/, "\\n").gsub('"', '\"')}' > /etc/gitlab-runner/config.toml && + printf '#{config.chomp.gsub(/\n/, '\\n').gsub('"', '\"')}' > /etc/gitlab-runner/config.toml && gitlab-runner register \ #{args.join(' ')} && gitlab-runner run diff --git a/qa/qa/service/praefect_manager.rb b/qa/qa/service/praefect_manager.rb index 1215268919c94d9292b108cf512e19655cf1407f..8563c3656a8f6ff0ba4d22561b69a32bc12049d1 100644 --- a/qa/qa/service/praefect_manager.rb +++ b/qa/qa/service/praefect_manager.rb @@ -115,7 +115,7 @@ def stop_node(name) def node_state(name) state = "stopped" - wait_until_shell_command("docker inspect -f {{.State.Status}} #{name}") do |line| + wait_until_shell_command("docker inspect -f {{.State.Status}} #{name}", stream_progress: false) do |line| QA::Runtime::Logger.debug(line) break state = "running" if line.include?("running") break state = "paused" if line.include?("paused") @@ -164,7 +164,8 @@ def reconcile_node_with_node(target, reference) end def query_read_distribution - output = shell "docker exec #{@gitlab} bash -c 'curl -s http://localhost:9090/api/v1/query?query=gitaly_praefect_read_distribution'" do |line| + cmd = "docker exec #{@gitlab} bash -c 'curl -s http://localhost:9090/api/v1/query?query=gitaly_praefect_read_distribution'" + output = shell(cmd, stream_progress: false) do |line| QA::Runtime::Logger.debug(line) break line end diff --git a/qa/qa/service/shellout.rb b/qa/qa/service/shellout.rb index 33d1d10b5151f4bad890072fea2b2ee18f7cf352..0507ed6b0b0cf443d618dca31ddbe204c342afcd 100644 --- a/qa/qa/service/shellout.rb +++ b/qa/qa/service/shellout.rb @@ -6,34 +6,40 @@ module QA module Service module Shellout using Rainbow + CommandError = Class.new(StandardError) module_function - ## - # TODO, make it possible to use generic QA framework classes - # as a library - gitlab-org/gitlab-qa#94 - # - def shell(command, stdin_data: nil, fail_on_exception: true) - QA::Runtime::Logger.info("Executing `#{command}`".cyan) + def shell(command, stdin_data: nil, fail_on_exception: true, stream_progress: true) # rubocop:disable Metrics/CyclomaticComplexity + cmd_string = Array(command).join(' ') + + QA::Runtime::Logger.info("Executing: `#{cmd_string.cyan}`") Open3.popen2e(*command) do |stdin, out, wait| stdin.puts(stdin_data) if stdin_data stdin.close if stdin_data + + print_progress_dots = stream_progress && !Runtime::Env.running_in_ci? cmd_output = '' - if block_given? - out.each do |line| - cmd_output += line - yield line - end + out.each do |line| + cmd_output += line + yield line if block_given? + + # indicate progress for local run by printing dots + print "." if print_progress_dots end - out.each_char { |char| print char } + # add newline after progress dots + puts if print_progress_dots && !cmd_output.empty? if wait.value.exited? && wait.value.exitstatus.nonzero? && fail_on_exception - raise CommandError, "Command failed: #{command} \nCommand Output: #{cmd_output}" + Runtime::Logger.error("Command output:\n#{cmd_output.strip}") unless cmd_output.empty? + raise CommandError, "Command: `#{cmd_string}` failed! ✘" end + + Runtime::Logger.debug("Command output:\n#{cmd_output.strip}") unless cmd_output.empty? end end @@ -46,18 +52,17 @@ def sql_to_docker_exec_cmd(sql, username, password, database, host, container) def wait_until_shell_command(cmd, **kwargs) sleep_interval = kwargs.delete(:sleep_interval) || 1 + stream_progress = kwargs.delete(:stream_progress).then { |arg| arg.nil? ? true : false } Support::Waiter.wait_until(sleep_interval: sleep_interval, **kwargs) do - shell cmd do |line| + shell(cmd, stream_progress: stream_progress) do |line| break true if yield line end end end def wait_until_shell_command_matches(cmd, regex, **kwargs) - wait_until_shell_command(cmd, **kwargs) do |line| - QA::Runtime::Logger.debug(line.chomp) - + wait_until_shell_command(cmd, stream_progress: false, **kwargs) do |line| line =~ regex end end diff --git a/qa/qa/support/matchers/eventually_matcher.rb b/qa/qa/support/matchers/eventually_matcher.rb index 2fb5249d9af3265b5089e3991c2d6abeb06ab903..01d07585f576e4e7d00deafdfc72c1f916b4824b 100644 --- a/qa/qa/support/matchers/eventually_matcher.rb +++ b/qa/qa/support/matchers/eventually_matcher.rb @@ -53,7 +53,7 @@ def supports_block_expectations? def wait_and_check(actual, expectation_name) attempt = 0 - QA::Runtime::Logger.debug( + QA::Runtime::Logger.info( "Running eventually matcher with '#{operator_msg}' operator with: '#{retry_args}' arguments" ) QA::Support::Retrier.retry_until(**retry_args, log: false) do diff --git a/qa/qa/support/page/logging.rb b/qa/qa/support/page/logging.rb index bc5ee645965eee3bd474771b71cf962798f9cfda..6dfb348a347abdda4b7cbf9c7c1ae004e8278d5e 100644 --- a/qa/qa/support/page/logging.rb +++ b/qa/qa/support/page/logging.rb @@ -13,15 +13,15 @@ def assert_no_element(name) end def refresh(skip_finished_loading_check: false) - log("refreshing #{current_url}") + log("refreshing #{current_url}", :info) super end def scroll_to(selector, text: nil) - msg = "scrolling to :#{Rainbow(selector).underline.bright}" + msg = "scrolling to :#{highlight_element(selector)}" msg += " with text: #{text}" if text - log(msg) + log(msg, :info) super end @@ -39,7 +39,7 @@ def find_element(name, **kwargs) element = super - log("found :#{Rainbow(name).underline.bright}") + log("found :#{name}") element end @@ -49,41 +49,41 @@ def all_elements(name, **kwargs) elements = super - log("found #{elements.size} :#{Rainbow(name).underline.bright}") if elements + log("found #{elements.size} :#{name}") if elements elements end def check_element(name, click_by_js = nil) - log("checking :#{name}") + log("checking :#{highlight_element(name)}", :info) super end def uncheck_element(name, click_by_js = nil) - log("unchecking :#{name}") + log("unchecking :#{highlight_element(name)}", :info) super end def click_element_coordinates(name, **kwargs) - log(%Q(clicking the coordinates of :#{name})) + log(%(clicking the coordinates of :#{highlight_element(name)}), :info) super end def click_element(name, page = nil, **kwargs) - msg = ["clicking :#{Rainbow(name).underline.bright}"] + msg = ["clicking :#{highlight_element(name)}"] msg << ", expecting to be at #{page.class}" if page - msg << "with args #{kwargs}" - log(msg.compact.join(' ')) + log(msg.join(' '), :info) + log("with args #{kwargs}") super end def click_via_capybara(method, locator) - log("clicking via capybara using '#{method}(#{locator})'") + log("clicking via capybara using '#{method}(#{locator})'", :info) super end @@ -91,13 +91,13 @@ def click_via_capybara(method, locator) def fill_element(name, content) masked_content = name.to_s.match?(/token|key|password/) ? '*****' : content - log(%Q(filling :#{name} with "#{masked_content}")) + log(%(filling :#{highlight_element(name)} with "#{masked_content}"), :info) super end def select_element(name, value) - log(%Q(selecting "#{value}" in :#{name})) + log(%(selecting "#{value}" in :#{highlight_element(name)}), :info) super end @@ -121,7 +121,7 @@ def has_no_element?(name, **kwargs) def has_text?(text, **kwargs) found = super - log(%Q{has_text?('#{text}', wait: #{kwargs[:wait] || Capybara.default_max_wait_time}) returned #{found}}) + log(%(has_text?('#{text}', wait: #{kwargs[:wait] || Capybara.default_max_wait_time}) returned #{found})) found end @@ -129,7 +129,7 @@ def has_text?(text, **kwargs) def has_no_text?(text, **kwargs) found = super - log(%Q{has_no_text?('#{text}', wait: #{kwargs[:wait] || Capybara.default_max_wait_time}) returned #{found}}) + log(%(has_no_text?('#{text}', wait: #{kwargs[:wait] || Capybara.default_max_wait_time}) returned #{found})) found end @@ -173,13 +173,26 @@ def within_element_by_index(name, index) private - def log(msg) - QA::Runtime::Logger.debug(msg) + # Log message + # + # @param [String] msg + # @param [Symbol] level + # @return [void] + def log(msg, level = :debug) + QA::Runtime::Logger.public_send(level, msg) + end + + # Highlight element for enhanced logging + # + # @param [String] element + # @return [String] + def highlight_element(element) + element.to_s.underline.bright end def log_has_element_or_not(method, name, found, **kwargs) - msg = ["#{method} :#{Rainbow(name).underline.bright}"] - msg << %Q(with text "#{kwargs[:text]}") if kwargs[:text] + msg = ["#{method} :#{name}"] + msg << %(with text "#{kwargs[:text]}") if kwargs[:text] msg << "class: #{kwargs[:class]}" if kwargs[:class] msg << "(wait: #{kwargs[:wait] || Capybara.default_max_wait_time})" msg << "returned: #{found}" diff --git a/qa/spec/git/repository_spec.rb b/qa/spec/git/repository_spec.rb index a6a49f5907af780ad8ea042ed0106a21b72d2354..1b88cba2ba5b62ed44f957c74c31b92fd306e435 100644 --- a/qa/spec/git/repository_spec.rb +++ b/qa/spec/git/repository_spec.rb @@ -4,6 +4,7 @@ include QA::Support::Helpers::StubEnv shared_context 'unresolvable git directory' do + let(:logger) { instance_double(Logger, info: nil, debug: nil) } let(:repo_uri) { 'http://foo/bar.git' } let(:repo_uri_with_credentials) { 'http://root@foo/bar.git' } let(:env_vars) { [%q{HOME="temp"}] } @@ -22,6 +23,7 @@ before do stub_env('GITLAB_USERNAME', 'root') allow(repository).to receive(:tmp_home_dir).and_return(tmp_netrc_dir) + allow(QA::Runtime::Logger).to receive(:logger).and_return(logger) end around do |example| diff --git a/qa/spec/resource/base_spec.rb b/qa/spec/resource/base_spec.rb index 6dac8e0e3eee0e193dbadbde37cefe4187e553ba..8ac00b0a2944c547986e68fd3dae952717f00bc7 100644 --- a/qa/spec/resource/base_spec.rb +++ b/qa/spec/resource/base_spec.rb @@ -112,7 +112,7 @@ def self.name let(:method) { 'api' } before do - allow(QA::Runtime::Logger).to receive(:debug) + allow(QA::Runtime::Logger).to receive(:info) allow(resource).to receive(:api_support?).and_return(true) allow(resource).to receive(:fabricate_via_api!) allow(resource).to receive(:api_client) { api_client } @@ -123,7 +123,7 @@ def self.name subject.fabricate_via_api!('something', resource: resource, parents: []) - expect(QA::Runtime::Logger).to have_received(:debug) do |&msg| + expect(QA::Runtime::Logger).to have_received(:info) do |&msg| expect(msg.call).to match_regex(log_regex) end end @@ -155,7 +155,7 @@ def self.name let(:method) { 'browser_ui' } before do - allow(QA::Runtime::Logger).to receive(:debug) + allow(QA::Runtime::Logger).to receive(:info) end it 'logs the resource and build method' do @@ -163,7 +163,7 @@ def self.name subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) - expect(QA::Runtime::Logger).to have_received(:debug) do |&msg| + expect(QA::Runtime::Logger).to have_received(:info) do |&msg| expect(msg.call).to match_regex(log_regex) end end diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb index 22603497019be3beb868b988b1bfc81e3089302c..7efcd9bb8d9b292bef47fdfabbd1637bd838e927 100644 --- a/qa/spec/runtime/env_spec.rb +++ b/qa/spec/runtime/env_spec.rb @@ -264,20 +264,6 @@ end end - describe '.log_destination' do - it 'returns $stdout if QA_LOG_PATH is not defined' do - stub_env('QA_LOG_PATH', nil) - - expect(described_class.log_destination).to eq($stdout) - end - - it 'returns the path if QA_LOG_PATH is defined' do - stub_env('QA_LOG_PATH', 'path/to_file') - - expect(described_class.log_destination).to eq('path/to_file') - end - end - describe '.can_test?' do it_behaves_like 'boolean method with parameter', method: :can_test?, diff --git a/qa/spec/runtime/logger_spec.rb b/qa/spec/runtime/logger_spec.rb index f0fcfa0564ec1a0201cfdd7473c91ca46420bc6e..652037a7041ccd48f201529625e8dd9665c854dd 100644 --- a/qa/spec/runtime/logger_spec.rb +++ b/qa/spec/runtime/logger_spec.rb @@ -2,6 +2,6 @@ RSpec.describe QA::Runtime::Logger do it 'returns logger instance' do - expect(described_class.logger).to be_an_instance_of(::Logger) + expect(described_class.logger).to be_an_instance_of(ActiveSupport::Logger) end end diff --git a/qa/tmp/.gitignore b/qa/tmp/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..cec9082b6d68f3fdd70f2fef2818bf74ec6ef362 --- /dev/null +++ b/qa/tmp/.gitignore @@ -0,0 +1,3 @@ +* + +!.gitignore