Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-09-27 18:09:37 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-09-27 18:09:37 +0300
commit57ae76cdb503afd6c0821de3a03b4387af6b59eb (patch)
treed6ffcbdd75157d969fcd5f4850f3a11f21b95a6c /spec/tooling
parent5471fef2360f9bcf604a026d5807a554dae243e9 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/tooling')
-rw-r--r--spec/tooling/lib/tooling/job_metrics_spec.rb721
1 files changed, 721 insertions, 0 deletions
diff --git a/spec/tooling/lib/tooling/job_metrics_spec.rb b/spec/tooling/lib/tooling/job_metrics_spec.rb
new file mode 100644
index 00000000000..c7f4d08394c
--- /dev/null
+++ b/spec/tooling/lib/tooling/job_metrics_spec.rb
@@ -0,0 +1,721 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'tempfile'
+require 'time'
+
+require_relative '../../../../tooling/lib/tooling/job_metrics'
+
+RSpec.describe Tooling::JobMetrics, feature_category: :tooling do
+ include StubENV
+
+ attr_accessor :job_metrics_file, :job_metrics_file_path
+
+ around do |example|
+ self.job_metrics_file = Tempfile.new('test-folder/job-metrics.json')
+ self.job_metrics_file_path = job_metrics_file.path
+
+ # See https://ruby-doc.org/stdlib-1.9.3/libdoc/tempfile/rdoc/
+ # Tempfile.html#class-Tempfile-label-Explicit+close
+ begin
+ example.run
+ ensure
+ job_metrics_file.close
+ job_metrics_file.unlink
+ end
+ end
+
+ let(:instance) { described_class.new(metrics_file_path: job_metrics_file_path) }
+ let(:pipeline_created_at) { '2023-05-03T12:35:39.932Z' }
+
+ before do
+ stub_env(
+ 'CI_JOB_ID' => '1234',
+ 'CI_JOB_NAME' => 'rspec unit pg13 1/24',
+ 'CI_JOB_STAGE' => 'test',
+ 'CI_JOB_STARTED_AT' => (Time.now - 3600).iso8601, # 1h ago
+ 'CI_JOB_STATUS' => 'success',
+ 'CI_MERGE_REQUEST_IID' => '23412',
+ 'CI_PIPELINE_CREATED_AT' => pipeline_created_at,
+ 'CI_PIPELINE_ID' => '3393923023',
+ 'CI_PROJECT_ID' => '7489',
+ 'CI_SERVER_HOST' => 'localhost:300',
+ 'JOB_METRICS_FILE_PATH' => job_metrics_file_path
+ )
+ end
+
+ describe '#initialize' do
+ context 'when a path is given' do
+ subject { described_class.new(metrics_file_path: job_metrics_file_path) }
+
+ it 'instantiates the object' do
+ expect(subject).to be_a(described_class)
+ end
+
+ it 'sets the correct path for the metrics file' do
+ expect(subject.metrics_file_path).to eq(job_metrics_file_path)
+ end
+ end
+
+ context 'when a path is not given' do
+ subject { described_class.new }
+
+ context 'when the JOB_METRICS_FILE_PATH env variable is set' do
+ before do
+ stub_env(
+ 'JOB_METRICS_FILE_PATH' => job_metrics_file_path
+ )
+ end
+
+ it 'instantiates the object' do
+ expect(subject).to be_a(described_class)
+ end
+
+ it 'sets the correct path for the metrics file' do
+ expect(subject.metrics_file_path).to eq(ENV['JOB_METRICS_FILE_PATH'])
+ end
+ end
+
+ context 'when the JOB_METRICS_FILE_PATH env variable is not set' do
+ before do
+ stub_env(
+ 'JOB_METRICS_FILE_PATH' => nil
+ )
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error('Please specify a path for the job metrics file.')
+ end
+ end
+ end
+ end
+
+ describe '#create_metrics_file' do
+ subject { instance.create_metrics_file }
+
+ context 'when a valid metrics file exists' do
+ before do
+ allow(instance).to receive(:warn)
+ allow(instance).to receive(:valid_metrics_file?).and_return(true)
+ end
+
+ it 'prints a message to the user' do
+ allow(instance).to receive(:warn).and_call_original
+
+ expect { subject }.to output(
+ "A valid job metrics file already exists. We're not going to overwrite it.\n"
+ ).to_stderr
+ end
+
+ it 'does not overwrite the existing metrics file' do
+ expect(instance).not_to receive(:persist_metrics_file)
+
+ subject
+ end
+ end
+
+ context 'when a valid metrics file does not exist' do
+ before do
+ allow(instance).to receive(:valid_metrics_file?).and_return(false)
+ end
+
+ it 'persists the metrics file' do
+ expect(instance).to receive(:persist_metrics_file).with(instance.default_metrics)
+
+ subject
+ end
+ end
+ end
+
+ describe '#update_field' do
+ subject { instance.update_field(field_name, field_value) }
+
+ let(:field_name) { instance.default_fields.each_key.first }
+ let(:field_value) { 'test_value' }
+
+ context 'when the field to update is not in the default fields list' do
+ let(:field_name) { 'not-in-default-list' }
+
+ before do
+ allow(instance).to receive(:warn)
+ end
+
+ it 'returns a warning to the user' do
+ allow(instance).to receive(:warn).and_call_original
+
+ expect { subject }.to output(
+ "[job-metrics] ERROR: Could not update field #{field_name}, as it is not part of the allowed fields.\n"
+ ).to_stderr
+ end
+
+ it 'does not write to the metrics file' do
+ expect(instance).not_to receive(:persist_metrics_file)
+
+ subject
+ end
+ end
+
+ context 'when the field to update is in the default fields list' do
+ it 'calls the update_file method with the correct arguments' do
+ expect(instance).to receive(:update_file).with(field_name, field_value, type: :field)
+
+ subject
+ end
+ end
+ end
+
+ describe '#update_tag' do
+ subject { instance.update_tag(tag_name, tag_value) }
+
+ let(:tag_name) { instance.default_tags.each_key.first }
+ let(:tag_value) { 'test_value' }
+
+ context 'when the tag to update is not in the default tags list' do
+ let(:tag_name) { 'not-in-default-list' }
+
+ before do
+ allow(instance).to receive(:warn)
+ end
+
+ it 'returns a warning to the user' do
+ allow(instance).to receive(:warn).and_call_original
+
+ expect { subject }.to output(
+ "[job-metrics] ERROR: Could not update tag #{tag_name}, as it is not part of the allowed tags.\n"
+ ).to_stderr
+ end
+
+ it 'does not write to the metrics file' do
+ expect(instance).not_to receive(:persist_metrics_file)
+
+ subject
+ end
+ end
+
+ context 'when the tag to update is in the default tags list' do
+ it 'calls the update_file method with the correct arguments' do
+ expect(instance).to receive(:update_file).with(tag_name, tag_value, type: :tag)
+
+ subject
+ end
+ end
+ end
+
+ describe '#update_file' do
+ subject { instance.update_file(tag_name, tag_value, type: type) }
+
+ let(:type) { :tag }
+ let(:tag_name) { instance.default_tags.each_key.first }
+ let(:tag_value) { 'test_value' }
+
+ context 'when the metrics file is not valid' do
+ before do
+ allow(instance).to receive(:valid_metrics_file?).and_return(false)
+ allow(instance).to receive(:warn)
+ end
+
+ it 'returns a warning to the user' do
+ allow(instance).to receive(:warn).and_call_original
+
+ expect { subject }.to output(
+ "[job-metrics] ERROR: Invalid job metrics file.\n"
+ ).to_stderr
+ end
+
+ it 'does not write to the metrics file' do
+ expect(instance).not_to receive(:persist_metrics_file)
+
+ subject
+ end
+ end
+
+ context 'when the metrics file is valid' do
+ let(:metrics_hash) do
+ {
+ name: 'job-metrics',
+ time: ENV['CI_PIPELINE_CREATED_AT'].to_time,
+ tags: tags_hash,
+ fields: fields_hash
+ }
+ end
+
+ let(:tags_hash) { instance.default_tags }
+ let(:fields_hash) { instance.default_fields }
+
+ before do
+ allow(instance).to receive(:valid_metrics_file?).and_return(true)
+ allow(instance).to receive(:load_metrics_file).and_return(metrics_hash)
+ end
+
+ context 'when updating a tag' do
+ let(:type) { :tag }
+
+ it 'updates the tag value' do
+ expect(instance).to receive(:persist_metrics_file).with(
+ hash_including(
+ tags: hash_including(tag_name)
+ )
+ )
+
+ subject
+ end
+ end
+
+ context 'when updating a field' do
+ let(:type) { :field }
+
+ let(:field_name) { instance.default_fields.each_key.first }
+ let(:field_value) { 'test_value' }
+
+ it 'updates the field value' do
+ expect(instance).to receive(:persist_metrics_file).with(
+ hash_including(
+ fields: hash_including(field_name)
+ )
+ )
+
+ subject
+ end
+ end
+ end
+ end
+
+ describe '#push_metrics' do
+ subject { instance.push_metrics }
+
+ context 'when the metrics file is not valid' do
+ before do
+ allow(instance).to receive(:valid_metrics_file?).and_return(false)
+ allow(instance).to receive(:warn)
+ end
+
+ it 'returns a warning to the user' do
+ allow(instance).to receive(:warn).and_call_original
+
+ expect { subject }.to output(
+ "[job-metrics] ERROR: Invalid job metrics file. We will not push the metrics to InfluxDB\n"
+ ).to_stderr
+ end
+
+ it 'does not write to the metrics file' do
+ expect(instance).not_to receive(:persist_metrics_file)
+
+ subject
+ end
+ end
+
+ context 'when the metrics file is valid' do
+ let(:metrics_hash) do
+ {
+ name: 'job-metrics',
+ time: ENV['CI_PIPELINE_CREATED_AT'].to_time,
+ tags: tags_hash,
+ fields: fields_hash
+ }
+ end
+
+ let(:tags_hash) { instance.default_tags }
+ let(:fields_hash) { instance.default_fields }
+ let(:influx_write_api) { double('influx_write_api') } # rubocop:disable RSpec:VerifiedDoubles
+
+ before do
+ allow(instance).to receive(:influx_write_api).and_return(influx_write_api)
+ allow(instance).to receive(:valid_metrics_file?).and_return(true)
+ allow(instance).to receive(:load_metrics_file).and_return(metrics_hash)
+ allow(instance).to receive(:warn)
+ allow(instance).to receive(:puts)
+ end
+
+ context 'when we are missing ENV variables to push to influxDB' do
+ before do
+ stub_env(
+ 'QA_INFLUXDB_URL' => 'https://test.com',
+ 'EP_CI_JOB_METRICS_TOKEN' => nil
+ )
+ end
+
+ it 'displays an error to the user' do
+ allow(instance).to receive(:influx_write_api).and_call_original
+ allow(instance).to receive(:warn).and_call_original
+
+ expect { subject }.to output(
+ "[job-metrics] Failed to push CI job metrics to InfluxDB, " \
+ "error: Missing EP_CI_JOB_METRICS_TOKEN env variable\n"
+ ).to_stderr
+ end
+ end
+
+ context 'when pushing the data to InfluxDB raises an exception' do
+ it 'displays an error to the user' do
+ allow(instance).to receive(:warn).and_call_original
+ expect(influx_write_api).to receive(:write).and_raise("connectivity issues")
+
+ expect { subject }.to output(
+ "[job-metrics] Failed to push CI job metrics to InfluxDB, error: connectivity issues\n"
+ ).to_stderr
+ end
+ end
+
+ context 'when some tags/fields are empty/nil' do
+ before do
+ allow(instance).to receive(:load_metrics_file).and_return({
+ name: 'job-metrics',
+ time: ENV['CI_PIPELINE_CREATED_AT'].to_time,
+ tags: {
+ first_tag: '',
+ third_tag: 'hello'
+ },
+ fields: {
+ second_tag: nil
+ }
+ })
+ end
+
+ it 'removes the metrics with empty/nil values from the metrics list' do
+ expect(influx_write_api).to receive(:write).with(data: {
+ name: 'job-metrics',
+ time: anything,
+ tags: { third_tag: 'hello' },
+ fields: {
+ job_duration_seconds: anything # Added right before pushing to influxDB
+ }
+ })
+
+ subject
+ end
+ end
+
+ it 'pushes the data to InfluxDB' do
+ expect(influx_write_api).to receive(:write).with(data: metrics_hash)
+
+ subject
+ end
+
+ it 'sets the job_duration_seconds field' do
+ # We want the job to last for 10 minutes (600 seconds)
+ allow(Time).to receive(:now).and_return(Time.parse(ENV.fetch('CI_JOB_STARTED_AT')) + 600)
+
+ expect(influx_write_api).to receive(:write).with(
+ data: hash_including(
+ fields: hash_including(
+ job_duration_seconds: 600
+ )
+ )
+ )
+
+ subject
+ end
+ end
+ end
+
+ describe '#load_metrics_file' do
+ subject { instance.load_metrics_file }
+
+ context 'when the metrics file does not exist on disk' do
+ before do
+ allow(File).to receive(:exist?).with(job_metrics_file_path).and_return(false)
+ end
+
+ it 'returns nil' do
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'when the metrics file exists on disk' do
+ context 'when the metrics file does not contain valid JSON' do
+ before do
+ File.write(job_metrics_file_path, 'THIS IS NOT JSON CONTENT!')
+ end
+
+ it 'returns nil' do
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'when the metrics file contains valid JSON' do
+ before do
+ File.write(job_metrics_file_path, { 'key' => 'value' }.to_json)
+ end
+
+ it 'returns the content of the file as a hash with symbolized keys' do
+ expect(subject).to eq({ key: 'value' })
+ end
+ end
+ end
+ end
+
+ describe '#valid_metrics_file?' do
+ subject { instance.valid_metrics_file? }
+
+ context 'when the metrics file cannot be loaded in memory' do
+ before do
+ allow(instance).to receive(:load_metrics_file).and_return(nil)
+ end
+
+ it 'returns false' do
+ expect(subject).to be_falsey
+ end
+ end
+
+ context 'when the metrics file can be loaded in memory' do
+ let(:metrics_file_content) do
+ { key: 'value' }
+ end
+
+ before do
+ allow(instance).to receive(:load_metrics_file).and_return(metrics_file_content)
+ end
+
+ context 'when the metrics file validation succeeds' do
+ before do
+ allow(instance).to receive(:valid_metrics?).with(metrics_file_content).and_return(true)
+ end
+
+ it 'returns true' do
+ expect(subject).to be_truthy
+ end
+ end
+
+ context 'when the metrics file validation fails' do
+ before do
+ allow(instance).to receive(:valid_metrics?).with(metrics_file_content).and_return(false)
+ end
+
+ it 'returns false' do
+ expect(subject).to be_falsey
+ end
+ end
+ end
+ end
+
+ describe '#valid_metrics?' do
+ subject { instance.valid_metrics?(metrics_hash) }
+
+ let(:metrics_hash) do
+ {
+ name: 'job-metrics',
+ time: ENV['CI_PIPELINE_CREATED_AT'].to_time,
+ tags: tags_hash,
+ fields: fields_hash
+ }
+ end
+
+ let(:tags_hash) { instance.default_tags }
+ let(:fields_hash) { instance.default_fields }
+
+ describe 'metrics hash keys' do
+ context 'when it is missing a key' do
+ before do
+ metrics_hash.delete(:time)
+ end
+
+ it 'returns false' do
+ expect(subject).to be_falsey
+ end
+ end
+
+ context 'when it has an extra key' do
+ before do
+ metrics_hash[:extra_key] = ''
+ end
+
+ it 'returns false' do
+ expect(subject).to be_falsey
+ end
+ end
+ end
+
+ describe 'metrics hash tags keys' do
+ context 'when it is missing a key' do
+ before do
+ tags_hash.delete(tags_hash.each_key.first)
+ end
+
+ it 'returns false' do
+ expect(subject).to be_falsey
+ end
+ end
+
+ context 'when it has an extra key' do
+ before do
+ tags_hash[:extra_key] = ''
+ end
+
+ it 'returns false' do
+ expect(subject).to be_falsey
+ end
+ end
+ end
+
+ describe 'metrics hash fields keys' do
+ context 'when it is missing a key' do
+ before do
+ fields_hash.delete(fields_hash.each_key.first)
+ end
+
+ it 'returns false' do
+ expect(subject).to be_falsey
+ end
+ end
+
+ context 'when it has an extra key' do
+ before do
+ fields_hash[:extra_key] = ''
+ end
+
+ it 'returns false' do
+ expect(subject).to be_falsey
+ end
+ end
+ end
+
+ context 'when the metrics hash is valid' do
+ it 'returns true' do
+ expect(subject).to be_truthy
+ end
+ end
+ end
+
+ describe '#persist_metrics_file' do
+ let(:metrics_hash) do
+ { key: 'value' }.to_json
+ end
+
+ subject { instance.persist_metrics_file(metrics_hash) }
+
+ context 'when the metrics hash is not valid' do
+ before do
+ allow(instance).to receive(:valid_metrics?).and_return(false)
+ allow(instance).to receive(:warn)
+ end
+
+ it 'returns a warning to the user' do
+ allow(instance).to receive(:warn).and_call_original
+
+ expect { subject }.to output(
+ "cannot persist the metrics, as it doesn't have the correct data structure.\n"
+ ).to_stderr
+ end
+
+ it 'does not write to the metrics file' do
+ expect(File).not_to receive(:write).with(job_metrics_file_path, any_args)
+
+ subject
+ end
+ end
+
+ context 'when the metrics hash is valid' do
+ before do
+ allow(instance).to receive(:valid_metrics?).and_return(true)
+ end
+
+ it 'persists the metrics file' do
+ expect { subject }.to change { File.read(job_metrics_file_path) }.from('').to(metrics_hash.to_json)
+ end
+ end
+ end
+
+ describe '#default_metrics' do
+ subject { instance.default_metrics }
+
+ let(:returned_time) { ENV['CI_PIPELINE_CREATED_AT'].to_time }
+ let(:default_tags) { instance.default_tags }
+ let(:default_fields) { instance.default_fields }
+
+ it 'returns the expected metrics keys' do
+ expect(subject).to eq(
+ name: 'job-metrics',
+ time: returned_time,
+ tags: default_tags,
+ fields: default_fields
+ )
+ end
+ end
+
+ describe '#default_tags' do
+ subject { instance.default_tags }
+
+ it 'returns the expected tags keys' do
+ expect(subject).to eq(
+ job_name: ENV['CI_JOB_NAME'],
+ job_stage: ENV['CI_JOB_STAGE'],
+ job_status: ENV['CI_JOB_STATUS'],
+ project_id: ENV['CI_PROJECT_ID'],
+ rspec_retried_in_new_process: 'false',
+ server_host: ENV['CI_SERVER_HOST']
+ )
+ end
+
+ context 'when an ENV variable is not set' do
+ before do
+ stub_env('CI_JOB_NAME' => nil)
+ end
+
+ it 'replaces the value with nil' do
+ expect(subject).to eq(
+ job_name: nil,
+ job_stage: ENV['CI_JOB_STAGE'],
+ job_status: ENV['CI_JOB_STATUS'],
+ project_id: ENV['CI_PROJECT_ID'],
+ rspec_retried_in_new_process: 'false',
+ server_host: ENV['CI_SERVER_HOST']
+ )
+ end
+ end
+ end
+
+ describe '#default_fields' do
+ subject { instance.default_fields }
+
+ it 'returns the expected fields keys' do
+ expect(subject).to eq(
+ job_id: ENV['CI_JOB_ID'],
+ job_duration_seconds: nil,
+ merge_request_iid: ENV['CI_MERGE_REQUEST_IID'],
+ pipeline_id: ENV['CI_PIPELINE_ID']
+ )
+ end
+
+ context 'when an ENV variable is not set' do
+ before do
+ stub_env('CI_JOB_ID' => nil)
+ end
+
+ it 'replaces the value with nil' do
+ expect(subject).to eq(
+ job_id: nil,
+ job_duration_seconds: nil,
+ merge_request_iid: ENV['CI_MERGE_REQUEST_IID'],
+ pipeline_id: ENV['CI_PIPELINE_ID']
+ )
+ end
+ end
+ end
+
+ describe '#time' do
+ subject { instance.time }
+
+ let(:current_time) { '2011-01-01' }
+
+ before do
+ stub_env('CI_PIPELINE_CREATED_AT' => pipeline_created_at)
+ allow(DateTime).to receive(:now).and_return(current_time)
+ end
+
+ context 'when the CI_PIPELINE_CREATED_AT env variable is set' do
+ let(:pipeline_created_at) { '2000-01-01T00:00:00Z' }
+
+ it 'returns the correct time' do
+ expect(subject).to eq(pipeline_created_at)
+ end
+ end
+
+ context 'when the CI_PIPELINE_CREATED_AT env variable is not set' do
+ let(:pipeline_created_at) { nil }
+
+ it 'returns the current time' do
+ expect(subject).to eq(current_time)
+ end
+ end
+ end
+end