diff --git a/qa/qa/support/formatters/test_metrics_formatter.rb b/qa/qa/support/formatters/test_metrics_formatter.rb index 1e7d4f10f0d7dedae8d98fa95a7e6e860856d69f..9bc530fedf579070f494049625686221ddf07aa2 100644 --- a/qa/qa/support/formatters/test_metrics_formatter.rb +++ b/qa/qa/support/formatters/test_metrics_formatter.rb @@ -74,6 +74,7 @@ def push_test_metrics_to_influxdb # # @return [void] def push_test_metrics_to_gcs + init_gcs_client! # init client and exit early if mandatory configuration is missing retry_on_exception(sleep_interval: 30, message: 'Failed to push test metrics to GCS') do gcs_client.put_object(gcs_bucket, metrics_file_name(prefix: 'test', postfix: "-#{env('CI_PIPELINE_ID') || 'local'}"), execution_data.to_json, @@ -81,6 +82,8 @@ def push_test_metrics_to_gcs log(:info, "Pushed #{execution_data.length} test execution entries to GCS") end + rescue StandardError => e + log(:error, "Failed to push test execution metrics to gcs, error: #{e}") end # Push resource fabrication metrics to influxdb @@ -104,6 +107,7 @@ def push_fabrication_metrics # @param [Hash] data fabrication data hash # @return [void] def push_fabrication_metrics_gcs(data) + init_gcs_client! # init client and exit early if mandatory configuration is missing retry_on_exception(sleep_interval: 30, message: 'Failed to push resource fabrication metrics to GCS') do gcs_client.put_object(gcs_bucket, metrics_file_name(prefix: 'fabrication', @@ -112,6 +116,8 @@ def push_fabrication_metrics_gcs(data) log(:info, "Pushed #{data.length} resource fabrication entries to GCS") end + rescue StandardError => e + log(:error, "Failed to push test fabrication metrics to gcs, error: #{e}") end # Push resource fabrication metrics to InfluxDB @@ -125,6 +131,13 @@ def push_fabrication_metrics_influxdb(data) log(:error, "Failed to push fabrication metrics to influxdb, error: #{e}") end + # Init client raising error if configuration variables are missing + # + # @return [void] + def init_gcs_client! + gcs_client || gcs_bucket + end + # Get GCS Bucket Name or raise error if missing # # @return [String] @@ -220,8 +233,9 @@ def fields(example) pipeline_id: env('CI_PIPELINE_ID'), job_id: env('CI_JOB_ID'), merge_request_iid: merge_request_iid, - failure_issue: example.metadata[:quarantine] ? example.metadata[:quarantine][:issue] : nil, failure_exception: example.execution_result.exception.to_s.delete("\n"), + location: example_location(example), + failure_issue: example.metadata.dig(:quarantine, :issue), **custom_metrics_fields(example.metadata) }.compact end @@ -351,6 +365,23 @@ def devops_stage(file_path) def log(level, message) QA::Runtime::Logger.public_send(level, "[influxdb exporter]: #{message}") end + + # Example location + # + # @param [RSpec::Core::Example] example + # @return [String] + def example_location(example) + # ensures that location will be correct even in case of shared examples + file = example + .metadata + .fetch(:shared_group_inclusion_backtrace) + .last + &.formatted_inclusion_location + + return example.location unless file + + file + end end end end diff --git a/qa/spec/support/formatters/test_metrics_formatter_spec.rb b/qa/spec/support/formatters/test_metrics_formatter_spec.rb index ecba65a3711fe90f337af535405bc22a2d9f84b9..fee781b9a22740a8b9a97d08bff167eb31bbc732 100644 --- a/qa/spec/support/formatters/test_metrics_formatter_spec.rb +++ b/qa/spec/support/formatters/test_metrics_formatter_spec.rb @@ -2,6 +2,7 @@ require 'rspec/core/sandbox' require 'active_support/testing/time_helpers' +require 'pathname' # rubocop:disable RSpec/MultipleMemoizedHelpers, Lint/EmptyBlock -- false positives for empty blocks and memoized helpers help with testing different data hash parameters describe QA::Support::Formatters::TestMetricsFormatter do @@ -75,7 +76,8 @@ pipeline_url: ci_pipeline_url, pipeline_id: ci_pipeline_id, job_id: ci_job_id, - failure_exception: '' + failure_exception: '', + location: %r{./#{Pathname.new(__FILE__).relative_path_from(Dir.pwd)}:\d+} } } end @@ -111,6 +113,8 @@ def run_spec(passed: true, &spec) .and_return(gcs_client) allow(QA::Tools::TestResourceDataProcessor).to receive(:resources) { fabrication_resources } allow_any_instance_of(RSpec::Core::Example::ExecutionResult).to receive(:run_time).and_return(0) # rubocop:disable RSpec/AnyInstanceOf -- simplifies mocking runtime + + stub_env('QA_RUN_TYPE', run_type) end context 'without influxdb variables configured' do @@ -159,16 +163,14 @@ def run_spec(passed: true, &spec) end end - context 'with influxdb and GCS variables configured' do + context 'with influxdb variables configured' do let(:spec_name) { 'exports data' } let(:run_type) { ci_job_name.gsub(%r{ \d{1,2}/\d{1,2}}, '') } before do + stub_env('QA_METRICS_GCS_CREDS', nil) stub_env('QA_INFLUXDB_URL', url) stub_env('QA_INFLUXDB_TOKEN', token) - stub_env('QA_METRICS_GCS_PROJECT_ID', metrics_gcs_project_id) - stub_env('QA_METRICS_GCS_CREDS', metrics_gcs_creds) - stub_env('QA_METRICS_GCS_BUCKET_NAME', metrics_gcs_bucket_name) stub_env('CI_PIPELINE_CREATED_AT', ci_timestamp) stub_env('CI_JOB_URL', ci_job_url) stub_env('CI_JOB_NAME', ci_job_name) @@ -177,7 +179,6 @@ def run_spec(passed: true, &spec) stub_env('CI_JOB_ID', ci_job_id) stub_env('CI_MERGE_REQUEST_IID', nil) stub_env('TOP_UPSTREAM_MERGE_REQUEST_IID', nil) - stub_env('QA_RUN_TYPE', run_type) stub_env('QA_EXPORT_TEST_METRICS', "true") stub_env('QA_RSPEC_RETRIED', "false") stub_env('QA_INFLUXDB_TIMEOUT', "10") @@ -193,14 +194,11 @@ def run_spec(passed: true, &spec) expect(influx_write_api).to have_received(:write).once expect(influx_write_api).to have_received(:write).with(data: [data]) - - expect(gcs_client).to have_received(:put_object).with(metrics_gcs_bucket_name, - anything, [data].to_json, **gcs_client_options) end end context 'with product group tag' do - let(:expected_data) { [data.tap { |d| d[:tags][:product_group] = :import }] } + let(:expected_data) { data.tap { |d| d[:tags][:product_group] = :import } } it 'exports data with correct product group tag' do run_spec do @@ -208,9 +206,7 @@ def run_spec(passed: true, &spec) end expect(influx_write_api).to have_received(:write).once - expect(influx_write_api).to have_received(:write).with( - data: expected_data - ) + expect(influx_write_api).to have_received(:write).with(data: [expected_data]) end end @@ -224,32 +220,13 @@ def run_spec(passed: true, &spec) expect(influx_write_api).to have_received(:write).once expect(influx_write_api).to have_received(:write).with(data: [data]) - - expect(gcs_client).to have_received(:put_object).with(metrics_gcs_bucket_name, - anything, [data].to_json, **gcs_client_options) end end context 'with quarantined spec' do let(:quarantined) { 'true' } let(:status) { :pending } - let(:expected_data) do - data.tap do |d| - d[:fields] = { - id: './spec/support/formatters/test_metrics_formatter_spec.rb[1:1]', - run_time: 0, - api_fabrication: api_fabrication * 1000, - ui_fabrication: ui_fabrication * 1000, - total_fabrication: (api_fabrication + ui_fabrication) * 1000, - job_url: ci_job_url, - pipeline_url: ci_pipeline_url, - pipeline_id: ci_pipeline_id, - job_id: ci_job_id, - failure_issue: 'https://example.com/issue/1234', - failure_exception: '' - } - end - end + let(:expected_data) { data.tap { |d| d[:fields][:failure_issue] = 'https://example.com/issue/1234' } } it 'exports data with correct quarantine tag', :aggregate_failures do run_spec do @@ -266,31 +243,12 @@ def run_spec(passed: true, &spec) expect(influx_write_api).to have_received(:write).once expect(influx_write_api).to have_received(:write).with(data: [expected_data]) - - expect(gcs_client).to have_received(:put_object).with(metrics_gcs_bucket_name, - anything, [expected_data].to_json, **gcs_client_options) end end context 'with context quarantined spec' do let(:quarantined) { 'false' } - let(:expected_data) do - data.tap do |d| - d[:fields] = { - id: './spec/support/formatters/test_metrics_formatter_spec.rb[1:1]', - run_time: 0, - api_fabrication: api_fabrication * 1000, - ui_fabrication: ui_fabrication * 1000, - total_fabrication: (api_fabrication + ui_fabrication) * 1000, - job_url: ci_job_url, - pipeline_url: ci_pipeline_url, - pipeline_id: ci_pipeline_id, - job_id: ci_job_id, - failure_issue: 'https://example.com/issue/1234', - failure_exception: '' - } - end - end + let(:expected_data) { data.tap { |d| d[:fields][:failure_issue] = 'https://example.com/issue/1234' } } it 'exports data with correct quarantine tag', :aggregate_failures do run_spec do @@ -303,9 +261,6 @@ def run_spec(passed: true, &spec) expect(influx_write_api).to have_received(:write).once expect(influx_write_api).to have_received(:write).with(data: [expected_data]) - - expect(gcs_client).to have_received(:put_object).with(metrics_gcs_bucket_name, - anything, [expected_data].to_json, **gcs_client_options) end end @@ -322,9 +277,6 @@ def run_spec(passed: true, &spec) end expect(influx_write_api).to have_received(:write).with(data: [data]) - - expect(gcs_client).to have_received(:put_object).with(metrics_gcs_bucket_name, - anything, [data].to_json, **gcs_client_options) end end @@ -337,12 +289,7 @@ def run_spec(passed: true, &spec) it('spec', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234') { raise } end - expect(influx_write_api).to have_received(:write).with( - data: [expected_data] - ) - - expect(gcs_client).to have_received(:put_object).with(metrics_gcs_bucket_name, - anything, [expected_data].to_json, **gcs_client_options) + expect(influx_write_api).to have_received(:write).with(data: [expected_data]) end end @@ -356,8 +303,6 @@ def run_spec(passed: true, &spec) end expect(influx_write_api).to have_received(:write).with(data: []) - expect(gcs_client).to have_received(:put_object).with(metrics_gcs_bucket_name, - anything, [].to_json, **gcs_client_options) end end @@ -374,8 +319,6 @@ def run_spec(passed: true, &spec) end expect(influx_write_api).to have_received(:write).with(data: [data]) - expect(gcs_client).to have_received(:put_object).with(metrics_gcs_bucket_name, - anything, [data].to_json, **gcs_client_options) end end end @@ -393,9 +336,6 @@ def run_spec(passed: true, &spec) expect(influx_write_api).to have_received(:write).once expect(influx_write_api).to have_received(:write).with(data: [data]) - - expect(gcs_client).to have_received(:put_object).with(metrics_gcs_bucket_name, - anything, [data].to_json, **gcs_client_options) end end @@ -410,16 +350,12 @@ def run_spec(passed: true, &spec) end custom_data = data.merge({ - **data, tags: data[:tags].merge({ custom_tag: "tag" }), fields: data[:fields].merge({ custom_field: 1 }) }) expect(influx_write_api).to have_received(:write).once expect(influx_write_api).to have_received(:write).with(data: [custom_data]) - - expect(gcs_client).to have_received(:put_object).with(metrics_gcs_bucket_name, - anything, [custom_data].to_json, **gcs_client_options) end end @@ -443,25 +379,23 @@ def run_spec(passed: true, &spec) expect(influx_write_api).to have_received(:write).once expect(influx_write_api).to have_received(:write).with(data: [data]) - - expect(gcs_client).to have_received(:put_object).with(metrics_gcs_bucket_name, - anything, [data].to_json, **gcs_client_options) end end context 'with a shared example' do let(:file_path) { './qa/specs/features/shared_examples/merge_with_code_owner_shared_examples.rb' } let(:rerun_file_path) { './qa/specs/features/3_create/subfolder/another_spec.rb' } + let(:expected_data) do + data.tap do |d| + d[:tags][:file_path] = '/3_create/subfolder/another_spec.rb' + d[:tags][:stage] = 'create' + end + end it 'exports data to influxdb with correct filename', :aggregate_failures do run_spec - data[:tags][:file_path] = '/3_create/subfolder/another_spec.rb' - data[:tags][:stage] = 'create' - expect(influx_write_api).to have_received(:write).with(data: [data]) - - expect(gcs_client).to have_received(:put_object).with(metrics_gcs_bucket_name, - anything, [data].to_json, **gcs_client_options) + expect(influx_write_api).to have_received(:write).with(data: [expected_data]) end end @@ -503,17 +437,18 @@ def run_spec(passed: true, &spec) freeze_time { example.run } end - it 'exports fabrication stats data to influxdb and GCS', :aggregate_failures do + it 'exports fabrication data to influxdb and GCS', :aggregate_failures do run_spec expect(influx_write_api).to have_received(:write).with(data: [fabrication_data]) - - expect(gcs_client).to have_received(:put_object).with(metrics_gcs_bucket_name, - anything, [fabrication_data].to_json, **gcs_client_options) end end context 'with persisting metrics' do + let(:expected_data) do + data.tap { |d| d[:fields][:location] = "./#{Pathname.new(__FILE__).relative_path_from(Dir.pwd)}:86" } + end + before do stub_env('QA_EXPORT_TEST_METRICS', "false") stub_env('QA_SAVE_TEST_METRICS', "true") @@ -528,7 +463,7 @@ def run_spec(passed: true, &spec) it 'saves test metrics as json files' do run_spec - expect(File).to have_received(:write).with(file, [data].to_json) + expect(File).to have_received(:write).with(file, [expected_data].to_json) end end @@ -539,10 +474,76 @@ def run_spec(passed: true, &spec) it 'saves test metrics as json files' do run_spec - expect(File).to have_received(:write).with(file, [data].to_json) + expect(File).to have_received(:write).with(file, [expected_data].to_json) end end end + + context "with metrics upload to gcs" do + let(:fabrication_resources) do + { + 'QA::Resource::Project' => [{ + info: "with id '1'", + api_path: '/project', + fabrication_method: :api, + fabrication_time: 1, + http_method: :post, + timestamp: Time.now.to_s + }] + } + end + + let(:test_data) do + data.tap { |d| d[:fields][:location] = "./#{Pathname.new(__FILE__).relative_path_from(Dir.pwd)}:86" } + end + + let(:fabrication_data) do + { + name: 'fabrication-stats', + time: DateTime.strptime(ci_timestamp).to_time, + tags: { + resource: 'QA::Resource::Project', + fabrication_method: :api, + http_method: :post, + run_type: run_type, + merge_request: "false" + }, + fields: { + fabrication_time: 1, + info: "with id '1'", + job_url: ci_job_url, + timestamp: Time.now.to_s + } + } + end + + before do + stub_env('QA_METRICS_GCS_PROJECT_ID', metrics_gcs_project_id) + stub_env('QA_METRICS_GCS_CREDS', metrics_gcs_creds) + stub_env('QA_METRICS_GCS_BUCKET_NAME', metrics_gcs_bucket_name) + end + + around do |example| + freeze_time { example.run } + end + + it "creates correct json files and uploads metrics to gcs" do + run_spec + + expect(gcs_client).to have_received(:put_object).with( + metrics_gcs_bucket_name, + /test-metrics-\S+\.json/, + [test_data].to_json, + **gcs_client_options + ) + expect(gcs_client).to have_received(:put_object).with( + metrics_gcs_bucket_name, + /fabrication-metrics-\S+\.json/, + [fabrication_data].to_json, + **gcs_client_options + ) + end + end end end # rubocop:enable RSpec/MultipleMemoizedHelpers, Lint/EmptyBlock