pax_global_header00006660000000000000000000000064132532753730014524gustar00rootroot0000000000000052 comment=9af4ba3d264db44106c4a8c8719bbc8a8da0c008 ruby-rollout-2.4.3/000077500000000000000000000000001325327537300142115ustar00rootroot00000000000000ruby-rollout-2.4.3/.document000066400000000000000000000000741325327537300160310ustar00rootroot00000000000000README.rdoc lib/**/*.rb bin/* features/**/*.feature LICENSE ruby-rollout-2.4.3/.gitignore000066400000000000000000000002501325327537300161760ustar00rootroot00000000000000## MAC OS .DS_Store ## TEXTMATE *.tmproj tmtags ## EMACS *~ \#* .\#* ## VIM *.swp ## PROJECT::GENERAL coverage rdoc pkg *.gem gemfiles/*.lock ## PROJECT::SPECIFIC ruby-rollout-2.4.3/.rspec000066400000000000000000000000101325327537300153150ustar00rootroot00000000000000--color ruby-rollout-2.4.3/.travis.yml000066400000000000000000000004211325327537300163170ustar00rootroot00000000000000language: ruby cache: bundler sudo: false services: - redis-server rvm: - 2.4.1 - 2.3.1 - 2.2 - 2.1 - 2.0.0 - 1.9.3 - jruby-19mode env: - USE_REAL_REDIS=true gemfile: - gemfiles/redis_3.gemfile - gemfiles/redis_4.gemfile script: - bundle exec rspec ruby-rollout-2.4.3/Appraisals000066400000000000000000000001521325327537300162310ustar00rootroot00000000000000appraise "redis-3" do gem "redis", "~> 3.3.2" end appraise "redis-4" do gem "redis", "4.0.0.rc1" end ruby-rollout-2.4.3/Gemfile000066400000000000000000000000471325327537300155050ustar00rootroot00000000000000source "https://rubygems.org" gemspec ruby-rollout-2.4.3/Gemfile.lock000066400000000000000000000020351325327537300164330ustar00rootroot00000000000000PATH remote: . specs: rollout (2.4.3) GEM remote: https://rubygems.org/ specs: appraisal (2.2.0) bundler rake thor (>= 0.14.0) codeclimate-test-reporter (1.0.8) simplecov (<= 0.13) diff-lcs (1.3) docile (1.1.5) fakeredis (0.6.0) redis (~> 3.2) json (2.1.0) rake (12.0.0) redis (3.3.3) rspec (3.6.0) rspec-core (~> 3.6.0) rspec-expectations (~> 3.6.0) rspec-mocks (~> 3.6.0) rspec-core (3.6.0) rspec-support (~> 3.6.0) rspec-expectations (3.6.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.6.0) rspec-mocks (3.6.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.6.0) rspec-support (3.6.0) simplecov (0.13.0) docile (~> 1.1.0) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.1) thor (0.19.4) PLATFORMS ruby DEPENDENCIES appraisal bundler (>= 1.0.0) codeclimate-test-reporter fakeredis redis rollout! rspec simplecov BUNDLED WITH 1.15.1 ruby-rollout-2.4.3/LICENSE000066400000000000000000000020631325327537300152170ustar00rootroot00000000000000Copyright (c) 2010-InfinityAndBeyond BitLove, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ruby-rollout-2.4.3/README.md000066400000000000000000000110531325327537300154700ustar00rootroot00000000000000# rollout Fast feature flags based on Redis. [![Build Status](https://travis-ci.org/fetlife/rollout.svg?branch=master)](https://travis-ci.org/fetlife/rollout) [![Code Climate](https://codeclimate.com/github/FetLife/rollout/badges/gpa.svg)](https://codeclimate.com/github/fetlife/rollout) [![Test Coverage](https://codeclimate.com/github/FetLife/rollout/badges/coverage.svg)](https://codeclimate.com/github/fetlife/rollout/coverage) [![Dependency Status](https://gemnasium.com/FetLife/rollout.svg)](https://gemnasium.com/fetlife/rollout) ## Install it ```bash gem install rollout ``` ## How it works Initialize a rollout object. I assign it to a global var. ```ruby require 'redis' $redis = Redis.new $rollout = Rollout.new($redis) ``` Update data specific to a feature: ```ruby $rollout.set_feature_data(:chat, description: 'foo', release_date: 'bar', whatever: 'baz') ``` Check whether a feature is active for a particular user: ```ruby $rollout.active?(:chat, User.first) # => true/false ``` Check whether a feature is active globally: ```ruby $rollout.active?(:chat) ``` You can activate features using a number of different mechanisms. ## Groups Rollout ships with one group by default: "all", which does exactly what it sounds like. You can activate the all group for the chat feature like this: ```ruby $rollout.activate_group(:chat, :all) ``` You might also want to define your own groups. We have one for our caretakers: ```ruby $rollout.define_group(:caretakers) do |user| user.caretaker? end ``` You can activate multiple groups per feature. Deactivate groups like this: ```ruby $rollout.deactivate_group(:chat, :all) ``` ## Specific Users You might want to let a specific user into a beta test or something. If that user isn't part of an existing group, you can let them in specifically: ```ruby $rollout.activate_user(:chat, @user) ``` Deactivate them like this: ```ruby $rollout.deactivate_user(:chat, @user) ``` ## User Percentages If you're rolling out a new feature, you might want to test the waters by slowly enabling it for a percentage of your users. ```ruby $rollout.activate_percentage(:chat, 20) ``` The algorithm for determining which users get let in is this: ```ruby CRC32(user.id) % 100_000 < percentage * 1_000 ``` So, for 20%, users 0, 1, 10, 11, 20, 21, etc would be allowed in. Those users would remain in as the percentage increases. Deactivate all percentages like this: ```ruby $rollout.deactivate_percentage(:chat) ``` _Note that activating a feature for 100% of users will also make it active "globally". That is when calling Rollout#active? without a user object._ In some cases you might want to have a feature activated for a random set of users. It can come specially handy when using Rollout for split tests. ```ruby $rollout = Rollout.new($redis, randomize_percentage: true) ``` When on `randomize_percentage` will make sure that 50% of users for feature A are selected independently from users for feature B. ## Global actions While groups can come in handy, the actual global setter for a feature does not require a group to be passed. ```ruby $rollout.activate(:chat) ``` In that case you can check the global availability of a feature using the following ```ruby $rollout.active?(:chat) ``` And if something is wrong you can set a feature off for everybody using Deactivate everybody at once: ```ruby $rollout.deactivate(:chat) ``` For many of our features, we keep track of error rates using redis, and deactivate them automatically when a threshold is reached to prevent service failures from cascading. See https://github.com/jamesgolick/degrade for the failure detection code. ## Namespacing Rollout separates its keys from other keys in the data store using the "feature" keyspace. If you're using redis, you can namespace keys further to support multiple environments by using the [redis-namespace](https://github.com/resque/redis-namespace) gem. ```ruby $ns = Redis::Namespace.new(Rails.env, redis: $redis) $rollout = Rollout.new($ns) $rollout.activate_group(:chat, :all) ``` This example would use the "development:feature:chat:groups" key. ## Frontend / UI * [Rollout-Dashboard](https://github.com/fiverr/rollout_dashboard/) ## Implementations in other languages * Python: https://github.com/asenchi/proclaim * PHP: https://github.com/opensoft/rollout * Clojure: https://github.com/yeller/shoutout ## Contributors * James Golick - Creator - https://github.com/jamesgolick * Eric Rafaloff - Maintainer - https://github.com/EricR ## Copyright Copyright (c) 2010-InfinityAndBeyond BitLove, Inc. See LICENSE for details. ruby-rollout-2.4.3/Rakefile000066400000000000000000000002161325327537300156550ustar00rootroot00000000000000begin require "rspec/core/rake_task" RSpec::Core::RakeTask.new(:spec) task default: :spec rescue LoadError # no rspec available end ruby-rollout-2.4.3/gemfiles/000077500000000000000000000000001325327537300160045ustar00rootroot00000000000000ruby-rollout-2.4.3/gemfiles/redis_3.gemfile000066400000000000000000000003001325327537300206570ustar00rootroot00000000000000# This file was generated by Appraisal source "https://rubygems.org" if RUBY_VERSION >= '2.0' gem "json" else gem "json", "~> 1.8.3" end gem "redis", "~> 3.3.2" gemspec :path => "../" ruby-rollout-2.4.3/gemfiles/redis_4.gemfile000066400000000000000000000003011325327537300206610ustar00rootroot00000000000000# This file was generated by Appraisal source "https://rubygems.org" if RUBY_VERSION >= '2.0' gem "json" else gem "json", "~> 1.8.3" end gem "redis", "4.0.0.rc1" gemspec :path => "../" ruby-rollout-2.4.3/lib/000077500000000000000000000000001325327537300147575ustar00rootroot00000000000000ruby-rollout-2.4.3/lib/rollout.rb000066400000000000000000000147521325327537300170150ustar00rootroot00000000000000require "rollout/version" require "zlib" require "set" require "json" class Rollout RAND_BASE = (2**32 - 1) / 100.0 class Feature attr_accessor :groups, :users, :percentage, :data attr_reader :name, :options def initialize(name, string = nil, opts = {}) @options = opts @name = name if string raw_percentage,raw_users,raw_groups,raw_data = string.split('|', 4) @percentage = raw_percentage.to_f @users = users_from_string(raw_users) @groups = groups_from_string(raw_groups) @data = raw_data.nil? || raw_data.strip.empty? ? {} : JSON.parse(raw_data) else clear end end def serialize "#{@percentage}|#{@users.to_a.join(",")}|#{@groups.to_a.join(",")}|#{serialize_data}" end def add_user(user) id = user_id(user) @users << id unless @users.include?(id) end def remove_user(user) @users.delete(user_id(user)) end def add_group(group) @groups << group.to_sym unless @groups.include?(group.to_sym) end def remove_group(group) @groups.delete(group.to_sym) end def clear @groups = groups_from_string("") @users = users_from_string("") @percentage = 0 @data = {} end def active?(rollout, user) if user id = user_id(user) user_in_percentage?(id) || user_in_active_users?(id) || user_in_active_group?(user, rollout) else @percentage == 100 end end def user_in_active_users?(user) @users.include?(user_id(user)) end def to_hash { percentage: @percentage, groups: @groups, users: @users } end private def user_id(user) if user.is_a?(Integer) || user.is_a?(String) user.to_s else user.send(id_user_by).to_s end end def id_user_by @options[:id_user_by] || :id end def user_in_percentage?(user) Zlib.crc32(user_id_for_percentage(user)) < RAND_BASE * @percentage end def user_id_for_percentage(user) if @options[:randomize_percentage] user_id(user).to_s + @name.to_s else user_id(user) end end def user_in_active_group?(user, rollout) @groups.any? do |g| rollout.active_in_group?(g, user) end end def serialize_data return "" unless @data.is_a? Hash @data.to_json end def users_from_string(raw_users) users = (raw_users || "").split(",").map(&:to_s) if @options[:use_sets] users.to_set else users end end def groups_from_string(raw_groups) groups = (raw_groups || "").split(",").map(&:to_sym) if @options[:use_sets] groups.to_set else groups end end end def initialize(storage, opts = {}) @storage = storage @options = opts @groups = { all: lambda { |user| true } } end def activate(feature) with_feature(feature) do |f| f.percentage = 100 end end def deactivate(feature) with_feature(feature) do |f| f.clear end end def delete(feature) features = (@storage.get(features_key) || "").split(",") features.delete(feature.to_s) @storage.set(features_key, features.join(",")) @storage.del(key(feature)) end def set(feature, desired_state) with_feature(feature) do |f| if desired_state f.percentage = 100 else f.clear end end end def activate_group(feature, group) with_feature(feature) do |f| f.add_group(group) end end def deactivate_group(feature, group) with_feature(feature) do |f| f.remove_group(group) end end def activate_user(feature, user) with_feature(feature) do |f| f.add_user(user) end end def deactivate_user(feature, user) with_feature(feature) do |f| f.remove_user(user) end end def activate_users(feature, users) with_feature(feature) do |f| users.each{|user| f.add_user(user)} end end def deactivate_users(feature, users) with_feature(feature) do |f| users.each{|user| f.remove_user(user)} end end def set_users(feature, users) with_feature(feature) do |f| f.users = [] users.each{|user| f.add_user(user)} end end def define_group(group, &block) @groups[group.to_sym] = block end def active?(feature, user = nil) feature = get(feature) feature.active?(self, user) end def user_in_active_users?(feature, user = nil) feature = get(feature) feature.user_in_active_users?(user) end def inactive?(feature, user = nil) !active?(feature, user) end def activate_percentage(feature, percentage) with_feature(feature) do |f| f.percentage = percentage end end def deactivate_percentage(feature) with_feature(feature) do |f| f.percentage = 0 end end def active_in_group?(group, user) f = @groups[group.to_sym] f && f.call(user) end def get(feature) string = @storage.get(key(feature)) Feature.new(feature, string, @options) end def set_feature_data(feature, data) with_feature(feature) do |f| f.data.merge!(data) if data.is_a? Hash end end def clear_feature_data(feature) with_feature(feature) do |f| f.data = {} end end def multi_get(*features) feature_keys = features.map{ |feature| key(feature) } @storage.mget(*feature_keys).map.with_index { |string, index| Feature.new(features[index], string, @options) } end def features (@storage.get(features_key) || "").split(",").map(&:to_sym) end def feature_states(user = nil) features.each_with_object({}) do |f, hash| hash[f] = active?(f, user) end end def active_features(user = nil) features.select do |f| active?(f, user) end end def clear! features.each do |feature| with_feature(feature) { |f| f.clear } @storage.del(key(feature)) end @storage.del(features_key) end def exists?(feature) @storage.exists(key(feature)) end private def key(name) "feature:#{name}" end def features_key "feature:__features__" end def with_feature(feature) f = get(feature) yield(f) save(f) end def save(feature) @storage.set(key(feature.name), feature.serialize) @storage.set(features_key, (features | [feature.name.to_sym]).join(",")) end end ruby-rollout-2.4.3/lib/rollout/000077500000000000000000000000001325327537300164575ustar00rootroot00000000000000ruby-rollout-2.4.3/lib/rollout/version.rb000066400000000000000000000000461325327537300204710ustar00rootroot00000000000000class Rollout VERSION = "2.4.3" end ruby-rollout-2.4.3/rollout.gemspec000066400000000000000000000017541325327537300172650ustar00rootroot00000000000000# -*- encoding: utf-8 -*- $:.push File.expand_path("../lib", __FILE__) require "rollout/version" Gem::Specification.new do |s| s.name = "rollout" s.version = Rollout::VERSION s.authors = ["James Golick"] s.email = ["jamesgolick@gmail.com"] s.description = "Feature flippers with redis." s.summary = "Feature flippers with redis." s.homepage = "https://github.com/FetLife/rollout" s.license = "MIT" s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } s.require_paths = ["lib"] s.add_development_dependency "rspec" s.add_development_dependency "appraisal" s.add_development_dependency "bundler", ">= 1.0.0" s.add_development_dependency "redis" s.add_development_dependency "fakeredis" s.add_development_dependency "simplecov" s.add_development_dependency "codeclimate-test-reporter" end ruby-rollout-2.4.3/spec/000077500000000000000000000000001325327537300151435ustar00rootroot00000000000000ruby-rollout-2.4.3/spec/rollout_spec.rb000066400000000000000000000520031325327537300202020ustar00rootroot00000000000000require "spec_helper" RSpec.describe "Rollout" do before do @redis = Redis.new @rollout = Rollout.new(@redis) end describe "when a group is activated" do before do @rollout.define_group(:fivesonly) { |user| user.id == 5 } @rollout.activate_group(:chat, :fivesonly) end it "the feature is active for users for which the block evaluates to true" do expect(@rollout).to be_active(:chat, double(id: 5)) end it "is not active for users for which the block evaluates to false" do expect(@rollout).not_to be_active(:chat, double(id: 1)) end it "is not active if a group is found in Redis but not defined in Rollout" do @rollout.activate_group(:chat, :fake) expect(@rollout).not_to be_active(:chat, double(id: 1)) end end describe "the default all group" do before do @rollout.activate_group(:chat, :all) end it "evaluates to true no matter what" do expect(@rollout).to be_active(:chat, double(id: 0)) end end describe "deactivating a group" do before do @rollout.define_group(:fivesonly) { |user| user.id == 5 } @rollout.activate_group(:chat, :all) @rollout.activate_group(:chat, :some) @rollout.activate_group(:chat, :fivesonly) @rollout.deactivate_group(:chat, :all) @rollout.deactivate_group(:chat, "some") end it "deactivates the rules for that group" do expect(@rollout).not_to be_active(:chat, double(id: 10)) end it "leaves the other groups active" do expect(@rollout.get(:chat).groups).to eq [:fivesonly] end it "leaves the other groups active using sets" do @options = @rollout.instance_variable_get("@options") @options[:use_sets] = true expect(@rollout.get(:chat).groups).to eq [:fivesonly].to_set end end describe "deactivating a feature completely" do before do @rollout.define_group(:fivesonly) { |user| user.id == 5 } @rollout.activate_group(:chat, :all) @rollout.activate_group(:chat, :fivesonly) @rollout.activate_user(:chat, double(id: 51)) @rollout.activate_percentage(:chat, 100) @rollout.activate(:chat) @rollout.deactivate(:chat) end it "removes all of the groups" do expect(@rollout).not_to be_active(:chat, double(id: 0)) end it "removes all of the users" do expect(@rollout).not_to be_active(:chat, double(id: 51)) end it "removes the percentage" do expect(@rollout).not_to be_active(:chat, double(id: 24)) end it "removes globally" do expect(@rollout).not_to be_active(:chat) end end describe "activating a specific user" do before do @rollout.activate_user(:chat, double(id: 42)) end it "is active for that user" do expect(@rollout).to be_active(:chat, double(id: 42)) end it "remains inactive for other users" do expect(@rollout).not_to be_active(:chat, double(id: 24)) end end describe "activating a specific user by ID" do before do @rollout.activate_user(:chat, 42) end it "is active for that user" do expect(@rollout).to be_active(:chat, double(id: 42)) end it "remains inactive for other users" do expect(@rollout).not_to be_active(:chat, double(id: 24)) end end describe "activating a specific user with a string id" do before do @rollout.activate_user(:chat, double(id: "user-72")) end it "is active for that user" do expect(@rollout).to be_active(:chat, double(id: "user-72")) end it "remains inactive for other users" do expect(@rollout).not_to be_active(:chat, double(id: "user-12")) end end describe "activating a group of users" do context "specified by user objects" do let(:users) { [double(id: 1), double(id: 2), double(id: 3)] } before { @rollout.activate_users(:chat, users) } it "is active for the given users" do users.each { |user| expect(@rollout).to be_active(:chat, user) } end it "remains inactive for other users" do expect(@rollout).not_to be_active(:chat, double(id: 4)) end end context "specified by user ids" do let(:users) { [1, 2, 3] } before { @rollout.activate_users(:chat, users) } it "is active for the given users" do users.each { |user| expect(@rollout).to be_active(:chat, user) } end it "remains inactive for other users" do expect(@rollout).not_to be_active(:chat, 4) end end end describe "deactivating a specific user" do before do @rollout.activate_user(:chat, double(id: 42)) @rollout.activate_user(:chat, double(id: 4242)) @rollout.activate_user(:chat, double(id: 24)) @rollout.deactivate_user(:chat, double(id: 42)) @rollout.deactivate_user(:chat, double(id: "4242")) end it "that user should no longer be active" do expect(@rollout).not_to be_active(:chat, double(id: 42)) end it "remains active for other active users" do @options = @rollout.instance_variable_get("@options") @options[:use_sets] = false expect(@rollout.get(:chat).users).to eq %w(24) end it "remains active for other active users using sets" do @options = @rollout.instance_variable_get("@options") @options[:use_sets] = true expect(@rollout.get(:chat).users).to eq %w(24).to_set end end describe "deactivating a group of users" do context "specified by user objects" do let(:active_users) { [double(id: 1), double(id: 2)] } let(:inactive_users) { [double(id: 3), double(id: 4)] } before do @rollout.activate_users(:chat, active_users + inactive_users) @rollout.deactivate_users(:chat, inactive_users) end it "is active for the active users" do active_users.each { |user| expect(@rollout).to be_active(:chat, user) } end it "is not active for inactive users" do inactive_users.each { |user| expect(@rollout).not_to be_active(:chat, user) } end end context "specified by user ids" do let(:active_users) { [1, 2] } let(:inactive_users) { [3, 4] } before do @rollout.activate_users(:chat, active_users + inactive_users) @rollout.deactivate_users(:chat, inactive_users) end it "is active for the active users" do active_users.each { |user| expect(@rollout).to be_active(:chat, user) } end it "is not active for inactive users" do inactive_users.each { |user| expect(@rollout).not_to be_active(:chat, user) } end end end describe 'set a group of users' do it 'should replace the users with the given array' do users = %w(1 2 3 4) @rollout.activate_users(:chat, %w(10 20 30)) @rollout.set_users(:chat, users) expect(@rollout.get(:chat).users).to eq(users) end end describe "activating a feature globally" do before do @rollout.activate(:chat) end it "activates the feature" do expect(@rollout).to be_active(:chat) end it "sets @data to empty hash" do expect(@rollout.get(:chat).data).to eq({}) end end describe "activating a feature for a percentage of users" do before do @rollout.activate_percentage(:chat, 20) end it "activates the feature for that percentage of the users" do expect((1..100).select { |id| @rollout.active?(:chat, double(id: id)) }.length).to be_within(2).of(20) end end describe "activating a feature for a percentage of users" do before do @rollout.activate_percentage(:chat, 20) end it "activates the feature for that percentage of the users" do expect((1..200).select { |id| @rollout.active?(:chat, double(id: id)) }.length).to be_within(4).of(40) end end describe "activating a feature for a percentage of users" do before do @rollout.activate_percentage(:chat, 5) end it "activates the feature for that percentage of the users" do expect((1..100).select { |id| @rollout.active?(:chat, double(id: id)) }.length).to be_within(2).of(5) end end describe "activating a feature for a percentage of users" do before do @rollout.activate_percentage(:chat, 0.1) end it "activates the feature for that percentage of the users" do expect((1..10_000).to_set.select { |id| @rollout.active?(:chat, double(id: id)) }.length).to be_within(2).of(10) end end describe "activating a feature for a percentage of users" do before do @rollout.activate_percentage(:chat, 20) @rollout.activate_percentage(:beta, 20) @options = @rollout.instance_variable_get("@options") end it "activates the feature for a random set of users when opt is set" do @options[:randomize_percentage] = true chat_users = (1..100).select { |id| @rollout.active?(:chat, double(id: id)) } beta_users = (1..100).select { |id| @rollout.active?(:beta, double(id: id)) } expect(chat_users).not_to eq beta_users end it "activates the feature for the same set of users when opt is not set" do @options[:randomize_percentage] = false chat_users = (1..100).select { |id| @rollout.active?(:chat, double(id: id)) } beta_users = (1..100).select { |id| @rollout.active?(:beta, double(id: id)) } expect(chat_users).to eq beta_users end end describe "activating a feature for a group as a string" do before do @rollout.define_group(:admins) { |user| user.id == 5 } @rollout.activate_group(:chat, "admins") end it "the feature is active for users for which the block evaluates to true" do expect(@rollout).to be_active(:chat, double(id: 5)) end it "is not active for users for which the block evaluates to false" do expect(@rollout).not_to be_active(:chat, double(id: 1)) end end describe "deactivating the percentage of users" do before do @rollout.activate_percentage(:chat, 100) @rollout.deactivate_percentage(:chat) end it "becomes inactivate for all users" do expect(@rollout).not_to be_active(:chat, double(id: 24)) end end describe "deactivating the feature globally" do before do @rollout.activate(:chat) @rollout.deactivate(:chat) end it "becomes inactivate" do expect(@rollout).not_to be_active(:chat) end end describe "setting a feature on" do before do @rollout.set(:chat, true) end it "becomes activated" do expect(@rollout).to be_active(:chat) end end describe "setting a feature off" do before do @rollout.set(:chat, false) end it "becomes inactivated" do expect(@rollout).not_to be_active(:chat) end end describe "deleting a feature" do before do @rollout.set(:chat, true) end context "when feature was passed as string" do it "should be removed from features list" do expect(@rollout.features.size).to eq 1 @rollout.delete('chat') expect(@rollout.features.size).to eq 0 end end it "should be removed from features list" do expect(@rollout.features.size).to eq 1 @rollout.delete(:chat) expect(@rollout.features.size).to eq 0 end it "should have metadata cleared" do expect(@rollout.get(:chat).percentage).to eq 100 @rollout.delete(:chat) expect(@rollout.get(:chat).percentage).to eq 0 end end describe "keeps a list of features" do it "saves the feature" do @rollout.activate(:chat) expect(@rollout.features).to be_include(:chat) end it "does not contain doubles" do @rollout.activate(:chat) @rollout.activate(:chat) expect(@rollout.features.size).to eq(1) end it "does not contain doubles when using string" do @rollout.activate(:chat) @rollout.activate("chat") expect(@rollout.features.size).to eq(1) end end describe "#get" do before do @rollout.activate_percentage(:chat, 10) @rollout.activate_group(:chat, :caretakers) @rollout.activate_group(:chat, :greeters) @rollout.activate(:signup) @rollout.activate_user(:chat, double(id: 42)) end it "returns the feature object" do feature = @rollout.get(:chat) expect(feature.groups).to eq [:caretakers, :greeters] expect(feature.percentage).to eq 10 expect(feature.users).to eq %w(42) expect(feature.to_hash).to eq( groups: [:caretakers, :greeters], percentage: 10, users: %w(42) ) feature = @rollout.get(:signup) expect(feature.groups).to be_empty expect(feature.users).to be_empty expect(feature.percentage).to eq(100) end it "returns the feature objects using sets" do @options = @rollout.instance_variable_get("@options") @options[:use_sets] = true feature = @rollout.get(:chat) expect(feature.groups).to eq [:caretakers, :greeters].to_set expect(feature.percentage).to eq 10 expect(feature.users).to eq %w(42).to_set expect(feature.to_hash).to eq( groups: [:caretakers, :greeters].to_set, percentage: 10, users: %w(42).to_set ) feature = @rollout.get(:signup) expect(feature.groups).to be_empty expect(feature.users).to be_empty expect(feature.percentage).to eq(100) end end describe "#clear" do let(:features) { %w(signup beta alpha gm) } before do features.each { |f| @rollout.activate(f) } @rollout.clear! end it "each feature is cleared" do features.each do |feature| expect(@rollout.get(feature).to_hash).to eq( percentage: 0, users: [], groups: [] ) end end it "each feature is cleared with sets" do @options = @rollout.instance_variable_get("@options") @options[:use_sets] = true features.each do |feature| expect(@rollout.get(feature).to_hash).to eq( percentage: 0, users: Set.new, groups: Set.new ) end end it "removes all features" do expect(@rollout.features).to be_empty end end describe "#feature_states" do let(:user_double) { double(id: 7) } before do @rollout.activate(:chat) @rollout.activate_user(:video, user_double) @rollout.deactivate(:vr) end it "returns a hash" do expect(@rollout.feature_states).to be_a(Hash) end context "with user argument" do it "maps active feature as true" do state = @rollout.feature_states(user_double)[:video] expect(state).to eq(true) end it "maps inactive feature as false" do state = @rollout.feature_states[:vr] expect(state).to eq(false) end end context "with no argument" do it "maps active feature as true" do state = @rollout.feature_states[:chat] expect(state).to eq(true) end it "maps inactive feature as false" do state = @rollout.feature_states[:video] expect(state).to eq(false) end end end describe "#active_features" do let(:user_double) { double(id: 19) } before do @rollout.activate(:chat) @rollout.activate_user(:video, user_double) @rollout.deactivate(:vr) end it "returns an array" do expect(@rollout.active_features).to be_a(Array) end context "with user argument" do it "includes active feature" do features = @rollout.active_features(user_double) expect(features).to include(:video) expect(features).to include(:chat) end it "excludes inactive feature" do features = @rollout.active_features(user_double) expect(features).to_not include(:vr) end end context "with no argument" do it "includes active feature" do features = @rollout.active_features expect(features).to include(:chat) end it "excludes inactive feature" do features = @rollout.active_features expect(features).to_not include(:video) end end end describe "#user_in_active_users?" do it "returns true if activated for user" do @rollout.activate_user(:chat, double(id: 5)) expect(@rollout.user_in_active_users?(:chat, "5")).to eq(true) end it "returns false if activated for group" do @rollout.activate_group(:chat, :all) expect(@rollout.user_in_active_users?(:chat, "5")).to eq(false) end end describe "#multi_get" do before do @rollout.activate_percentage(:chat, 10) @rollout.activate_group(:chat, :caretakers) @rollout.activate_group(:videos, :greeters) @rollout.activate(:signup) @rollout.activate_user(:photos, double(id: 42)) end it "returns an array of features" do features = @rollout.multi_get(:chat, :videos, :signup) expect(features[0].name).to eq :chat expect(features[0].groups).to eq [:caretakers] expect(features[0].percentage).to eq 10 expect(features[1].name).to eq :videos expect(features[1].groups).to eq [:greeters] expect(features[2].name).to eq :signup expect(features[2].percentage).to eq 100 expect(features.size).to eq 3 end end describe "#set_feature_data" do before do @rollout.set_feature_data(:chat, description: 'foo', release_date: 'bar') end it 'sets the data attribute on feature' do expect(@rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar') end it 'updates a data attribute' do @rollout.set_feature_data(:chat, description: 'baz') expect(@rollout.get(:chat).data).to include('description' => 'baz', 'release_date' => 'bar') end it 'only sets data on specified feature' do @rollout.set_feature_data(:talk, image_url: 'kittens.png') expect(@rollout.get(:chat).data).not_to include('image_url' => 'kittens.png') expect(@rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar') end it 'does not modify @data if param is nil' do expect(@rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar') @rollout.set_feature_data(:chat, nil) expect(@rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar') end it 'does not modify @data if param is empty string' do expect(@rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar') @rollout.set_feature_data(:chat, " ") expect(@rollout.get(:chat).data).to include('description' => 'foo', 'release_date' => 'bar') end it 'properly parses data when it contains a |' do user = double("User", id: 8) @rollout.activate_user(:chat, user) @rollout.set_feature_data(:chat, "|call||text|" => "a|bunch|of|stuff") expect(@rollout.get(:chat).data).to include("|call||text|" => "a|bunch|of|stuff") expect(@rollout.active?(:chat, user)).to be true end end describe "#clear_feature_data" do it 'resets data to empty string' do @rollout.set_feature_data(:chat, description: 'foo') expect(@rollout.get(:chat).data).to include('description' => 'foo') @rollout.clear_feature_data(:chat) expect(@rollout.get(:chat).data).to eq({}) end end describe 'Check if feature exists' do it 'it should return true if the feature is exist' do @rollout.activate_percentage(:chat, 1) expect(@rollout.exists?(:chat)).to be true end it 'it should return false if the feature is not exist' do expect(@rollout.exists?(:chat)).to be false end end end describe "Rollout::Feature" do describe "#add_user" do it "ids a user using id_user_by" do user = double("User", email: "test@test.com") feature = Rollout::Feature.new(:chat, nil, id_user_by: :email) feature.add_user(user) expect(user).to have_received :email end end describe "#initialize" do describe "when string does not exist" do it 'clears feature attributes when string is not given' do feature = Rollout::Feature.new(:chat) expect(feature.groups).to be_empty expect(feature.users).to be_empty expect(feature.percentage).to eq 0 expect(feature.data).to eq({}) end it 'clears feature attributes when string is nil' do feature = Rollout::Feature.new(:chat, nil) expect(feature.groups).to be_empty expect(feature.users).to be_empty expect(feature.percentage).to eq 0 expect(feature.data).to eq({}) end it 'clears feature attributes when string is empty string' do feature = Rollout::Feature.new(:chat, "") expect(feature.groups).to be_empty expect(feature.users).to be_empty expect(feature.percentage).to eq 0 expect(feature.data).to eq({}) end describe "when there is no data" do it 'sets @data to empty hash' do feature = Rollout::Feature.new(:chat, "0||") expect(feature.data).to eq({}) end it 'sets @data to empty hash' do feature = Rollout::Feature.new(:chat, "||| ") expect(feature.data).to eq({}) end end end end end ruby-rollout-2.4.3/spec/spec_helper.rb000066400000000000000000000006521325327537300177640ustar00rootroot00000000000000$LOAD_PATH.unshift(File.dirname(__FILE__)) $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) require "simplecov" require "rspec" require ENV["USE_REAL_REDIS"] == "true" ? "redis" : "fakeredis" SimpleCov.start do formatter SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::HTMLFormatter, ]) end require "rollout" RSpec.configure do |config| config.before { Redis.new.flushdb } end