From 388228c8fe41bab463f36c34c0b936e27bf44f48 Mon Sep 17 00:00:00 2001 From: Andrejs Cunskis <acunskis@gitlab.com> Date: Tue, 1 Oct 2024 01:26:21 +0000 Subject: [PATCH] Prevent coverband from starting with default config Improve error messaging in tests Remove unnecessary CI variable definition Fetch only runtime coverage data --- .gitlab/ci/test-on-gdk/main.gitlab-ci.yml | 2 +- config/coverband.rb | 15 +++ config/initializers/coverband.rb | 16 +--- lib/api/internal/coverage.rb | 10 +- .../support/formatters/coverband_formatter.rb | 58 ++++++------ .../formatters/coverband_formatter_spec.rb | 93 ++++++++----------- spec/requests/api/internal/coverage_spec.rb | 6 +- 7 files changed, 102 insertions(+), 98 deletions(-) create mode 100644 config/coverband.rb diff --git a/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml b/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml index 7b3b115ad1a81..b182643850812 100644 --- a/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml +++ b/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml @@ -83,7 +83,7 @@ include: GITLAB_QA_ADMIN_ACCESS_TOKEN: $QA_ADMIN_ACCESS_TOKEN FF_NETWORK_PER_BUILD: "true" RSPEC_LAST_RUN_RESULTS_FILE: "$CI_PROJECT_DIR/qa/tmp/examples.txt" - COVERBAND_ENABLED: "$COVERBAND_ENABLED" + COVERBAND_ENABLED: "$COVERBAND_ENABLED" # explicitly define variable so it is passed in to gdk service container before_script: - echo "SUITE_RAN=true" > "$QA_SUITE_STATUS_ENV_FILE" - echo -e "\e[0Ksection_start:`date +%s`:install_gems[collapsed=true]\r\e[0KInstall gems" diff --git a/config/coverband.rb b/config/coverband.rb new file mode 100644 index 0000000000000..8cf99014f569d --- /dev/null +++ b/config/coverband.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# This file is loaded from 'initializers/coverband.rb' if coverband is enabled + +Coverband.configure do |config| + config.store = Coverband::Adapters::RedisStore.new(Gitlab::Redis::SharedState.redis) + config.background_reporting_sleep_seconds = 1 + config.reporting_wiggle = 0 # Since this is not run in production disable wiggle and report every second. + config.ignore += %w[spec/.* lib/tasks/.* + config/application.rb config/boot.rb config/initializers/.* db/post_migrate/.* + config/puma.rb bin/.* config/environments/.* db/migrate/.*] + + config.verbose = false # this spams logfile a lot, set to true for debugging locally + config.logger = Gitlab::AppLogger.primary_logger +end diff --git a/config/initializers/coverband.rb b/config/initializers/coverband.rb index aed72a3ff61e0..98ef0c08e36b8 100644 --- a/config/initializers/coverband.rb +++ b/config/initializers/coverband.rb @@ -3,17 +3,7 @@ # Configuration used by coverband gem when "COVERBAND_ENABLED" is set to "true" return unless Gitlab::Utils.to_boolean(ENV['COVERBAND_ENABLED'], default: false) -require 'coverband' - -Coverband.configure do |config| - config.store = Coverband::Adapters::RedisStore.new(Gitlab::Redis::SharedState.redis) - config.background_reporting_sleep_seconds = 1 - config.reporting_wiggle = nil # Since this is not run in production disable wiggle and report every second. - config.ignore += %w[spec/.* lib/tasks/.* - config/application.rb config/boot.rb config/initializers/.* db/post_migrate/.* - config/puma.rb bin/.* config/environments/.* db/migrate/.*] +# Set coverband config file name so it is not started with default configuration +ENV["COVERBAND_CONFIG"] = Rails.root.join("config/coverband.rb").to_s - config.verbose = true - config.csp_policy = true - config.logger = Gitlab::AppLogger -end +require 'coverband' diff --git a/lib/api/internal/coverage.rb b/lib/api/internal/coverage.rb index cba50280e5d6e..e2fbbd9897d7c 100644 --- a/lib/api/internal/coverage.rb +++ b/lib/api/internal/coverage.rb @@ -22,12 +22,18 @@ class Coverage < ::API::Base end get do - coverage = ::Coverband.configuration.store.coverage - coverage&.keys + # Fetch only runtime coverage data which is tracked during E2E spec execution + # skip hash check due to hash mismatch on some environments which results in empty coverage data + ::Coverband.configuration.store.coverage(::Coverband::RUNTIME_TYPE, skip_hash_check: true).keys end delete do ::Coverband.configuration.store.clear! + + status 200 + { + message: "Cleared source code paths coverage mapping" + } end end end diff --git a/qa/qa/support/formatters/coverband_formatter.rb b/qa/qa/support/formatters/coverband_formatter.rb index d4c669744d1f7..2e3659e59d6e5 100644 --- a/qa/qa/support/formatters/coverband_formatter.rb +++ b/qa/qa/support/formatters/coverband_formatter.rb @@ -9,13 +9,13 @@ module Formatters class CoverbandFormatter < ::RSpec::Core::Formatters::BaseFormatter include Support::API - COVERAGE_API_PATH = '/api/v4/internal/coverage' - def initialize(output) super + @test_mapping = Hash.new { |hsh, key| hsh[key] = [] } @logger = Runtime::Logger.logger @headers_access_token = { "PRIVATE-TOKEN" => Runtime::Env.admin_personal_access_token } + @cov_api_endpoint = "#{Runtime::Scenario.gitlab_address}/api/v4/internal/coverage" end ::RSpec::Core::Formatters.register( @@ -30,45 +30,49 @@ def initialize(output) # @param [RSpec::Core::Notifications::ExamplesNotification] notification # @return [void] def stop(_notification) - logger.info("Saving test coverage mapping json file") - save_test_mapping end # Example start event - def example_started(_example_notification) - QA::Support::Retrier.retry_until(max_attempts: 5, sleep_interval: 1, message: "Retry clear coverage") do - resp = delete("#{Runtime::Scenario.gitlab_address}#{COVERAGE_API_PATH}", - headers: headers_access_token) + def example_started(example_notification) + return if example_notification.example.metadata[:skip] - logger.error("Failed to clear coverage, code: #{resp.code}, body: #{resp.body}") if resp.code != 200 + response = nil + QA::Support::Retrier.retry_until(max_attempts: 5, sleep_interval: 1) do + response = delete(cov_api_endpoint, headers: headers_access_token) + next true if response.code == 200 - resp.code == 200 + logger.debug("Failed to clear coverage, code: #{response.code}, body: #{response.body}") + false end - logger.debug("Cleared coverage data before example starts") - rescue StandardError => e - logger.error("Failed to clear coverage. Exception trace: #{e}") + logger.info("Cleared coverage data") + rescue StandardError + logger.error("Failed to clear coverage, code: #{response.code}, body: #{response.body}") end # Example finish event def example_finished(example_notification) - cov_resp = nil - QA::Support::Retrier.retry_until(max_attempts: 10, sleep_interval: 2, message: "Retry fetch coverage") do - cov_resp = get("#{Runtime::Scenario.gitlab_address}/api/v4/internal/coverage", - headers: headers_access_token) + return if example_notification.example.metadata[:skip] || example_failed?(example_notification) + + response = nil + QA::Support::Retrier.retry_until(max_attempts: 10, sleep_interval: 2) do + response = get(cov_api_endpoint, headers: headers_access_token) + coverage = JSON.parse(response.body) + next true if response.code == 200 && coverage.any? - if cov_resp.code != 200 - logger.error("Fetching coverage data failed, code: #{cov_resp.code}, body: #{cov_resp.body}") + if response.code != 200 + logger.debug("Fetching coverage data failed, code: #{response.code}, body: #{response.body}") end - cov_resp.code == 200 && !JSON.parse(cov_resp.body).empty? + logger.debug("Fetching coverage data failed, no coverage data available") if coverage.empty? + false end example_path = example_notification.example.metadata[:location] - test_mapping[example_path] = JSON.parse(cov_resp.body) unless example_failed?(example_notification) - logger.debug("Coverage paths were stored in mapping hash") - rescue StandardError => e - logger.error("Failed to fetch coverage mapping. Trace: #{e}") + test_mapping[example_path] = JSON.parse(response.body) + logger.info("Fetched coverage data") + rescue StandardError + logger.error("Failed to fetch coverage data, code: #{response.code}, body: #{response.body}") end def example_failed?(example_notification) @@ -83,14 +87,14 @@ def save_test_mapping # To write two different files in case of failed specs being retried File.write(file, test_mapping.to_json) - logger.debug("Saved test code paths mapping to #{file}") + logger.info("Saved test coverage mapping data to #{file}") rescue StandardError => e - logger.error("Failed to save test code paths mapping, error: #{e}") + logger.error("Failed to save test coverage mapping data, error: #{e}") end private - attr_reader :test_mapping, :logger, :headers_access_token + attr_reader :test_mapping, :logger, :headers_access_token, :cov_api_endpoint end end end diff --git a/qa/spec/support/formatters/coverband_formatter_spec.rb b/qa/spec/support/formatters/coverband_formatter_spec.rb index a10a44a65f557..cbc1fad8a7ce1 100644 --- a/qa/spec/support/formatters/coverband_formatter_spec.rb +++ b/qa/spec/support/formatters/coverband_formatter_spec.rb @@ -9,10 +9,8 @@ instance_double(RSpec::Core::Notifications::ExampleNotification, example: rspec_example) end - # rubocop:disable RSpec/VerifiedDoubles -- Custom object - let(:rspec_example) do - double( + instance_double( RSpec::Core::Example, file_path: 'create_issue_spec.rb', execution_result: instance_double(RSpec::Core::Example::ExecutionResult, status: status), @@ -24,22 +22,14 @@ ) end - # rubocop:enable RSpec/VerifiedDoubles - let(:gitlab_address) { 'http://gitlab.test.com' } let(:api_path) { "#{gitlab_address}/api/v4/internal/coverage" } - let(:status) { :failed } let(:token_header) { { "PRIVATE-TOKEN" => 'token' } } - let(:api_response) do - instance_double( - ::RestClient::Response - ) - end - - let(:non_empty_response) do - '{"test mapping":1}' - end + let(:logger) { instance_double(ActiveSupport::Logger, debug: true, error: true, info: true) } + let(:api_response) { instance_double(::RestClient::Response, code: code, body: body) } + let(:code) { 200 } + let(:body) { '[]' } let(:mapping) do { "./qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb:5": { "test mapping": 1 } } @@ -47,108 +37,103 @@ before do stub_env('GITLAB_QA_ADMIN_ACCESS_TOKEN', 'token') + + allow(QA::Runtime::Logger).to receive(:logger).and_return(logger) + allow(QA::Runtime::Scenario).to receive(:gitlab_address).and_return(gitlab_address) + allow(::RestClient::Request).to receive(:execute).and_return(api_response) end context 'with example_started' do - before do - allow(QA::Runtime::Scenario).to receive(:gitlab_address).and_return(gitlab_address) - allow(api_response).to receive(:body).and_return({}) - allow(::RestClient::Request).to receive(:execute).and_return(api_response) - end - context 'when success response' do - before do - allow(api_response).to receive(:code).and_return(200) - end - it 'logs coverage cleared', :aggregate_failures do - expect(QA::Runtime::Logger.logger).to receive(:debug).with("Cleared coverage data before example starts").once formatter.example_started(rspec_example_notification) + + expect(logger).to have_received(:info).with("Cleared coverage data").once end end context 'with failure response' do + let(:code) { 401 } + before do - allow(api_response).to receive(:code).and_return(401) - allow(QA::Support::Retrier).to receive(:retry_until).and_wrap_original do |method| - method.call(max_attempts: 1) + allow(QA::Support::Retrier).to receive(:retry_until).and_wrap_original do |method, *_args, &block| + method.call(max_attempts: 1, &block) end end it 'logs error message' do - expect(QA::Runtime::Logger.logger).to receive(:error).with(/Failed to clear coverage.*/).once formatter.example_started(rspec_example_notification) + + expect(logger).to have_received(:error).with("Failed to clear coverage, code: #{code}, body: #{body}").once end end end context 'when example finished' do - before do - allow(QA::Runtime::Scenario).to receive(:gitlab_address).and_return(gitlab_address) - allow(::RestClient::Request).to receive(:execute).and_return(api_response) - end - context 'with success response and non empty coverage' do let(:status) { :passed } - - before do - allow(api_response).to receive(:code).and_return(200) - allow(api_response).to receive(:body).and_return(non_empty_response) - end + let(:body) { '{"test mapping":1}' } it 'logs success message and does not log any errors' do - expect(QA::Runtime::Logger.logger).not_to receive(:error) - expect(QA::Runtime::Logger.logger).to receive(:debug).with("Coverage paths were stored in mapping hash").once formatter.example_finished(rspec_example_notification) + + expect(logger).not_to have_received(:error) + expect(logger).to have_received(:info).with("Fetched coverage data").once end end context 'with failure response' do let(:status) { :passed } + let(:code) { 401 } before do - allow(api_response).to receive(:code).and_return(401) - allow(api_response).to receive(:body).and_return({}) - allow(QA::Support::Retrier).to receive(:retry_until).and_wrap_original do |method| - method.call(max_attempts: 1) + allow(QA::Support::Retrier).to receive(:retry_until).and_wrap_original do |method, *_args, &block| + method.call(max_attempts: 1, &block) end end it 'logs error message' do - expect(QA::Runtime::Logger.logger).to receive(:error).with(/Failed to fetch coverage mapping.*/).once formatter.example_finished(rspec_example_notification) + + expect(logger).to have_received(:error) + .with("Failed to fetch coverage data, code: #{code}, body: #{body}") + .once end end end context 'when save_test_mapping is called' do + let(:file_name_pattern) { "test-code-paths-mapping-job-name" } + before do stub_env('CI_JOB_NAME_SLUG', 'job-name') end - let(:file_name_pattern) { "test-code-paths-mapping-job-name" } - context 'with mapping data present' do before do allow(formatter).to receive(:test_mapping).and_return(mapping) + allow(::File).to receive(:write) end it 'writes to file' do - expect(::File).to receive(:write).with(/#{file_name_pattern}/, mapping.to_json).once - expect(QA::Runtime::Logger.logger).to receive(:debug).with(/Saved test code paths mapping to.*/).once formatter.save_test_mapping + + expect(::File).to have_received(:write).with(/#{file_name_pattern}/, mapping.to_json).once + expect(logger).to have_received(:info).with(/Saved test coverage mapping data to \S+\.json/).once end end context 'when writing to file throws an error' do before do - allow(::File).to receive(:write).and_raise(StandardError) + allow(::File).to receive(:write).and_raise("some error") end it 'raises an error' do - expect(QA::Runtime::Logger.logger).to receive(:error) - .with(/Failed to save test code paths mapping, error:.*/).once formatter.save_test_mapping + + expect(logger).to have_received(:error) + .with("Failed to save test coverage mapping data, error: some error") + .once end end end diff --git a/spec/requests/api/internal/coverage_spec.rb b/spec/requests/api/internal/coverage_spec.rb index 10731231761df..82862567a2a37 100644 --- a/spec/requests/api/internal/coverage_spec.rb +++ b/spec/requests/api/internal/coverage_spec.rb @@ -36,8 +36,12 @@ before do stub_const('Coverband', Class.new) - allow(Coverband).to receive_message_chain(:configuration, :store, :coverage).and_return(coverage_hash) + stub_const('Coverband::RUNTIME_TYPE', :runtime) + allow(Coverband).to receive_message_chain(:configuration, :store, :clear!).and_return({}) + allow(Coverband).to receive_message_chain(:configuration, :store, :coverage) + .with(:runtime, skip_hash_check: true) + .and_return(coverage_hash) end it 'GET returns 200', :aggregate_failures do -- GitLab