# frozen_string_literal: true require 'spec_helper' # As each associated, backwards-compatible experiment gets cleaned up and removed from the EXPERIMENTS list, its key will also get removed from this list. Once the list here is empty, we can remove the backwards compatibility code altogether. # Originally created as part of https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45733 for https://gitlab.com/gitlab-org/gitlab/-/issues/270858. RSpec.describe Gitlab::Experimentation::EXPERIMENTS do it 'temporarily ensures we know what experiments exist for backwards compatibility' do expected_experiment_keys = [ :upgrade_link_in_user_menu_a, :invite_members_version_b, :invite_members_empty_group_version_a, :contact_sales_btn_in_app ] backwards_compatible_experiment_keys = described_class.filter { |_, v| v[:use_backwards_compatible_subject_index] }.keys expect(backwards_compatible_experiment_keys).not_to be_empty, "Oh, hey! Let's clean up that :use_backwards_compatible_subject_index stuff now :D" expect(backwards_compatible_experiment_keys).to match(expected_experiment_keys) end end RSpec.describe Gitlab::Experimentation do using RSpec::Parameterized::TableSyntax before do stub_const('Gitlab::Experimentation::EXPERIMENTS', { backwards_compatible_test_experiment: { tracking_category: 'Team', use_backwards_compatible_subject_index: true }, test_experiment: { tracking_category: 'Team' }, tabular_experiment: { tracking_category: 'Team', rollout_strategy: rollout_strategy } }) skip_feature_flags_yaml_validation skip_default_enabled_yaml_check Feature.enable_percentage_of_time(:backwards_compatible_test_experiment_experiment_percentage, enabled_percentage) Feature.enable_percentage_of_time(:test_experiment_experiment_percentage, enabled_percentage) allow(Gitlab).to receive(:com?).and_return(true) end let(:enabled_percentage) { 10 } let(:rollout_strategy) { nil } describe '.get_experiment' do subject { described_class.get_experiment(:test_experiment) } context 'returns experiment' do it { is_expected.to be_instance_of(Gitlab::Experimentation::Experiment) } end context 'experiment is not defined' do subject { described_class.get_experiment(:missing_experiment) } it { is_expected.to be_nil } end end describe '.active?' do subject { described_class.active?(:test_experiment) } context 'feature toggle is enabled' do it { is_expected.to eq(true) } end describe 'experiment is not defined' do it 'returns false' do expect(described_class.active?(:missing_experiment)).to eq(false) end end describe 'experiment is disabled' do let(:enabled_percentage) { 0 } it { is_expected.to eq(false) } end end describe '.in_experiment_group?' do context 'with new index calculation' do let(:enabled_percentage) { 50 } let(:experiment_subject) { 'z' } # Zlib.crc32('test_experimentz') % 100 = 33 subject { described_class.in_experiment_group?(:test_experiment, subject: experiment_subject) } context 'when experiment is active' do context 'when subject is part of the experiment' do it { is_expected.to eq(true) } end context 'when subject is not part of the experiment' do let(:experiment_subject) { 'a' } # Zlib.crc32('test_experimenta') % 100 = 61 it { is_expected.to eq(false) } end context 'when subject has a global_id' do let(:experiment_subject) { double(:subject, to_global_id: 'z') } it { is_expected.to eq(true) } end context 'when subject is nil' do let(:experiment_subject) { nil } it { is_expected.to eq(false) } end context 'when subject is an empty string' do let(:experiment_subject) { '' } it { is_expected.to eq(false) } end end context 'when experiment is not active' do before do allow(described_class).to receive(:active?).and_return(false) end it { is_expected.to eq(false) } end end context 'with backwards compatible index calculation' do let(:experiment_subject) { 'abcd' } # Digest::SHA1.hexdigest('abcd').hex % 100 = 7 subject { described_class.in_experiment_group?(:backwards_compatible_test_experiment, subject: experiment_subject) } context 'when experiment is active' do before do allow(described_class).to receive(:active?).and_return(true) end context 'when subject is part of the experiment' do it { is_expected.to eq(true) } end context 'when subject is not part of the experiment' do let(:experiment_subject) { 'abc' } # Digest::SHA1.hexdigest('abc').hex % 100 = 17 it { is_expected.to eq(false) } end context 'when subject has a global_id' do let(:experiment_subject) { double(:subject, to_global_id: 'abcd') } it { is_expected.to eq(true) } end context 'when subject is nil' do let(:experiment_subject) { nil } it { is_expected.to eq(false) } end context 'when subject is an empty string' do let(:experiment_subject) { '' } it { is_expected.to eq(false) } end end context 'when experiment is not active' do before do allow(described_class).to receive(:active?).and_return(false) end it { is_expected.to eq(false) } end end end describe '.log_invalid_rollout' do subject { described_class.log_invalid_rollout(:test_experiment, 1) } before do allow(described_class).to receive(:valid_subject_for_rollout_strategy?).and_return(valid) end context 'subject is not valid for experiment' do let(:valid) { false } it 'logs a warning message' do expect_next_instance_of(Gitlab::ExperimentationLogger) do |logger| expect(logger) .to receive(:warn) .with( message: 'Subject must conform to the rollout strategy', experiment_key: :test_experiment, subject: 'Integer', rollout_strategy: :cookie ) end subject end end context 'subject is valid for experiment' do let(:valid) { true } it 'does not log a warning message' do expect(Gitlab::ExperimentationLogger).not_to receive(:build) subject end end end describe '.valid_subject_for_rollout_strategy?' do subject { described_class.valid_subject_for_rollout_strategy?(:tabular_experiment, experiment_subject) } where(:rollout_strategy, :experiment_subject, :result) do :cookie | nil | true nil | nil | true :cookie | 'string' | true nil | User.new | false :user | User.new | true :group | User.new | false :group | Group.new | true end with_them do it { is_expected.to be(result) } end end end