diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-16 09:10:26 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-16 09:10:26 +0000 |
commit | a11e5bac528dd5c84e19acb3db1b8addd698726e (patch) | |
tree | 70a10e6837c2a76cad6cc7fc603db77c6a4bf39f /spec/experiments | |
parent | 7c81500115967e071321d9d2c59bbaa7dc76c4b4 (diff) | |
download | gitlab-ce-a11e5bac528dd5c84e19acb3db1b8addd698726e.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/experiments')
-rw-r--r-- | spec/experiments/application_experiment/cache_spec.rb | 66 | ||||
-rw-r--r-- | spec/experiments/application_experiment_spec.rb | 127 |
2 files changed, 193 insertions, 0 deletions
diff --git a/spec/experiments/application_experiment/cache_spec.rb b/spec/experiments/application_experiment/cache_spec.rb new file mode 100644 index 00000000000..a420d557155 --- /dev/null +++ b/spec/experiments/application_experiment/cache_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ApplicationExperiment::Cache do + let(:key_name) { 'experiment_name' } + let(:field_name) { 'abc123' } + let(:key_field) { [key_name, field_name].join(':') } + let(:shared_state) { Gitlab::Redis::SharedState } + + around do |example| + shared_state.with { |r| r.del(key_name) } + example.run + shared_state.with { |r| r.del(key_name) } + end + + it "allows reading, writing and deleting", :aggregate_failures do + # we test them all together because they are largely interdependent + + expect(subject.read(key_field)).to be_nil + expect(shared_state.with { |r| r.hget(key_name, field_name) }).to be_nil + + subject.write(key_field, 'value') + + expect(subject.read(key_field)).to eq('value') + expect(shared_state.with { |r| r.hget(key_name, field_name) }).to eq('value') + + subject.delete(key_field) + + expect(subject.read(key_field)).to be_nil + expect(shared_state.with { |r| r.hget(key_name, field_name) }).to be_nil + end + + it "handles the fetch with a block behavior (which is read/write)" do + expect(subject.fetch(key_field) { 'value1' }).to eq('value1') # rubocop:disable Style/RedundantFetchBlock + expect(subject.fetch(key_field) { 'value2' }).to eq('value1') # rubocop:disable Style/RedundantFetchBlock + end + + it "can clear a whole experiment cache key" do + subject.write(key_field, 'value') + subject.clear(key: key_field) + + expect(shared_state.with { |r| r.get(key_name) }).to be_nil + end + + it "doesn't allow clearing a key from the cache that's not a hash (definitely not an experiment)" do + shared_state.with { |r| r.set(key_name, 'value') } + + expect { subject.clear(key: key_name) }.to raise_error( + ArgumentError, + 'invalid call to clear a non-hash cache key' + ) + end + + context "when the :caching_experiments feature is disabled" do + before do + stub_feature_flags(caching_experiments: false) + end + + it "doesn't write to the cache" do + subject.write(key_field, 'value') + + expect(subject.read(key_field)).to be_nil + end + end +end diff --git a/spec/experiments/application_experiment_spec.rb b/spec/experiments/application_experiment_spec.rb new file mode 100644 index 00000000000..ece52d37351 --- /dev/null +++ b/spec/experiments/application_experiment_spec.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ApplicationExperiment do + subject { described_class.new(:stub) } + + describe "publishing results" do + it "tracks the assignment" do + expect(subject).to receive(:track).with(:assignment) + + subject.publish(nil) + end + + it "pushes the experiment knowledge into the client using Gon.global" do + expect(Gon.global).to receive(:push).with( + { + experiment: { + 'stub' => { # string key because it can be namespaced + experiment: 'stub', + key: 'e8f65fd8d973f9985dc7ea3cf1614ae1', + variant: 'control' + } + } + }, + true + ) + + subject.publish(nil) + end + end + + describe "tracking events", :snowplow do + it "doesn't track if excluded" do + subject.exclude { true } + + subject.track(:action) + + expect_no_snowplow_event + end + + it "tracks the event with the expected arguments and merged contexts" do + subject.track(:action, property: '_property_', context: [ + SnowplowTracker::SelfDescribingJson.new('iglu:com.gitlab/fake/jsonschema/0-0-0', { data: '_data_' }) + ]) + + expect_snowplow_event( + category: 'stub', + action: 'action', + property: '_property_', + context: [ + { + schema: 'iglu:com.gitlab/fake/jsonschema/0-0-0', + data: { data: '_data_' } + }, + { + schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/0-3-0', + data: { experiment: 'stub', key: 'e8f65fd8d973f9985dc7ea3cf1614ae1', variant: 'control' } + } + ] + ) + end + end + + describe "variant resolution" do + it "returns nil when not rolled out" do + stub_feature_flags(stub: false) + + expect(subject.variant.name).to eq('control') + end + + context "when rolled out to 100%" do + it "returns the first variant name" do + subject.try(:variant1) {} + subject.try(:variant2) {} + + expect(subject.variant.name).to eq('variant1') + end + end + end + + context "when caching" do + let(:cache) { ApplicationExperiment::Cache.new } + + before do + cache.clear(key: subject.name) + + subject.use { } # setup the control + subject.try { } # setup the candidate + + allow(Gitlab::Experiment::Configuration).to receive(:cache).and_return(cache) + end + + it "caches the variant determined by the variant resolver" do + expect(subject.variant.name).to eq('candidate') # we should be in the experiment + + subject.run + + expect(cache.read(subject.cache_key)).to eq('candidate') + end + + it "doesn't cache a variant if we don't explicitly provide one" do + # by not caching "empty" variants, we effectively create a mostly + # optimal combination of caching and rollout flexibility. If we cached + # every control variant assigned, we'd inflate the cache size and + # wouldn't be able to roll out to subjects that we'd already assigned to + # the control. + stub_feature_flags(stub: false) # simulate being not rolled out + + expect(subject.variant.name).to eq('control') # if we ask, it should be control + + subject.run + + expect(cache.read(subject.cache_key)).to be_nil + end + + it "caches a control variant if we assign it specifically" do + # by specifically assigning the control variant here, we're guaranteeing + # that this context will always get the control variant unless we delete + # the field from the cache (or clear the entire experiment cache) -- or + # write code that would specify a different variant. + subject.run(:control) + + expect(cache.read(subject.cache_key)).to eq('control') + end + end +end |