summaryrefslogtreecommitdiff
path: root/spec/experiments
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-12-16 09:10:26 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-12-16 09:10:26 +0000
commita11e5bac528dd5c84e19acb3db1b8addd698726e (patch)
tree70a10e6837c2a76cad6cc7fc603db77c6a4bf39f /spec/experiments
parent7c81500115967e071321d9d2c59bbaa7dc76c4b4 (diff)
downloadgitlab-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.rb66
-rw-r--r--spec/experiments/application_experiment_spec.rb127
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