269 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			269 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Ruby
		
	
	
	
# frozen_string_literal: true
 | 
						|
 | 
						|
require 'spec_helper'
 | 
						|
 | 
						|
RSpec.describe Gitlab::Usage::MetricDefinition do
 | 
						|
  let(:attributes) do
 | 
						|
    {
 | 
						|
      description: 'GitLab instance unique identifier',
 | 
						|
      value_type: 'string',
 | 
						|
      product_category: 'collection',
 | 
						|
      product_stage: 'growth',
 | 
						|
      product_section: 'devops',
 | 
						|
      status: 'active',
 | 
						|
      milestone: '14.1',
 | 
						|
      default_generation: 'generation_1',
 | 
						|
      key_path: 'uuid',
 | 
						|
      product_group: 'group::product analytics',
 | 
						|
      time_frame: 'none',
 | 
						|
      data_source: 'database',
 | 
						|
      distribution: %w(ee ce),
 | 
						|
      tier: %w(free starter premium ultimate bronze silver gold),
 | 
						|
      name: 'uuid',
 | 
						|
      data_category: 'standard'
 | 
						|
    }
 | 
						|
  end
 | 
						|
 | 
						|
  let(:path) { File.join('metrics', 'uuid.yml') }
 | 
						|
  let(:definition) { described_class.new(path, attributes) }
 | 
						|
  let(:yaml_content) { attributes.deep_stringify_keys.to_yaml }
 | 
						|
 | 
						|
  around do |example|
 | 
						|
    described_class.instance_variable_set(:@definitions, nil)
 | 
						|
    example.run
 | 
						|
    described_class.instance_variable_set(:@definitions, nil)
 | 
						|
  end
 | 
						|
 | 
						|
  def write_metric(metric, path, content)
 | 
						|
    path = File.join(metric, path)
 | 
						|
    dir = File.dirname(path)
 | 
						|
    FileUtils.mkdir_p(dir)
 | 
						|
    File.write(path, content)
 | 
						|
  end
 | 
						|
 | 
						|
  after do
 | 
						|
    # Reset memoized `definitions` result
 | 
						|
    described_class.instance_variable_set(:@definitions, nil)
 | 
						|
  end
 | 
						|
 | 
						|
  it 'has all definitons valid' do
 | 
						|
    expect { described_class.definitions }.not_to raise_error
 | 
						|
  end
 | 
						|
 | 
						|
  describe '#with_instrumentation_class' do
 | 
						|
    let(:metric_status) { 'active' }
 | 
						|
    let(:all_definitions) do
 | 
						|
      metrics_definitions = [
 | 
						|
        { key_path: 'metric1', instrumentation_class: 'RedisHLLMetric', status: 'data_available' },
 | 
						|
        { key_path: 'metric2', instrumentation_class: 'RedisHLLMetric', status: 'implemented' },
 | 
						|
        { key_path: 'metric3', instrumentation_class: 'RedisHLLMetric', status: 'deprecated' },
 | 
						|
        { key_path: 'metric4', instrumentation_class: 'RedisHLLMetric', status: metric_status },
 | 
						|
        { key_path: 'metric5', status: 'active' },
 | 
						|
        { key_path: 'metric_missing_status' }
 | 
						|
      ]
 | 
						|
      metrics_definitions.map { |definition| described_class.new(definition[:key_path], definition.symbolize_keys) }
 | 
						|
    end
 | 
						|
 | 
						|
    before do
 | 
						|
      allow(described_class).to receive(:all).and_return(all_definitions)
 | 
						|
    end
 | 
						|
 | 
						|
    it 'includes definitions with instrumentation_class' do
 | 
						|
      expect(described_class.with_instrumentation_class.count).to eq(4)
 | 
						|
    end
 | 
						|
 | 
						|
    context 'with removed metric' do
 | 
						|
      let(:metric_status) { 'removed' }
 | 
						|
 | 
						|
      it 'excludes removed definitions' do
 | 
						|
        expect(described_class.with_instrumentation_class.count).to eq(3)
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe '#key' do
 | 
						|
    subject { definition.key }
 | 
						|
 | 
						|
    it 'returns a symbol from name' do
 | 
						|
      is_expected.to eq('uuid')
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe '#validate' do
 | 
						|
    using RSpec::Parameterized::TableSyntax
 | 
						|
 | 
						|
    where(:attribute, :value) do
 | 
						|
      :description        | nil
 | 
						|
      :value_type         | nil
 | 
						|
      :value_type         | 'test'
 | 
						|
      :status             | nil
 | 
						|
      :milestone          | nil
 | 
						|
      :data_category      | nil
 | 
						|
      :key_path           | nil
 | 
						|
      :product_group      | nil
 | 
						|
      :time_frame         | nil
 | 
						|
      :time_frame         | '29d'
 | 
						|
      :data_source        | 'other'
 | 
						|
      :data_source        | nil
 | 
						|
      :distribution       | nil
 | 
						|
      :distribution       | 'test'
 | 
						|
      :tier               | %w(test ee)
 | 
						|
      :name               | 'count_<adjective_describing>_boards'
 | 
						|
      :repair_issue_url   | nil
 | 
						|
 | 
						|
      :instrumentation_class | 'Metric_Class'
 | 
						|
      :instrumentation_class | 'metricClass'
 | 
						|
    end
 | 
						|
 | 
						|
    with_them do
 | 
						|
      before do
 | 
						|
        attributes[attribute] = value
 | 
						|
      end
 | 
						|
 | 
						|
      it 'raise exception' do
 | 
						|
        expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::MetricDefinition::InvalidError))
 | 
						|
 | 
						|
        described_class.new(path, attributes).validate!
 | 
						|
      end
 | 
						|
 | 
						|
      context 'with skip_validation' do
 | 
						|
        it 'raise exception if skip_validation: false' do
 | 
						|
          expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::MetricDefinition::InvalidError))
 | 
						|
 | 
						|
          described_class.new(path, attributes.merge( { skip_validation: false } )).validate!
 | 
						|
        end
 | 
						|
 | 
						|
        it 'does not raise exception if has skip_validation: true' do
 | 
						|
          expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception)
 | 
						|
 | 
						|
          described_class.new(path, attributes.merge( { skip_validation: true } )).validate!
 | 
						|
        end
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    context 'conditional validations' do
 | 
						|
      context 'when metric has broken status' do
 | 
						|
        it 'has to have repair issue url provided' do
 | 
						|
          attributes[:status] = 'broken'
 | 
						|
          attributes.delete(:repair_issue_url)
 | 
						|
 | 
						|
          expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).at_least(:once).with(instance_of(Gitlab::Usage::MetricDefinition::InvalidError))
 | 
						|
 | 
						|
          described_class.new(path, attributes).validate!
 | 
						|
        end
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe 'statuses' do
 | 
						|
    using RSpec::Parameterized::TableSyntax
 | 
						|
 | 
						|
    where(:status, :skip_validation?) do
 | 
						|
      'deprecated'     | true
 | 
						|
      'removed'        | true
 | 
						|
      'active'         | false
 | 
						|
    end
 | 
						|
 | 
						|
    with_them do
 | 
						|
      subject(:validation) do
 | 
						|
        described_class.new(path, attributes.merge( { status: status } )).send(:skip_validation?)
 | 
						|
      end
 | 
						|
 | 
						|
      it 'returns true/false for skip_validation' do
 | 
						|
        expect(validation).to eq(skip_validation?)
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe '.load_all!' do
 | 
						|
    let(:metric1) { Dir.mktmpdir('metric1') }
 | 
						|
    let(:metric2) { Dir.mktmpdir('metric2') }
 | 
						|
    let(:definitions) { {} }
 | 
						|
 | 
						|
    before do
 | 
						|
      allow(described_class).to receive(:paths).and_return(
 | 
						|
        [
 | 
						|
          File.join(metric1, '**', '*.yml'),
 | 
						|
          File.join(metric2, '**', '*.yml')
 | 
						|
        ]
 | 
						|
      )
 | 
						|
    end
 | 
						|
 | 
						|
    subject { described_class.send(:load_all!) }
 | 
						|
 | 
						|
    it 'has empty list when there are no definition files' do
 | 
						|
      is_expected.to be_empty
 | 
						|
    end
 | 
						|
 | 
						|
    it 'has one metric when there is one file' do
 | 
						|
      write_metric(metric1, path, yaml_content)
 | 
						|
 | 
						|
      is_expected.to be_one
 | 
						|
    end
 | 
						|
 | 
						|
    it 'when the same metric is defined multiple times raises exception' do
 | 
						|
      write_metric(metric1, path, yaml_content)
 | 
						|
      write_metric(metric2, path, yaml_content)
 | 
						|
 | 
						|
      expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(instance_of(Gitlab::Usage::MetricDefinition::InvalidError))
 | 
						|
 | 
						|
      subject
 | 
						|
    end
 | 
						|
 | 
						|
    after do
 | 
						|
      FileUtils.rm_rf(metric1)
 | 
						|
      FileUtils.rm_rf(metric2)
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe 'dump_metrics_yaml' do
 | 
						|
    let(:other_attributes) do
 | 
						|
      {
 | 
						|
        description: 'Test metric definition',
 | 
						|
        value_type: 'string',
 | 
						|
        product_category: 'collection',
 | 
						|
        product_stage: 'growth',
 | 
						|
        product_section: 'devops',
 | 
						|
        status: 'active',
 | 
						|
        milestone: '14.1',
 | 
						|
        default_generation: 'generation_1',
 | 
						|
        key_path: 'counter.category.event',
 | 
						|
        product_group: 'group::product analytics',
 | 
						|
        time_frame: 'none',
 | 
						|
        data_source: 'database',
 | 
						|
        distribution: %w(ee ce),
 | 
						|
        tier: %w(free starter premium ultimate bronze silver gold),
 | 
						|
        data_category: 'optional'
 | 
						|
      }
 | 
						|
    end
 | 
						|
 | 
						|
    let(:other_yaml_content) { other_attributes.deep_stringify_keys.to_yaml }
 | 
						|
    let(:other_path) { File.join('metrics', 'test_metric.yml') }
 | 
						|
    let(:metric1) { Dir.mktmpdir('metric1') }
 | 
						|
    let(:metric2) { Dir.mktmpdir('metric2') }
 | 
						|
 | 
						|
    before do
 | 
						|
      allow(described_class).to receive(:paths).and_return(
 | 
						|
        [
 | 
						|
          File.join(metric1, '**', '*.yml'),
 | 
						|
          File.join(metric2, '**', '*.yml')
 | 
						|
        ]
 | 
						|
      )
 | 
						|
    end
 | 
						|
 | 
						|
    after do
 | 
						|
      FileUtils.rm_rf(metric1)
 | 
						|
      FileUtils.rm_rf(metric2)
 | 
						|
    end
 | 
						|
 | 
						|
    subject { described_class.dump_metrics_yaml }
 | 
						|
 | 
						|
    it 'returns a YAML with both metrics in a sequence' do
 | 
						|
      write_metric(metric1, path, yaml_content)
 | 
						|
      write_metric(metric2, other_path, other_yaml_content)
 | 
						|
 | 
						|
      is_expected.to eq([attributes, other_attributes].map(&:deep_stringify_keys).to_yaml)
 | 
						|
    end
 | 
						|
  end
 | 
						|
end
 |