[
  {
    "path": ".gitignore",
    "content": "*.gem\n.bundle\nGemfile.lock\npkg/*\n.idea\n.rspec\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: ruby\ncache: bundler\nsudo: false\nrvm:\n  - 1.9.3\n  - 2.1\n  - 2.2\n  - 2.3\n  - 2.4\n  - 2.5\n  - 2.6\n\nbefore_install:\n  - gem install bundler -v 1.17.3\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## 0.12.0 (Nov 26, 2019)\n - Contain fiber-unsafe state of the main Trebuchet class into a state object and optionally store it as a thread/fiber local.\n\n## 0.11.0 (Aug 15, 2019)\n - add Rails 5 compatibility\n\n## 0.10.1 (Jul 13, 2018)\n - add PerDenomination strategy\n\n## 0.10.0 (Nov 17, 2017)\n  - Cache Trebuchet#launch?\n\n## 0.9.18 (Sep 18, 2017)\n  - Do not expose internal format\n\n## 0.9.17 (Aug 15, 2017)\n  - adds CustomRequestAware strategy\n\n## 0.9.16 (Aug 9, 2017)\n  - Don't raise an error if expiration date has not been previously set\n\n## 0.9.15 (Aug 9, 2017)\n  - Return payload for expiration date accessor\n\n## 0.9.14 (Aug 9, 2017)\n  - Expose expiration date accessor\n\n## 0.9.13 (Aug 8, 2017)\n  - Fix a typo\n\n## 0.9.12 (Aug 3, 2017)\n  - Add setter for expiration date\n\n## 0.9.11 (Nov 16, 2016)\n  - minor change: reduce number of times backend is hit in the launch methods\n\n## 0.9.10 (Nov 14, 2016)\n  - fix logical operators for logged out visitors\n\n## 0.9.9 (Sep 15, 2016)\n  - minor change: keep raw option hash in logic strategies for editing purpose\n\n## 0.9.8 (July 27, 2016)\n  - convert deserialized strategy names to symbols\n\n## 0.9.7 (July 20, 2016)\n  - logic operation strategies, e.g. AND, OR, and NOT\n\n## 0.9.6 (April 5, 2016)\n  - add :add_comment method to the Feature class\n\n## 0.9.2 (October 11, 2015)\n  - add :nobody and :everybody strategies\n  - cache features\n  - make :default strategy a singleton\n\n## 0.9.1 (Jul 22, 2015)\n  - fix last_update type in redis_hammerspaced\n\n## 0.9.0 (Jul 22, 2015)\n  - adds update_hammerspace method so we can sync redis into local hammerspace\n\n## 0.8.1 (Jul 21, 2015)\n  - update redis backend to update a sentinel to indicate modifications\n\n## 0.8.0 (Jul 20, 2015)\n  - adds a new backened redis_hammerspaced\n\n## 0.6.3 (Sep 7, 2013)\n  - reimplements the percent strategy\n  - adds per-feature stubbing interface\n\n## 0.0.4 (Dec 6, 2011)\n  - add experiment strategy\n  - fix percentage offset\n  - fix Redis/Memcache dependency issue\n"
  },
  {
    "path": "Gemfile",
    "content": "source \"http://rubygems.org\"\n\n# Specify your gem's dependencies in trebuchet.gemspec\ngemspec\n\ncurrent_ruby = Gem::Version.new(RUBY_VERSION)\nif current_ruby < Gem::Version.new(\"2.2\")\n  gem \"rake\", \"< 12.3\"\nend\n"
  },
  {
    "path": "README.md",
    "content": "Trebuchet\n=========\n\nTrebuchet launches features at people. Wisely choose a strategy, aim, and launch!\n\nInstallation\n------------\n\n\nTrebuchet can be used with Rails or standalone.\n\nTo use with Rails:\ngem 'trebuchet', :require => 'trebuchet_rails'\n\nSetup\n-----\n\n\nTrebuchet defaults to storing data in memory, or can be used with Redis or Memcache as a data store:\n\n    Trebuchet.set_backend :memcached\n    Trebuchet.set_backend :redis, :client => Redis.new(:host => 'example.com')\n    Trebuchet.set_backend :redis_cached, :client => Redis.new(:host => 'example.com')\n\nA Rails initializer is a great spot for this. You may want to use a few other settings, either hardcoded values or procs (eval'd in the context of the controller):\n\n    Trebuchet.admin_view = proc { current_user.try(:admin?) } # /trebuchet admin interface access\n    Trebuchet.time_zone = proc { current_user.time_zone } # or just \"Mountain Time (US & Canada)\"\n\n\nAim\n---\n\nTrebuchet can be aimed while your application is running. The syntax is:\n\n    Trebuchet.aim('awesome_feature', :percent, 1)\n\nWhich will launch 'awesome_feature' to 1% of users.\n\nAnother builtin strategy allows launching to particular user IDs:\n\n    Trebuchet.aim('awesome_feature', :users, [23, 42])\n\nYou can also combine multiple strategies, in which case the feature is launched if any of them is true:\n\n    Trebuchet.feature('awesome_feature').aim(:percent, 1).aim(:users, [23, 42])\n\nIf you don't aim Trebuchet for a feature, the default action is not to launch it to anyone.\n\n\nLaunch\n------\n\nIn a view, do this:\n\n    <% trebuchet.launch('time_machine') do %>\n        <p>Welcome to the future!</p>\n    <% end %>\n\nThe code between do .. end will only run if the strategy for 'time_machine' allows launching to current_user.\n\nYou can also use it in a controller:\n\n    def index\n        trebuchet.launch('time_machine') do\n            @time_machine = TimeMachine.new\n        end\n    end\n\n\nCustom Strategies\n-----------------\n\nTrebuchet ships with a number of default strategies but you can also define your own custom strategies like so:\n\n    Trebuchet.define_strategy(:admins) do |user|\n        !!(user && user.has_role?(:admin))\n    end\n\ncontroller.current_user is yielded to the block and it should return true for users you want to launch to.\nYou can use parameters with custom strategies too:\n\n    Trebuchet.define_strategy(:markets) do |user, markets|\n        markets.include?(user.market)\n    end\n\nLike parameters for builtin strategies, these can be changed while the application is running. For example:\n\n    Trebuchet.aim('time_machine', :markets, ['San Francisco', 'New York City'])\n\nWhen using Trebuchet together with Rails, a good place to define custom strategies is in an initializer.\n\n\nVisitor Strategy\n----------------\n\nTrebuchet can be used to launch to visitors (no user object present).\nFirst, set the visitor id either directly (in a before filter) or as a proc:\n\n    Trebuchet.visitor_id = 123\n\n    Trebuchet.visitor_id = proc { |request| request && request.cookies[:visitor] && request.cookies[:visitor].hash }\n\nIf you're using a proc, Trebuchet passes in the request object. It expects that the proc returns an integer.\nIf it returns anything else, Trebuchet will not launch.\n\nFiber and Thread Safety\n-------------\n\nTrebuchet stores global state such as `Trebuchet.current` which is thread and fiber unsafe behavior. In order to use these\nfeatures in a fiber or threaded environment, `Trebuchet.threadsafe_state = true` will cause Trebuchet to store these values\nin a thread-local state object instead. This is not the default for backward compatability reasons.\n"
  },
  {
    "path": "Rakefile",
    "content": "require 'bundler'\nBundler::GemHelper.install_tasks\n\ntask :spec do\n  system 'rspec ./spec'\nend\n\ntask :default => :spec"
  },
  {
    "path": "app/controllers/trebuchet_rails/features_controller.rb",
    "content": "module TrebuchetRails\n\n  class FeaturesController < ApplicationController\n\n    if Rails::VERSION::MAJOR >= 5\n      before_action :control_access, :get_time_zone\n    else\n      before_filter :control_access, :get_time_zone\n    end\n\n    layout 'trebuchet'\n    helper :trebuchet\n\n    def index\n      @features = Trebuchet::Feature.all\n      @features.sort! {|x,y| x.name.downcase <=> y.name.downcase }\n      \n      @dismantled_features = Trebuchet::Feature.dismantled\n      @dismantled_features.sort! {|x,y| x.name.downcase <=> y.name.downcase }\n      \n      respond_to do |wants|\n        wants.html # index.html.erb\n        wants.json { render :json => @features.map(&:export) }\n      end\n    end\n    \n    def timeline\n      @history = []\n      Trebuchet::Feature.all.each do |f|\n        f.history.each do |timestamp, strategy|\n          @history << { \n            :feature_name => f.name, \n            :timestamp => timestamp, \n            :strategy => strategy\n          }\n        end\n      end\n      @history = @history.sort_by { |h| h[:timestamp] }\n      @history.reverse!\n      respond_to do |wants|\n        wants.html # index.html.erb\n        wants.json do\n           json_history = @history.map do |history|\n            history.tap { |h| h[:strategy] = h[:strategy].export }\n          end\n          render :json => json_history\n        end\n      end\n    end\n    \n    private\n    def control_access\n      allowed = if Trebuchet.admin_view.is_a?(Proc)\n        begin\n          instance_eval &(Trebuchet.admin_view)\n        rescue\n          false\n        end\n      else\n        !!Trebuchet.admin_view\n      end\n      raise ActionController::RoutingError.new('Not Found') unless allowed\n    end\n    \n    def get_time_zone\n      @zone = if Trebuchet.time_zone\n        if Trebuchet.time_zone.is_a?(Proc)\n          Trebuchet.time_zone.call\n        elsif Trebuchet.time_zone.is_a?(String)\n          Trebuchet.time_zone\n        else\n          nil\n        end\n      end\n      @zone = ActiveSupport::TimeZone.new(@zone || 'UTC')\n    end\n    \n  end\n\nend\n"
  },
  {
    "path": "app/helpers/trebuchet_helper.rb",
    "content": "module TrebuchetHelper\n\n  def feature(feature)\n    \"<dd>#{feature.name}</dd>\n    <dt><ul>#{strategy feature.strategy}</ul></dt>\"\n  end\n  \n  def strategy(strategy)\n    html =  case strategy.name\n            when :multiple\n              strategy.strategies.map { |s| strategy s }.join().html_safe\n            else\n              strategy.to_s\n            end\n    strategy.name == :multiple ? html : content_tag(:li, html)\n  end\n  \n  def trebuchet_css\n    filename = File.expand_path(File.dirname(__FILE__) + \"/../views/trebuchet_rails/trebuchet.css\")\n    return IO.read(filename)\n  end\n  \nend"
  },
  {
    "path": "app/views/layouts/trebuchet.html.erb",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n  \t<title>Trebuchet</title>\n  \t<style type=\"text/css\">\n  \t\t<%= trebuchet_css %>\n  \t</style>\n  </head>\n\n  <body>\n    <%= yield %>\n  </body>\n</html>\n"
  },
  {
    "path": "app/views/trebuchet_rails/features/index.html.erb",
    "content": "<div id=\"trebuchet\">\n\t<h1>Trebuchet Admin</h1>\n\t<table class=\"treb-features\">\n\t\t<thead>\n\t\t\t<tr>\n\t\t\t\t<th>Feature Name</th>\n\t\t\t\t<th>Launch Strategy</th>\n\t\t\t\t<th>Strategy History</th>\n\t\t\t</tr>\n\t\t</thead>\n\t\t<tbody>\n\t\t\t<% @features.each do |feature| %>\n\t\t\t\t<%- has_history = feature.history.length > 0 %>\n\t\t\t\t<tr>\n\t\t\t\t\t<td><%= feature.name %></td>\n\t\t\t\t\t<td><%= strategy feature.strategy %></td>\n\t\t\t\t\t<% if has_history %>\n\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t<a class=\"history-toggle\" href=\"#\">History</a>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t<% end %>\n\t\t\t\t</tr>\n\t\t\t\t<% if has_history %>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"unstyled collapsed treb-features-history-wrapper\" colspan=\"2\">\n\t\t\t\t\t\t<table class=\"treb-features treb-features-history well\">\n\t\t\t\t\t\t\t<% feature.history.each do |time, strategy| %>\n\t\t\t\t\t\t\t  <tr><td><%= @zone.at(time.to_i).strftime(\"%x %X %Z\") %><br /><%= strategy strategy %></td></tr>\n\t\t\t\t\t\t\t<% end %>\n\t\t\t\t\t\t</table>\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<% end %>\n\t\t\t<% end %>\n\t\t</tbody>\n\t</table>\n\t<br />\n\t<br />\n\t<table class=\"treb-features\">\n\t\t<thead>\n\t\t\t<tr>\n\t\t\t\t<th>Dismantled Features</th>\n\t\t\t\t<th>Strategy History</th>\n\t\t\t</tr>\n\t\t</thead>\n\t\t<tbody>\n\t\t\t<% @dismantled_features.each do |feature| %>\n\t\t\t  <%- has_history = feature.history.length > 0 %>\n\t\t\t\t<tr>\n\t\t\t\t\t<td style=\"text-decoration: line-through\"><%= feature.name %></td>\n\t\t\t\t\t<% if has_history %>\n\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t<a class=\"history-toggle\" href=\"#\">History</a>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t<% end %>\n\t\t\t\t</tr>\n\t\t\t\t<% if has_history %>\n\t\t\t\t<tr>\n\t\t\t\t\t<td class=\"unstyled collapsed treb-features-history-wrapper\" colspan=\"1\">\n\t\t\t\t\t\t<table class=\"treb-features treb-features-history well\">\n\t\t\t\t\t\t\t<% feature.history.each do |time, strategy| %>\n\t\t\t\t\t\t\t  <tr><td><%= @zone.at(time.to_i).strftime(\"%x %X %Z\") %><br /><%= strategy strategy %></td></tr>\n\t\t\t\t\t\t\t<% end %>\n\t\t\t\t\t\t</table>\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t\t<% end %>\n\t\t\t<% end %>\n\t\t</tbody>\n\t</table>\n</div>\n\n<script src=\"//ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js\"></script>\n<script>\n\t(function(undefined) {\n\t\tvar trebFeatures = $('.treb-features');\n\t\ttrebFeatures.on('click', '.history-toggle', function(e) {\n\t\t\tvar $target = $(e.target);\n\n\t\t\t$target.parents('tr').next().\n\t\t\t\tfind('.treb-features-history-wrapper').toggleClass('collapsed');\n\t\t\te.preventDefault();\n\t\t});\n\t})();\n</script>\n"
  },
  {
    "path": "app/views/trebuchet_rails/features/timeline.html.erb",
    "content": "<div id=\"trebuchet\">\n\t<h1>Trebuchet Timeline</h1>\n\t<table class=\"treb-features\">\n\t\t<thead>\n\t\t\t<tr>\n\t\t\t  <th style=\"min-width: 15em\">Timestamp</th>\n\t\t\t\t<th>Feature Name</th>\n\t\t\t\t<th>Launch Strategy</th>\n\t\t\t</tr>\n\t\t</thead>\n\t\t<tbody>\n\t\t\t<% @history.each do |h| %>\n\t\t\t\t<tr>\n\t\t\t\t  <td><%= @zone.at(h[:timestamp].to_i).strftime(\"%x %X %Z\") %></td>\n\t\t\t\t\t<td><%= h[:feature_name] %></td>\n\t\t\t\t\t<td><%= strategy h[:strategy] %></td>\n\t\t\t\t</tr>\n\t\t\t<% end %>\n\t\t</tbody>\n\t</table>\n</div>"
  },
  {
    "path": "app/views/trebuchet_rails/trebuchet.css",
    "content": "#trebuchet {\n\tfont-family: HelveticaNeue, 'Helvetica Neue', HelveticaNeueRoman, HelveticaNeue-Roman, 'Helvetica Neue Roman', TeXGyreHerosRegular, Helvetica, Tahoma, Geneva, Arial, sans-serif;\n}\n\n.well {\n\t-webkit-box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.2), 0 0 0 black;\n\tbox-shadow: inset 0 0 15px rgba(0, 0, 0, 0.2), 0 0 0 black;\n}\n\n.collapsed {\n\tdisplay: none;\n}\n\n.treb-features {\n\tborder-collapse: collapse;\n\tborder: 1px solid #c9c9c9;\n\tborder-top-color: #000000;\n}\n\n.treb-features-history {\n\tborder: none;\n\tfont-size: 0.8em;\n\twidth: 100%;\n}\n\n.treb-features thead {\n\tbackground: -webkit-gradient(linear, center top, center bottom, from(#575a5b), to(#393c3d));\n\tbackground: -moz-linear-gradient(top, #575a5b, #393c3d);\n\tcolor: #ffffff;\n\ttext-shadow: -1px -1px 1px black;\n}\n\n.treb-features td,\n.treb-features th {\n\tborder: 1px solid #ccc;\n\tpadding: 1em 1.5em;\n\ttext-align: left;\n}\n\n.treb-features-history tr:first-child td {\n\tborder-top: none;\n}\n\n.treb-features-history tr:last-child td {\n\tborder-bottom: none;\n}\n\n.treb-features td {\n\tbackground-color: #f5f5f5;\n}\n\n.treb-features-history td {\n\tbackground: transparent;\n\tborder-left: none;\n\tborder-right: none;\n}\n\n.treb-features th {\n\tborder-left-color: #525657;\n\tborder-right-color: #74787a;\n\tfont-weight: normal;\n}\n\n.treb-features .unstyled {\n\tpadding: 0;\n}\n"
  },
  {
    "path": "config/routes.rb",
    "content": "# TrebuchetRails::Engine.routes do\n\nroutes_block = lambda do\n  scope \"trebuchet\", :module => \"trebuchet_rails\" do\n    get '/' => \"features#index\"\n    get 'timeline' => \"features#timeline\"\n  end\nend\n\nif Rails::VERSION::MAJOR == 3\n  case Rails::VERSION::MINOR\n  when 0\n    Rails.application.routes.draw &routes_block\n  when 1\n    Rails.application.routes.prepend &routes_block\n  when 2\n    Rails.application.routes &routes_block\n  end\nend\n"
  },
  {
    "path": "init.rb",
    "content": "require 'trebuchet'\n\n# FIXME: happens too early\n# def set_trebuchet_namespace(app_name)\n#   if Trebuchet.backend.respond_to?(:namespace=)\n#     Trebuchet.backend.namespace = \"trebuchet-#{app_name}/\"\n#   end\n# end\n\nif defined? Rails\n  Trebuchet.use_with_rails!\n  \n  if Rails.respond_to?(:version) && Rails.version =~ /^3/\n    # Rails 3.x\n    # no other setup needed  \n  else\n    # Rails 2.x\n    load_paths.each do |path|\n       ActiveSupport::Dependencies.load_once_paths.delete(path)\n    end if config.environment == 'development'\n    # set_trebuchet_namespace \"#{Rails.root.basename}-#{Rails.env}\" \n  end\nend"
  },
  {
    "path": "lib/trebuchet/action_controller.rb",
    "content": "require 'trebuchet/action_controller_filter'\n\nmodule Trebuchet::ActionController\n\n  def self.included(base)\n    base.helper_method :trebuchet\n    base.class_eval do\n      around_filter Trebuchet::ActionControllerFilter\n    end\n  end\n\n  def trebuchet\n    Trebuchet.current\n  end\n\nend\n"
  },
  {
    "path": "lib/trebuchet/action_controller_filter.rb",
    "content": "class Trebuchet::ActionControllerFilter\n\n  def self.before(controller)\n    Trebuchet.initialize_logs\n\n    if Trebuchet.backend.respond_to?(:refresh)\n      Trebuchet.backend.refresh\n    end\n\n    Trebuchet.current_block = Proc.new {\n      Trebuchet.new(controller.send(:current_user), controller.request)\n    }\n  end\n\n  def self.after(controller)\n    Trebuchet.current_block = nil\n    Trebuchet.reset_current! # very important\n  end\n\nend\n"
  },
  {
    "path": "lib/trebuchet/backend/disabled.rb",
    "content": "# This backend stores nothing and returns empty/false data (launch? will always be false)\n# It can be used to disable all Trebuchet features (especially if Trebuchet fails to connect to it's normal data store)\n\nclass Trebuchet::Backend::Disabled\n\n  def initialize(*args)\n  end\n\n  def get_strategy(feature_name)\n    [:default]\n  end\n\n  def set_strategy(feature, strategy, options = nil)\n    false\n  end\n\n  def append_strategy(feature, strategy, options = nil)\n    false\n  end\n  \n  def get_feature_names\n    []\n  end\n\nend\n"
  },
  {
    "path": "lib/trebuchet/backend/memcached.rb",
    "content": "require 'memcache'\n\nclass Trebuchet::Backend::Memcached\n\n  attr_accessor :namespace\n\n  def initialize(*args)\n    @memcache = MemCache.new(*args)\n    @namespace = 'trebuchet/'\n  end\n\n  def get_strategy(feature_name)\n    @memcache.get(key(feature_name))\n  end\n\n  def set_strategy(feature, strategy, options = nil)\n    @memcache.set(key(feature), [strategy, options])\n  end\n\n  def append_strategy(feature, strategy, options = nil)\n    @memcache.set(key(feature), get_strategy(feature) + [strategy, options])\n  end\n  \n  def get_feature_names\n    [] # TODO: store all key names\n  end\n\n  private\n\n  def key(feature_name)\n    \"#{namespace}#{feature_name}\"\n  end\n\nend\n"
  },
  {
    "path": "lib/trebuchet/backend/memory.rb",
    "content": "class Trebuchet::Backend::Memory\n\n  def initialize(*args)\n    @hash = {}\n    @archived = []\n  end\n\n  def get_strategy(feature_name)\n    @hash.fetch(feature_name, nil) || []\n  end\n\n  def set_strategy(feature, strategy, options = nil)\n    @archived.delete(feature)\n    @hash.store(feature, [strategy, options])\n  end\n\n  def append_strategy(feature, strategy, options = nil)\n    @archived.delete(feature)\n    strategies = get_strategy(feature) || []\n    if i = strategies.index(strategy)\n      strategies.delete_at(i) # remove strategy_name\n      strategies.delete_at(i) # remove options\n    end\n    strategies += [strategy, options]\n    @hash.store(feature, strategies)\n  end\n  \n  def remove_strategy(feature)\n    @hash.delete(feature)\n  end\n  \n  def get_feature_names\n    @hash.keys\n  end\n  \n  def remove_feature(feature)\n    @hash.delete(feature)\n    @archived << feature\n    @archived.uniq!\n  end\n  \n  def get_archived_feature_names\n    @archived\n  end\n\nend\n"
  },
  {
    "path": "lib/trebuchet/backend/redis.rb",
    "content": "require 'redis' unless defined?(Redis)\nrequire 'json'\n\nclass Trebuchet::Backend::Redis\n\n  attr_accessor :namespace\n\n  def initialize(*args)\n    @namespace = 'trebuchet/'\n    begin\n      if args.first.is_a?(Hash) && (client = args.first[:client]) && (client.is_a?(Redis) || client.is_a?(MockRedis))\n        # ignore other args and use provided Redis connection\n        @options = args.first\n        @redis = args.first[:client]\n      else\n        @redis = Redis.new(*args)\n      end\n      unless @options && @options[:skip_check]\n        # raise error if not connectedUncaught ReferenceError: google is not defined\n        @redis.exists(feature_names_key) # @redis.info is slow and @redis.client.connected? is NOT reliable\n      end\n    rescue Exception => e\n      raise Trebuchet::BackendInitializationError, e.message\n    end\n  end\n\n  def get_strategy(feature_name)\n    return nil unless h = @redis.hgetall(feature_key(feature_name))\n    unpack_strategy(h)\n  end\n\n  def unpack_strategy(options)\n    return nil unless options.is_a?(Hash)\n    [].tap do |a|\n      options.each do |k, v|\n        begin\n          key = k.to_sym\n          value = JSON.load(v).first # unpack from array\n          a << key\n          a << value\n        rescue\n          # if it can't parse the JSON, skip it\n        end\n      end\n    end\n  end\n\n  def set_strategy(feature_name, strategy, options = nil)\n    remove_strategy(feature_name)\n    append_strategy(feature_name, strategy, options)\n    update_sentinel\n  end\n\n  def append_strategy(feature_name, strategy, options = nil)\n    @redis.srem(archived_feature_names_key, feature_name)\n    @redis.hset(feature_key(feature_name), strategy, [options].to_json) # have to put options in container for json\n    @redis.sadd(feature_names_key, feature_name)\n    store_history(feature_name)\n    update_sentinel\n  end\n\n  def remove_strategy(feature_name)\n    @redis.del(feature_key(feature_name))\n    update_sentinel\n  end\n\n  def get_feature_names\n    @redis.smembers(feature_names_key)\n  end\n\n  def get_archived_feature_names\n    @redis.smembers(archived_feature_names_key)\n  end\n\n  def remove_feature(feature_name)\n    @redis.del(feature_key(feature_name))\n    @redis.srem(feature_names_key, feature_name)\n    @redis.sadd(archived_feature_names_key, feature_name)\n    update_sentinel\n  end\n\n  def store_history(feature_name)\n    timestamp = Time.now.to_i\n    h = @redis.hgetall(feature_key(feature_name))\n    @redis.hmset(feature_history_key(feature_name, timestamp), *h.to_a.flatten)\n    @redis.sadd(feature_history_key(feature_name), timestamp) # subtle\n  end\n\n  def get_history(feature_name)\n    [].tap do |history|\n      @redis.smembers(feature_history_key(feature_name)).sort.each do |timestamp|\n        h = @redis.hgetall(feature_history_key(feature_name, timestamp))\n        history << [timestamp.to_i, unpack_strategy(h)]\n      end\n    end\n  end\n\n  def get_all_history(include_archived = false)\n    history = []\n\n    features = @redis.smembers(feature_names_key)\n    features += @redis.smembers(archived_feature_names_key) if include_archived\n\n    result = @redis.pipelined do\n      features.each do |feature_name|\n        @redis.smembers(feature_history_key(feature_name))\n      end\n    end\n\n    features.zip(result).each do |feature_name, timestamps|\n      timestamps.each do |timestamp|\n        history << [timestamp.to_i, feature_name]\n      end\n    end\n\n    # sort in reverse timestamp order\n    history.sort! { |x,y| y.first <=> x.first }\n  end\n\n  def update_sentinel\n    @redis.set(sentinel_key, Time.now.to_i)\n  end\n\n  def get_sentinel\n    @redis.get(sentinel_key) || Time.now.to_i\n  end\n\n  private\n\n  def archived_feature_names_key\n    \"#{namespace}archived-feature-names\"\n  end\n\n  def feature_names_key\n    \"#{namespace}feature-names\"\n  end\n\n  def feature_key(feature_name)\n    \"#{namespace}features/#{feature_name}\"\n  end\n\n  def feature_history_key(feature_name, timestamp = nil)\n    key = \"#{namespace}feature-history/#{feature_name}\"\n    key = \"#{key}/#{timestamp}\" if timestamp\n    key\n  end\n\n  def sentinel_key\n    \"#{namespace}last_updated\"\n  end\n\nend\n"
  },
  {
    "path": "lib/trebuchet/backend/redis_cached.rb",
    "content": "require 'trebuchet/backend/redis'\n\nclass Trebuchet::Backend::RedisCached < Trebuchet::Backend::Redis\n\n  # cache strategies in memory until clear_cached_strategies is called\n\n  def get_strategy(feature_name)\n    if cached_strategies.has_key?(feature_name)\n      # use cached if available (even if value is nil)\n      cached_strategies[feature_name]\n    else\n      # or call Trebuchet::Backend::Redis#get_strategy\n      # which will fetch from Redis and unpack json\n      # and then cache it for next time\n      cache_strategy feature_name, super(feature_name)\n    end\n  end\n\n  def append_strategy(feature_name, strategy, options = nil)\n    # though we can't clear the strategy for all active instances\n    # this will clear the cache in the console environment to show current settings\n    clear_cached_strategies\n    super(feature_name, strategy, options)\n  end\n\n  def cache_strategy(feature_name, strategy)\n    cached_strategies[feature_name] = strategy\n  end\n\n  def cached_strategies\n    @cached_strategies ||= Hash.new\n  end\n\n  def cache_cleared_at\n    @cache_cleared_at ||= Time.now\n  end\n\n  def clear_cached_strategies\n    @cache_cleared_at = Time.now\n    @cached_strategies = nil\n  end\n\n  def refresh\n    clear_cached_strategies if Time.now > cache_cleared_at + 60.seconds\n  end\n\nend\n"
  },
  {
    "path": "lib/trebuchet/backend/redis_hammerspaced.rb",
    "content": "require 'trebuchet/backend/redis'\nrequire 'json'\n\nclass Trebuchet::Backend::RedisHammerspaced < Trebuchet::Backend::Redis\n\n  # This class will rely on a cron job to sync all trebuchet features\n  # to local hammerspace thus this class never directly updates hammerspace\n  # We also cache in memory the features and rely on before_filter\n  # to lazily invalidate local cache\n\n  attr_accessor :namespace\n\n  def initialize(*args)\n    # args.first must be a hash\n    super(*args)\n    @hammerspace = args.first[:hammerspace]\n  end\n\n  def get_strategy(feature_name)\n    if cached_strategies.has_key?(feature_name)\n      # use cached if available (even if value is nil)\n      cached_strategies[feature_name]\n    else\n      # call to hammerspace\n      cache_strategy feature_name, get_strategy_hammerspace(feature_name)\n    end\n  end\n\n  def get_strategy_hammerspace(feature_name)\n    # Read from hammerspace\n    h = @hammerspace[feature_key(feature_name)]\n    return nil unless h\n    # h will be a string, we need to convert it back to Hash\n    begin\n      h = JSON.load(h)\n    rescue\n      return nil\n    end\n    unpack_strategy_hammerspace(h)\n  end\n\n  def unpack_strategy_hammerspace(options)\n    # We don't need to further convert values\n    # because it's already taken care of\n    # by the refresh cron job\n    # assumption here is that v will be an array and we\n    # are using the first element for now\n    # This makes the format compatible with redis backend\n    return nil unless options.is_a?(Hash)\n    [].tap do |a|\n      options.each do |k, v|\n        key = k.to_sym\n        a << key\n        a << v.first\n      end\n    end\n  end\n\n  def get_feature_names\n    # Read from hammerspace\n    return [] unless @hammerspace.has_key?(feature_names_key)\n    JSON.load(@hammerspace[feature_names_key])\n  end\n\n  def append_strategy(feature_name, strategy, options = nil)\n    # though we can't clear the strategy for all active instances\n    # this will clear the cache in the console environment to show current settings\n    clear_cached_strategies\n    super(feature_name, strategy, options)\n  end\n\n  def cache_strategy(feature_name, strategy)\n    cached_strategies[feature_name] = strategy\n  end\n\n  def cached_strategies\n    @cached_strategies ||= Hash.new\n  end\n\n  def clear_cached_strategies\n    @cached_strategies = nil\n  end\n\n  def refresh\n    # We close and reopen hammerspace to see if we need to invalidate local cache\n    uid = @hammerspace.uid\n    @hammerspace.close\n    if @hammerspace.uid != uid\n      clear_cached_strategies\n    end\n  end\n\n  def update_hammerspace(forced = false)\n    last_updated = get_sentinel\n\n    return if !forced && last_updated == @hammerspace[sentinel_key]\n\n    feature_names = @redis.smembers(feature_names_key)\n\n    features = @redis.pipelined do\n      feature_names.each do |feature_name|\n        @redis.hgetall(feature_key(feature_name))\n      end\n    end\n\n    hash = generate_hammerspace_hash(feature_names, features, last_updated)\n\n    @hammerspace.replace(hash)\n    @hammerspace.close\n    clear_cached_strategies\n  end\n\n  # feature_names is an array of strings\n  # features is an array of strategies\n  # Each strategy is of form [\"<key1>\", \"<value1>\", \"<key2>\", \"<value2>\"...]\n  # We need to decode the values because they are in string form (not actual hash)\n  def generate_hammerspace_hash(feature_names, features, last_updated)\n    hash = {\n      sentinel_key => last_updated.to_s,\n      feature_names_key => feature_names.to_json,\n    }\n\n    feature_names.zip(features) do |feature_name, feature|\n      h = {}\n      feature.each_slice(2) {|k,v| h[k]=JSON.load(v)}\n      hash[feature_key(feature_name)] = h.to_json\n    end\n    hash\n  end\n\nend\n"
  },
  {
    "path": "lib/trebuchet/backend.rb",
    "content": "module Trebuchet::Backend\n\n  def self.lookup(name)\n    # From ActiveSupport::Inflector.camelize\n    const_name = name.to_s.gsub(/\\/(.?)/) { \"::#{$1.upcase}\" }.gsub(/(?:^|_)(.)/) { $1.upcase }\n\n    if const_defined?(const_name)\n      const_get(const_name)\n    else\n      raise ArgumentError.new(\"Unknown backend type #{name}\")\n    end\n  end\n\nend\n"
  },
  {
    "path": "lib/trebuchet/error.rb",
    "content": "class Trebuchet::Error < StandardError ; end\nclass Trebuchet::BackendInitializationError < Trebuchet::Error ; end\nclass Trebuchet::BackendError < Trebuchet::Error ; end"
  },
  {
    "path": "lib/trebuchet/feature/stubbing.rb",
    "content": "class Trebuchet\n  class Feature\n    module Stubbing\n\n      def stub(state)\n        self.class.stubbed_features[name] = state\n      end\n\n      def stubbed?\n        !!self.class.stubbed_features[name]\n      end\n\n      def self.included(base)\n        base.extend(ClassMethods)\n      end\n\n      module ClassMethods\n\n        def dismantle_stubs\n          @stubbed_features = nil\n        end\n\n        def stubbed_features\n          @stubbed_features ||= {}\n        end\n\n      end\n\n    end\n  end\nend"
  },
  {
    "path": "lib/trebuchet/feature.rb",
    "content": "require 'trebuchet/feature/stubbing'\n\nclass Trebuchet::Feature\n  include Stubbing\n\n  @@deprecated_strategies_enabled = true\n  @@features = {}\n\n  attr_accessor :name\n\n  def initialize(name)\n    @name = name\n  end\n\n  def self.find(name)\n    feature = @@features[name]\n    if !feature\n      feature = new(name)\n      @@features[name] = feature\n    end\n\n    feature.reset\n\n    feature\n  end\n\n  def reset\n    @chained = false\n  end\n\n  def self.all\n    Trebuchet.backend.get_feature_names.map{|name| new(name)}\n  end\n\n  def self.dismantled\n    Trebuchet.backend.get_archived_feature_names.map{|name| new(name)}\n  end\n\n  def self.exist?(name)\n    !!all.detect{|feature| feature.name == name }\n  end\n\n  # Runs the block with deprecated features enabled so that various methods\n  # do not raise exceptions. This was added to allow specs to test\n  # deprecated features. Not thread safe.\n  def self.with_deprecated_strategies_enabled(value=true, &block)\n    original_value = @@deprecated_strategies_enabled\n    begin\n      @@deprecated_strategies_enabled = value\n      block.call()\n    ensure\n      @@deprecated_strategies_enabled = original_value\n    end\n  end\n\n  def strategy\n    Trebuchet::Strategy.for_feature(self)\n  end\n\n  def valid?\n    strategy.name != :invalid\n  end\n\n  def launch_at?(user, request = nil)\n    # Store strategy so that only one call to the backend is needed.\n    s = strategy\n    (!s.needs_user? || !user.nil?) && s.launch_at?(user, request)\n  end\n\n  def aim(strategy_name, options = nil)\n    if !@@deprecated_strategies_enabled &&\n       Trebuchet::Strategy.deprecated_strategy_names.include?(strategy_name)\n      raise \"The #{strategy_name} strategy is deprecated.\"\n    end\n    if chained?\n      Trebuchet.backend.append_strategy(self.name, strategy_name, options)\n    else\n      Trebuchet.backend.set_strategy(self.name, strategy_name, options)\n    end\n    @chained = true\n    self\n  end\n\n  # add/edit just one strategy without affecting other chained strategies\n  def adjust(strategy_name, options = nil)\n    Trebuchet.backend.append_strategy(self.name, strategy_name, options)\n    self\n  end\n\n  # add to the options of a strategy (if it is an integer, hash or array)\n  def augment(strategy_name, new_options)\n    # get old options if any\n    strategy_array = Trebuchet.backend.get_strategy(self.name)\n    i = strategy_array.index(strategy_name)\n    old_options = i ? strategy_array[i+1] : nil\n    # augment them carefully\n    options = if old_options == nil\n      new_options\n    elsif old_options.is_a?(Array) && new_options.is_a?(Array)\n      old_options + new_options\n    elsif old_options.is_a?(Hash) && new_options.is_a?(Hash)\n      old_options.merge(new_options)\n    elsif old_options.is_a?(Numeric) && new_options.is_a?(Numeric)\n      old_options + new_options\n    else # otherwise, change nothing\n      old_options\n    end\n    # adjust that strategy\n    self.adjust(strategy_name, options)\n  end\n\n  def dismantle\n    Trebuchet.backend.remove_feature(self.name)\n  end\n\n  # add comments for a feature, as a place to hold change logs for example, to supported backends\n  def add_comment(comment)\n    if Trebuchet.backend.respond_to?(:add_comment)\n      Trebuchet.backend.add_comment(self.name, comment)\n    end\n  end\n\n  # Retrieve the expiration date of the feature.\n  # Return nil if the feature does not have an expiration date.\n  def expiration_date\n    return unless Trebuchet.backend.respond_to?(:expiration_date)\n    Trebuchet.backend.expiration_date(self.name)\n  end\n\n  # Set the expiration date of the feature.\n  def set_expiration_date(expiration_date)\n    return unless Trebuchet.backend.respond_to?(:set_expiration_date)\n    Trebuchet.backend.set_expiration_date(self.name, expiration_date)\n  end\n\n  def history\n    return [] unless Trebuchet.backend.respond_to?(:get_history)\n    Trebuchet.backend.get_history(self.name).map do |row|\n      [Time.at(row.first), Trebuchet::Strategy.find(*row.last)]\n    end\n  end\n\n  def feature_id\n    begin\n      @feature_id ||= Trebuchet::SHA1.hexdigest(@name).to_i(16)\n    rescue\n      return 0\n    end\n  end\n\n  def as_json(options = {})\n    {:name => @name, :strategy => strategy.export}\n  end\n\n  def to_s\n    str = \"name: \\\"#{@name}\\\", \"\n    str << \"#{strategy.name == :multiple ? 'strategies' : 'strategy'}: #{strategy}\"\n  end\n\n  def inspect\n    \"#<#{self.class.name} #{self}>\"\n  end\n\n  def export\n    {:feature_name => name, :strategy => strategy.export}\n  end\n\n  private\n\n  def chained?\n    @chained\n  end\n\nend\n"
  },
  {
    "path": "lib/trebuchet/state.rb",
    "content": "# Represents the internal, global and thread-unsafe state of Trebuchet\nTrebuchet::State = Struct.new(\n  :visitor_id, :current, :current_block,\n  :logs, :admin_view, :admin_edit, :time_zone,\n  :author\n)\n"
  },
  {
    "path": "lib/trebuchet/strategy/base.rb",
    "content": "require 'digest/sha1'\n\nclass Trebuchet::Strategy::Base\n\n  attr_accessor :feature\n\n  def name\n    self.class.strategy_name\n  end\n\n  def feature_id\n    feature.feature_id\n  end\n\n  def needs_user?\n    true\n  end\n\n  def self.strategy_name\n    Trebuchet::Strategy.name_for_class(self)\n  end\n\n  def as_json(options = {})\n    excluded = [:feature, :block]\n    {:name => name}.tap do |h|\n      instance_variables.map do |v|\n        key = v.to_s.gsub('@','').to_sym\n        h[key] = instance_variable_get(v) unless excluded.include?(key)\n      end\n    end\n  end\n\n  def inspect\n    self.to_s\n  end\n\n  def export(options = nil)\n    {:name => self.name, :options => options}\n  end\n\nend\n"
  },
  {
    "path": "lib/trebuchet/strategy/custom.rb",
    "content": "class Trebuchet::Strategy::Custom < Trebuchet::Strategy::Base\n\n  attr_reader :options, :custom_name\n\n  @@custom_strategies = {}\n\n  def initialize(name, options = nil)\n    @custom_name = name\n    @options = options\n    @block = @@custom_strategies[name]\n  end\n\n  def launch_at?(user, request = nil)\n    !!(options ? @block.call(user, options) : @block.call(user))\n  end\n\n  def self.define(name, block)\n    @@custom_strategies[name] = block\n  end\n\n  def self.exists?(name)\n    @@custom_strategies.has_key?(name)\n  end\n\n  def needs_user?\n    false\n    # re-enable after adding  { |options, user, request| }\n    # if block = @@custom_strategies[custom_name]\n    #   block.arity > 0\n    # else\n    #   false\n    # end\n  end\n\n  def as_json(options = {})\n    {:custom_name => @custom_name, :options => @options}\n  end\n\n  def to_s\n    \"#{custom_name} (custom) #{options.inspect if options}\"\n  end\n\n  def export\n    super as_json\n  end\n\nend\n"
  },
  {
    "path": "lib/trebuchet/strategy/custom_request_aware.rb",
    "content": "class Trebuchet::Strategy::CustomRequestAware < Trebuchet::Strategy::Custom\n  @@custom_request_aware_strategies = {}\n\n  def initialize(name, options = nil)\n    @custom_name = name\n    @options = options\n    @block = @@custom_request_aware_strategies[name]\n  end\n\n  def self.define(name, block)\n    @@custom_request_aware_strategies[name] = block\n  end\n\n  def self.exists?(name)\n    @@custom_request_aware_strategies.has_key?(name)\n  end\n\n  def launch_at?(user, request = nil)\n    request ||= {}\n    !!(options ? @block.call(user, request, options) : @block.call(user, request))\n  end\n\n  def to_s\n    \"#{custom_name} (custom_request_aware) #{options.inspect if options}\"\n  end\nend\n"
  },
  {
    "path": "lib/trebuchet/strategy/default.rb",
    "content": "require 'singleton'\n\n# Default is to not launch the feature to anyone\nclass Trebuchet::Strategy::Default < Trebuchet::Strategy::Base\n  include Singleton\n\n  def initialize(options = nil)\n    # ignore options\n  end\n\n  def name\n    :default\n  end\n\n  def launch_at?(user, request = nil)\n    false\n  end\n\n  def needs_user?\n    false\n  end\n\n  def to_s\n    \"not launched (default)\"\n  end\n\nend\n"
  },
  {
    "path": "lib/trebuchet/strategy/everyone.rb",
    "content": "require 'singleton'\n# Everyone is to launch the feature to everyone\nclass Trebuchet::Strategy::Everyone < Trebuchet::Strategy::Base\n  include Singleton\n\n  def initialize(options = nil)\n    # ignore options\n  end\n\n  def name\n    :everyone\n  end\n\n  def launch_at?(user, request = nil)\n    true\n  end\n\n  def needs_user?\n    false\n  end\n\n  def to_s\n    \"launched to everyone\"\n  end\n\nend\n"
  },
  {
    "path": "lib/trebuchet/strategy/experiment.rb",
    "content": "# require 'digest/sha1'\n\nclass Trebuchet::Strategy::Experiment < Trebuchet::Strategy::Base\n  \n  include Trebuchet::Strategy::Experimentable  \n\n  def initialize(options = {})\n    initialize_experiment(options)\n  end\n\n  def launch_at?(user, request = nil)\n    return false unless user && user.id\n    value_in_bucket?(user.id)\n  end\n\n  # def to_s from experimentable\n\n  # def export from experimentable\n\nend"
  },
  {
    "path": "lib/trebuchet/strategy/hostname.rb",
    "content": "class Trebuchet::Strategy::Hostname < Trebuchet::Strategy::Base\n\n  attr_reader :hostnames\n\n  def initialize(hostnames)\n    @hostnames = if hostnames.is_a?(Array)\n      hostnames\n    else\n      [hostnames]\n    end\n  end\n\n  def launch_at?(user, request = nil)\n    return false if request.nil?\n    self.hostnames.include?(request.host)\n  end\n\n  def needs_user?\n    false\n  end\n\n  def to_s\n    \"hostnames (#{hostnames.empty? ? 'none' : hostnames.join(', ')})\"\n  end\n\n  def export\n    super @hostnames\n  end\n\nend\n"
  },
  {
    "path": "lib/trebuchet/strategy/invalid.rb",
    "content": "# Default is to not launch the feature to anyone\nclass Trebuchet::Strategy::Invalid < Trebuchet::Strategy::Base\n\n  attr_reader :invalid_name, :options\n\n  def initialize(name, options = nil)\n    @invalid_name = name\n    @options = options\n  end\n\n  def name\n    :invalid\n  end\n\n  def launch_at?(user, request = nil)\n    false\n  end\n  \n  def needs_user?\n    false\n  end\n\n  def to_s\n    \"#{invalid_name} (invalid) #{options.inspect if options}\"\n  end\n  \nend"
  },
  {
    "path": "lib/trebuchet/strategy/logic_and.rb",
    "content": "require 'trebuchet/strategy/logic_base'\n\nclass Trebuchet::Strategy::LogicAnd < Trebuchet::Strategy::LogicBase\n\n  def launch_at?(user, request = nil)\n    @strategies\n      .all? { |s| (!s.needs_user? || !user.nil?) && s.launch_at?(user, request) }\n  end\n\nend"
  },
  {
    "path": "lib/trebuchet/strategy/logic_base.rb",
    "content": "class Trebuchet::Strategy::LogicBase < Trebuchet::Strategy::Base\n\n  attr_reader :strategies\n  attr_reader :options\n\n  def initialize(options = {})\n    @options = options\n    @strategies = []\n    options.each do |strategy_name, strategy_options|\n      @strategies << Trebuchet::Strategy.find(strategy_name.to_sym, strategy_options)\n    end\n  end\n\n  # Override feature setter so that @feature gets set on @strategies as well\n  def feature=(f)\n    @feature = f\n    @strategies.each { |s| s.feature = f }\n  end\n\n  def launch_at?(user, request = nil)\n    false # To be overriden in implementation classes.\n  end\n\n  def needs_user?\n    false\n  end\n\nend"
  },
  {
    "path": "lib/trebuchet/strategy/logic_not.rb",
    "content": "require 'trebuchet/strategy/logic_base'\n\nclass Trebuchet::Strategy::LogicNot < Trebuchet::Strategy::LogicBase\n\n  def launch_at?(user, request = nil)\n    @strategies\n      .none? { |s| (!s.needs_user? || !user.nil?) && s.launch_at?(user, request) }\n  end\n\nend"
  },
  {
    "path": "lib/trebuchet/strategy/logic_or.rb",
    "content": "require 'trebuchet/strategy/logic_base'\n\nclass Trebuchet::Strategy::LogicOr < Trebuchet::Strategy::LogicBase\n\n  def launch_at?(user, request = nil)\n    @strategies\n      .any? { |s| (!s.needs_user? || !user.nil?) && s.launch_at?(user, request) }\n  end\n\nend"
  },
  {
    "path": "lib/trebuchet/strategy/multiple.rb",
    "content": "class Trebuchet::Strategy::Multiple < Trebuchet::Strategy::Base\n\n  attr_reader :strategies\n\n  def initialize(args)\n    @strategies = []\n    args.each_slice(2) do |pair|\n      @strategies << Trebuchet::Strategy.find(*pair)\n    end\n  end\n\n  # override setter so that @feature gets set on @strategies as well\n  def feature=(f)\n    @feature = f\n    @strategies.each {|s| s.feature = f}\n  end\n\n  def launch_at?(user, request = nil)\n    !!(strategies.select{|s| !user.nil? || !s.needs_user?}.find { |s| s.launch_at?(user, request) })\n    # !!(strategies.find { |s| s.launch_at?(user, request) })\n  end\n\n  def as_json(options = {})\n    @strategies\n  end\n\n  def needs_user?\n    false # assume some of the strategies may not need user\n    # could change this so it calls only the strategies that don't need a user when none is present\n    # strategies.any? { |s| s.needs_user? }\n  end\n\n  def export\n    super :strategies => strategies.map(&:export)\n  end\n\nend\n"
  },
  {
    "path": "lib/trebuchet/strategy/nobody.rb",
    "content": "require 'singleton'\n# Nobody is to launch the feature to nobody\nclass Trebuchet::Strategy::Nobody < Trebuchet::Strategy::Base\n  include Singleton\n\n  def initialize(options = nil)\n    # ignore options\n  end\n\n  def name\n    :nobody\n  end\n\n  def launch_at?(user, request = nil)\n    false\n  end\n\n  def needs_user?\n    false\n  end\n\n  def to_s\n    \"launched to nobody\"\n  end\n\nend\n"
  },
  {
    "path": "lib/trebuchet/strategy/per_denomination.rb",
    "content": "class Trebuchet::Strategy::PerDenomination < Trebuchet::Strategy::Base\n  include Trebuchet::Strategy::PerDenominationable\n\n  def set_range_from_options(options = {})\n    numerator = options['numerator'] || options[:numerator] || 0\n    denominator = options['denominator'] || options[:denominator] || 0\n\n    super(numerator: numerator, denominator: denominator)\n  end\n\n  def launch_at?(user, request = nil)\n    return false unless user && user.id\n    value_in_range?(user.id.to_i)\n  end\n\n  # def to_s from PerDenominationable\n\n  # def export from PerDenominationable\nend\n"
  },
  {
    "path": "lib/trebuchet/strategy/percent.rb",
    "content": "class Trebuchet::Strategy::Percent < Trebuchet::Strategy::Base\n\n  include Trebuchet::Strategy::Percentable\n\n  def initialize(options)\n    set_range_from_options(options)\n  end\n\n  def launch_at?(user, request = nil)\n    return false unless user && user.id\n    value_in_range?(user.id.to_i)\n  end\n\n  # def to_s from percentable\n\n  # def export from percentable\n\nend\n"
  },
  {
    "path": "lib/trebuchet/strategy/percent_deprecated.rb",
    "content": "class Trebuchet::Strategy::PercentDeprecated < Trebuchet::Strategy::Base\n\n  include Trebuchet::Strategy::PercentableDeprecated\n\n  def initialize(options)\n    set_range_from_options(options)\n  end\n\n  def launch_at?(user, request = nil)\n    return false unless user && user.id\n    value_in_range?(user.id.to_i)\n  end\n\n  # def to_s from percentable\n\n  # def export from percentable\n\nend\n\n"
  },
  {
    "path": "lib/trebuchet/strategy/stub.rb",
    "content": "class Trebuchet\n  module Strategy\n\n    class Stub < Trebuchet::Strategy::Base\n      attr_reader :state\n\n      def initialize(state)\n        @state = state\n      end\n\n      def launch_at?(user, request = nil)\n        state == :launched\n      end\n\n      def needs_user?\n        false\n      end\n\n      def to_s\n        \"stub (#{state}}\"\n      end\n\n      def export\n        super state\n      end\n\n    end\n\n  end\nend"
  },
  {
    "path": "lib/trebuchet/strategy/user_id.rb",
    "content": "class Trebuchet::Strategy::UserId < Trebuchet::Strategy::Base\n\n  attr_reader :user_ids\n\n  def initialize(user_ids)\n    user_ids = Array(user_ids).flatten\n    @user_ids = Set.new(user_ids)\n  end\n\n  def launch_at?(user, request = nil)\n    @user_ids.include?(user.id)\n  end\n\n  def to_s\n    \"user ids (#{user_ids.empty? ? 'none' : user_ids.to_a.join(', ')})\"\n  end\n\n  def export\n    super @user_ids.to_a\n  end\n\nend"
  },
  {
    "path": "lib/trebuchet/strategy/visitor_experiment.rb",
    "content": "class Trebuchet::Strategy::VisitorExperiment < Trebuchet::Strategy::Base\n\n  include Trebuchet::Strategy::Experimentable\n\n  def initialize(options = {})\n    initialize_experiment(options)\n  end\n\n  def launch_at?(user, request = nil)\n   if Trebuchet.visitor_id.respond_to?(:call)\n      visitor_id = Trebuchet.visitor_id.call(request)\n    else\n      visitor_id = nil\n    end\n    return false if visitor_id.nil?\n    value_in_bucket?(visitor_id)\n  end\n\n  def needs_user?\n    false\n  end\n\n  # def to_s from experimentable\n\n  # def export from experimentable\n\nend"
  },
  {
    "path": "lib/trebuchet/strategy/visitor_percent.rb",
    "content": "class Trebuchet::Strategy::VisitorPercent < Trebuchet::Strategy::Base\n\n  include Trebuchet::Strategy::Percentable\n\n  def initialize(options)\n    set_range_from_options(options)\n  end\n\n  def launch_at?(user, request = nil)\n    return false if request.nil?\n    if Trebuchet.visitor_id.respond_to?(:call)\n      visitor_id = Trebuchet.visitor_id.call(request)\n    else\n      visitor_id = nil\n    end\n    return false if visitor_id.nil?\n    value_in_range?(visitor_id.to_i)\n  end\n\n  def needs_user?\n    false\n  end\n\n  # def to_s from percentable\n\nend\n"
  },
  {
    "path": "lib/trebuchet/strategy/visitor_percent_deprecated.rb",
    "content": "class Trebuchet::Strategy::VisitorPercentDeprecated < Trebuchet::Strategy::Base\n\n  include Trebuchet::Strategy::PercentableDeprecated\n\n  def initialize(options)\n    set_range_from_options(options)\n  end\n\n  def launch_at?(user, request = nil)\n    return false if request.nil?\n    if Trebuchet.visitor_id.respond_to?(:call)\n      visitor_id = Trebuchet.visitor_id.call(request)\n    else\n      visitor_id = nil\n    end\n    return false if visitor_id.nil?\n    value_in_range?(visitor_id.to_i)\n  end\n\n  def needs_user?\n    false\n  end\n\n  # def to_s from percentable\n\nend\n\n"
  },
  {
    "path": "lib/trebuchet/strategy.rb",
    "content": "require 'digest/sha1'\n\nmodule Trebuchet::Strategy\n\n  def self.for_feature(feature)\n    stub_state = Trebuchet::Feature.stubbed_features[feature.name]\n    if stub_state\n      Stub.new(stub_state)\n    else\n      strategy_args = Trebuchet.backend.get_strategy(feature.name)\n      find(*strategy_args).tap {|s| s.feature = feature }\n    end\n  end\n\n  def self.find(*args)\n    strategy_name, options = args\n\n    if args.size > 2\n      Multiple.new(args)\n    elsif strategy_name.nil? || strategy_name == :default\n      # Strategy hasn't been defined yet\n      Default.instance\n    elsif strategy_name == :everyone\n      Everyone.instance\n    elsif strategy_name == :nobody\n      Nobody.instance\n    elsif CustomRequestAware.exists?(strategy_name)\n      CustomRequestAware.new(strategy_name, options)\n    elsif Custom.exists?(strategy_name)\n      Custom.new(strategy_name, options)\n    elsif klass = class_for_name(strategy_name)\n      # percent, users\n      klass.new(options)\n    else\n      Invalid.new(strategy_name, options)\n    end\n  end\n\n  # The stub strategy purposely left out of this list as it should be\n  # accessible via the testing interface only and not externally.\n  def self.name_class_map\n    [\n      [:visitor_percent_deprecated, VisitorPercentDeprecated],\n      [:percent_deprecated, PercentDeprecated],\n      [:percent, Percent],\n      [:per_denomination, PerDenomination],\n      [:users, UserId],\n      [:default, Default],\n      [:everyone, Everyone],\n      [:nobody, Nobody],\n      [:custom, Custom],\n      [:multiple, Multiple],\n      [:experiment, Experiment],\n      [:visitor_percent, VisitorPercent],\n      [:hostname, Hostname],\n      [:visitor_experiment, VisitorExperiment],\n      [:logic_and, LogicAnd],\n      [:logic_or, LogicOr],\n      [:logic_not, LogicNot],\n    ]\n  end\n\n  def self.deprecated_strategy_names\n    [\n      :percent_deprecated,\n      :visitor_percent_deprecated\n    ]\n  end\n\n  def self.class_for_name(name)\n    classes = Hash[name_class_map]\n    classes[name]\n  end\n\n  def self.name_for_class(klass)\n    names = Hash[name_class_map.map(&:reverse)]\n    names[klass]\n  end\n\n  module PerDenominationable\n    attr_reader :numerator, :denominator\n\n    def initialize(options)\n      set_range_from_options(options)\n    end\n\n    # must be called from initialize\n    def set_range_from_options(options)\n      raise ArgumentError, \"Missing required input numerator\" unless options[:numerator]\n      raise ArgumentError, \"Missing required input denominator\" unless options[:denominator]\n\n      @numerator = options[:numerator].to_i\n      @denominator = options[:denominator].to_i\n\n      raise ArgumentError, \"Invalid denominator #{@denominator}\" if @denominator <= 0\n      if @numerator > @denominator\n        raise ArgumentError, \"Invalid numerator #{@numerator} > denominator #{@denominator}\"\n      end\n    end\n\n    def value_in_range?(value)\n      bucket = Trebuchet::SHA1.hexdigest(\"#{@feature.name}|#{value}\").to_i(16) % denominator\n\n      bucket < numerator\n    end\n\n    def to_s\n      \"#{numerator} / #{denominator} of users\"\n    end\n\n    def export\n      super({ numerator: numerator, denominator: denominator })\n    end\n  end\n\n  ### Percentable module standardizes logic for percentage-based strategies\n\n  module Percentable\n    include PerDenominationable\n\n    alias_method :percentage, :numerator\n\n    # must be called from initialize\n    def set_range_from_options(options)\n      numerator =\n        if options == nil || options.is_a?(Numeric)\n          options.to_i\n        else\n          0\n        end\n\n      super(numerator: numerator, denominator: 100)\n    end\n\n    def to_s\n      kind = self.name == :visitor_percent ? \"visitors\" : \"users\"\n      percentage_str = \"#{percentage}% of #{kind}\"\n      \"#{percentage_str}\"\n    end\n\n    def export\n      super(percentage)\n    end\n  end\n\n\n  # This module is deprecated because the implementation is such that it's\n  # not possible to trust per-feature analysis if multiple features are\n  # using the PercentableDeprecated based strategies because the same\n  # visitors will tend to get the same features (even with the offset).\n  module PercentableDeprecated\n\n    def initialize(options)\n      set_range_from_options(options)\n    end\n\n    # must be called from initialize\n    def set_range_from_options(options)\n      if options == nil || options.is_a?(Numeric)\n        @from = 0\n        @to = options.to_i - 1\n        @style = :percentage\n      elsif options.is_a?(Hash) && (p = options['percentage'] || options[:percentage])\n        @from = 0\n        @to = p.to_i - 1\n        @style = :percentage\n      elsif options.is_a?(Hash)\n        @from = options['from'] || options[:from]\n        @to = options['to'] || options[:to]\n        @style = :range\n      else\n        @from = 0\n        @to = -1\n      end\n    end\n\n    def offset\n      if @style == :percentage\n        feature_id % 100\n      else\n        0\n      end\n    end\n\n    def percentage\n      return 0 unless @to.is_a?(Integer) && @from.is_a?(Integer)\n      return 0 if @to < 0\n      ((@to - @from) + 100) % 100 + 1\n    end\n\n    # call from launch_at? and pass in user id or another integer\n    def value_in_range?(value)\n      return false unless @from && @to\n      return false if @from.to_i < 0 || @to.to_i < 0\n      return false if value == nil || !value.is_a?(Numeric)\n      cutoff = percentage\n      value = ((value - @from) + 200 - offset) % 100\n      !!(value < cutoff)\n    end\n\n    def offset_from\n      (@from + offset) % 100\n    end\n\n    def offset_to\n      (@to + offset) % 100\n    end\n\n    def to_s\n      kind = self.name == :visitor_percent ? \"visitors\" : \"users\"\n      percentage_str = \"#{percentage}% of #{kind}\"\n      range_str = if @to < 0\n        \"nobody\"\n      else\n        str = ''\n        str << \"user id ending with \" if kind != \"visitors\"\n        str << \"#{offset_from.to_s.rjust(2, '0')}\"\n        str << \" to #{offset_to.to_s.rjust(2, '0')}\" if @to != @from\n        str\n      end\n      @style == :range ? \"#{range_str} (#{percentage_str})\" : \"#{percentage_str} (#{range_str})\"\n    end\n\n    def export\n      if @style == :percentage\n        super :percentage => @to\n      else\n        super :from => @from, :to => @to\n      end\n    end\n\n  end\n\n  module Experimentable\n\n    attr_reader :bucket, :total_buckets, :experiment_name\n\n    def initialize_experiment(options)\n      options.keys.each {|k| options[k.to_sym] = options.delete(k)} # cheap symbolize_keys\n      @experiment_name = options[:name]\n      @bucket = [ options[:bucket] ].flatten # always treat as an array\n      @total_buckets = options[:total_buckets] || 5\n    end\n\n    def value_in_bucket?(value)\n      return false if value == nil || !value.is_a?(Numeric)\n      return false unless self.valid?\n      # must hash feature name and value together to ensure uniform distribution\n      b = Trebuchet::SHA1.hexdigest(\"experiment: #{@experiment_name.downcase} user: #{value}\").to_i(16) % total_buckets\n      !!@bucket.include?(b + 1) # is user in this bucket?\n    end\n\n    def valid?\n      experiment_name && total_buckets > 0 && bucket.max <= total_buckets && (1..total_buckets).include?(bucket.min)\n    rescue\n      false\n    end\n\n    def type\n      \"#{name == :experiment ? \"user\" : \"visitor\"} experiment\"\n    end\n\n    def as_json(options = {})\n      {\n        :name => experiment_name,\n        :bucket => bucket,\n        :total_buckets => total_buckets,\n        :type => self.type\n      }\n    end\n\n    def to_s\n      str = \"buckets (#{bucket.join(', ')}) of total: #{total_buckets}\"\n      str << \" for #{self.type} experiment: #{experiment_name}\"\n    end\n\n    def export\n      super :name => experiment_name, :bucket => bucket, :total_buckets => total_buckets\n    end\n\n    def inspect\n      \"#<#{self.class.name} #{self}>\"\n    end\n\n  end\n\nend\n"
  },
  {
    "path": "lib/trebuchet/version.rb",
    "content": "class Trebuchet\n\n  VERSION = \"0.12.1\".freeze\n\nend\n"
  },
  {
    "path": "lib/trebuchet.rb",
    "content": "require 'digest/sha1'\nrequire 'forwardable'\nclass Trebuchet\n  # initialize a single one to save object allocations\n  # Todo perhaps choose a better hash instead of sha1\n  SHA1 = Digest::SHA1.new\n\n  class << self\n    extend Forwardable\n\n    def_delegators :state, :current=, :current_block=, :current_block,\n      :logs, :admin_view, :admin_view=, :admin_edit, :admin_edit=,\n      :time_zone, :time_zone=, :author=, :author, :visitor_id\n\n    attr_accessor :exception_handler\n    attr_accessor :threadsafe_state\n\n    def backend\n      self.backend = :memory unless @backend\n      @backend\n    end\n\n    def set_backend(backend_type, *args)\n      if backend_type.is_a?(Symbol)\n        require \"trebuchet/backend/#{backend_type}\"\n        @backend = Backend.lookup(backend_type).new(*args)\n      elsif backend_type.class.name =~ /Trebuchet::Backend/\n        @backend = backend_type\n      end\n    end\n\n    # this only works with additional args, e.g.: Trebuchet.backend = :memory\n    alias_method :backend=, :set_backend\n\n    alias_method :threadsafe_state?, :threadsafe_state\n\n    # Logging done at class level\n    # TODO: split by user identifier so instance can return scoped to one user\n    # (in case multiple users have user.trebuchet called)\n    def initialize_logs\n      state.logs = {}\n    end\n\n    def log(feature_name, result)\n      initialize_logs if state.logs.nil?\n      logs[feature_name] = result\n    end\n\n    def logs\n      state.logs\n    end\n\n    def current=(other)\n      state.current = other\n    end\n\n    def current\n      state.current ||= current_block.call if current_block.respond_to?(:call)\n      state.current || new(nil) # return an blank Trebuchet instance if @current is not set\n    end\n\n    def reset_current!\n      self.current = nil\n    end\n\n    def thread_local_key\n      :trebuchet_state\n    end\n\n    # state is a representation of the current context of Trebuchet such as current and\n    # current_proc, which are expected to be different between threads or fibers.\n    # exception_handler and backend are not included in this state object as they are\n    # not expected to change from fiber to fiber or request to request, therefore they\n    # must be thread/fibersafe on their own accord.\n    def state\n      if threadsafe_state?\n        Thread.current[thread_local_key] ||= State.new\n      else\n        @state ||= State.new\n      end\n    end\n\n    def state=(new_state)\n      if threadsafe_state?\n        Thread.current[thread_local_key] = new_state\n      else\n        @state = new_state\n      end\n    end\n\n    def threadsafe_state?\n      threadsafe_state\n    end\n  end\n\n  def self.aim(feature_name, *args)\n    Feature.find(feature_name).aim(*args)\n  end\n\n  def self.dismantle(feature_name)\n    Feature.find(feature_name).dismantle\n  end\n\n  def self.dismantle_stubs\n    Feature.dismantle_stubs\n  end\n\n  def self.define_strategy(name, &block)\n    Strategy::Custom.define(name, block)\n  end\n\n  def self.define_request_aware_strategy(name, &block)\n    Strategy::CustomRequestAware.define(name, block)\n  end\n\n  def self.visitor_id=(id_or_proc)\n    if id_or_proc.is_a?(Proc)\n      state.visitor_id = id_or_proc\n    elsif id_or_proc.is_a?(Integer)\n      state.visitor_id = proc { |request| id_or_proc }\n    else\n      state.visitor_id = nil\n    end\n  end\n\n  def self.use_with_rails!\n    if defined?(ActionController::Base)\n      ActionController::Base.send(:include, Trebuchet::ActionController)\n    end\n  end\n\n  def self.feature(name)\n    Feature.find(name)\n  end\n\n  def initialize(current_user, request = nil)\n    @current_user = current_user\n    @request = request\n    @result_cache = {}\n  end\n\n  def launch(feature, &block)\n    if launch?(feature)\n      yield if block_given?\n    end\n  end\n\n  def launch?(feature)\n    result = @result_cache[feature]\n\n    if result.nil?\n      result = @result_cache[feature] =\n        !!Feature.find(feature).launch_at?(@current_user, @request)\n      Trebuchet.log(feature, result)\n    end\n\n    result\n  rescue => e\n    handle_exception(e, feature)\n    return false\n  end\n\n  def handle_exception(exception, feature = nil)\n    if self.class.exception_handler.is_a?(Proc)\n      argc = self.class.exception_handler.arity\n      argc = 3 if argc < 0\n      self.class.exception_handler.call *[exception, feature, self][0,argc]\n    end\n  end\n\n  def self.export\n    {}.tap do |features|\n      Trebuchet.backend.get_feature_names.map do |fn|\n        features[fn] = self.feature(fn).strategy.export\n      end\n    end\n  end\n\n  def self.history(include_archived = false)\n    return [] unless Trebuchet.backend.respond_to?(:get_all_history)\n    Trebuchet.backend.get_all_history(include_archived).map do |row|\n      [Time.at(row.first), Feature.find(row.last)]\n    end\n  end\n\nend\n\nrequire 'set'\nrequire 'trebuchet/version'\nrequire 'trebuchet/error'\nrequire 'trebuchet/backend'\nrequire 'trebuchet/backend/disabled'\n# load other backends on demand so their dependencies can load first\nrequire 'trebuchet/state'\nrequire 'trebuchet/feature'\nrequire 'trebuchet/strategy'\nrequire 'trebuchet/strategy/base'\nrequire 'trebuchet/strategy/custom'\nrequire 'trebuchet/strategy/custom_request_aware'\nrequire 'trebuchet/strategy/default'\nrequire 'trebuchet/strategy/everyone'\nrequire 'trebuchet/strategy/experiment'\nrequire 'trebuchet/strategy/hostname'\nrequire 'trebuchet/strategy/invalid'\nrequire 'trebuchet/strategy/logic_and'\nrequire 'trebuchet/strategy/logic_not'\nrequire 'trebuchet/strategy/logic_or'\nrequire 'trebuchet/strategy/multiple'\nrequire 'trebuchet/strategy/nobody'\nrequire 'trebuchet/strategy/percent'\nrequire 'trebuchet/strategy/percent_deprecated'\nrequire 'trebuchet/strategy/per_denomination'\nrequire 'trebuchet/strategy/user_id'\nrequire 'trebuchet/strategy/visitor_experiment'\nrequire 'trebuchet/strategy/visitor_percent'\nrequire 'trebuchet/strategy/visitor_percent_deprecated'\n\nrequire 'trebuchet/strategy/stub'\nrequire 'trebuchet/action_controller'\n"
  },
  {
    "path": "lib/trebuchet_rails/engine.rb",
    "content": "require 'rails'\nrequire File.expand_path(File.dirname(__FILE__) + \"/../trebuchet\")\n\n\nmodule TrebuchetRails\n  class Engine < Rails::Engine\n    isolate_namespace TrebuchetRails if respond_to?(:isolate_namespace)\n  end\nend\n\n\nTrebuchet.use_with_rails!"
  },
  {
    "path": "lib/trebuchet_rails.rb",
    "content": "require 'trebuchet_rails/engine'\n\nrequire 'trebuchet'"
  },
  {
    "path": "spec/custom_request_aware_strategy_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::CustomRequestAware do\n  it \"should launch according to the custom strategy with options\" do\n    Trebuchet.define_request_aware_strategy(:ip_address_strategy) do |current_user, request, ip_address|\n      request[:ip_address] == ip_address\n    end\n\n    Trebuchet.aim('ip_limited_feature', :ip_address_strategy, '1.1.1.1')\n\n    Trebuchet.new(User.new, { :ip_address => '1.1.1.1' }).launch?('ip_limited_feature')\n      .should be_true\n    Trebuchet.new(User.new, { :ip_address => '2.2.2.2' }).launch?('ip_limited_feature')\n      .should be_false\n  end\n\n  it \"should launch according to the custom strategy without options\" do\n    Trebuchet.define_request_aware_strategy(:ip_address_strategy) do |current_user, request|\n      request[:ip_address] == '1.1.1.1'\n    end\n\n    Trebuchet.aim('ip_limited_feature', :ip_address_strategy)\n\n    Trebuchet.new(User.new, { :ip_address => '1.1.1.1' }).launch?('ip_limited_feature')\n      .should be_true\n    Trebuchet.new(User.new, { :ip_address => '2.2.2.2' }).launch?('ip_limited_feature')\n      .should be_false\n  end\n\n  it \"should not explode when request is nil\" do\n    Trebuchet.define_request_aware_strategy(:ip_address_strategy) do |current_user, request|\n      request[:ip_address] == '1.1.1.1'\n    end\n\n    Trebuchet.aim('ip_limited_feature', :ip_address_strategy)\n\n    Trebuchet.new(User.new).launch?('ip_limited_feature').should be_false\n  end\nend\n"
  },
  {
    "path": "spec/custom_strategy_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::Custom do\n\n  it \"should launch according to the custom strategy\" do\n    Trebuchet.define_strategy(:admins) do |current_user|\n      current_user.has_role?(:admin)\n    end\n\n    Trebuchet.aim('admin_feature', :admins)\n\n    Trebuchet.new(User.new(1, :admin)).launch?('admin_feature').should be_true\n    Trebuchet.new(User.new(1, :user)).launch?('admin_feature').should be_false\n  end\n\n  it \"should pass arguments to the custom strategy\" do\n    Trebuchet.define_strategy(:role) do |current_user, role|\n      current_user.has_role?(role.to_sym)\n    end\n\n    Trebuchet.aim('power_feature', :role, :power_user)\n\n    Trebuchet.new(User.new(1, :power_user)).launch?('power_feature').should be_true\n    Trebuchet.new(User.new(1, :user)).launch?('power_feature').should be_false\n  end\n\n  it \"should allow an always-on strategy\" do\n    Trebuchet.define_strategy(:yes) { |user| true }\n    Trebuchet.aim(\"perma-feature\", :yes)\n    Trebuchet.new(User.new 999).launch?(\"perma-feature\").should be_true\n    Trebuchet.new(User.new nil).launch?(\"perma-feature\").should be_true\n  end\n\n  it \"should needs_user? based on block arity\" do\n    # still a good idea to nilcheck within block however\n    pending \"re-enable after switching to  { |options, user, request| }\"\n    Trebuchet.define_strategy(:yes) { |user| true }\n    Trebuchet.define_strategy(:heck_yeah) { |user, request| true }\n    Trebuchet.define_strategy(:never) { false }\n    Trebuchet.define_strategy(:always) { true }\n    Trebuchet::Strategy::Custom.new(:yes).needs_user?.should be_true\n    Trebuchet::Strategy::Custom.new(:heck_yeah).needs_user?.should be_true\n    Trebuchet::Strategy::Custom.new(:never).needs_user?.should be_false\n    Trebuchet::Strategy::Custom.new(:always).needs_user?.should be_false\n  end\n\nend\n"
  },
  {
    "path": "spec/default_strategy_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::Default do\n\n  it \"should not launch if no strategy was defined\" do\n    Trebuchet.new(User.new(rand(2 << 32))).launch?('default').should be_false\n  end\n  \n  it \"should be named default\" do\n    Trebuchet::Strategy::Default.strategy_name.should == :default\n    Trebuchet.feature('whatever').strategy.name.should == :default\n  end\n  \n  it \"should always return false\" do\n    Trebuchet.feature('time_machine').aim(:default)\n    t = Trebuchet.new User.new(1)\n    t.launch?('time_machine').should === false\n  end\n  \n  it \"should return false when missing user\" do\n    Trebuchet.feature('time_machine').aim(:default)\n    t = Trebuchet.new nil\n    t.launch?('time_machine').should === false\n  end\n\nend\n"
  },
  {
    "path": "spec/disabled_backend_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe Trebuchet::Backend::Disabled do\n  \n  before do\n    Trebuchet.backend = :disabled\n    Trebuchet.backend.should be_a(Trebuchet::Backend::Disabled)\n  end\n\n  it \"should not store features\" do\n    Trebuchet.feature('thing').aim(:users, [5]).aim(:percent, 9)\n    Trebuchet.feature('thing').strategy.should be_a(Trebuchet::Strategy::Default)\n    Trebuchet::Feature.all.should eql []\n  end\n  \n  it \"should always return false\" do\n    Trebuchet.feature('time_machine').aim(:disabled)\n    t = Trebuchet.new User.new(1)\n    t.launch?('time_machine').should === false\n  end\n  \n  after do\n    Trebuchet.backend = :memory\n  end\n \n\nend\n"
  },
  {
    "path": "spec/everyone_strategy_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::Everyone do\n\n  it \"should be named everyone\" do\n    Trebuchet::Strategy::Everyone.strategy_name.should == :everyone\n    Trebuchet.feature('time_machine').aim(:everyone)\n    Trebuchet.feature('time_machine').strategy.name.should == :everyone\n  end\n\n  it \"should always return true\" do\n    Trebuchet.feature('time_machine').aim(:everyone)\n    t = Trebuchet.new User.new(1)\n    t.launch?('time_machine').should === true\n  end\n\n  it \"should return true when missing user\" do\n    Trebuchet.feature('time_machine').aim(:everyone)\n    t = Trebuchet.new nil\n    t.launch?('time_machine').should === true\n  end\n\nend\n"
  },
  {
    "path": "spec/experiment_strategy_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::Experiment do\n  \n  before do\n    @feature_name = \"Photographic Memory\"\n    @experiment_name = \"Superhumanity\"\n  end\n  \n  it \"should match a user in a bucket\" do\n    Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => 1)\n    strategy = Trebuchet.feature(@feature_name).strategy\n    # these values just happen to hash for the algorithm and experiment name\n    strategy.launch_at?(User.new(5)).should be_true\n    strategy.launch_at?(User.new(4)).should be_false\n  end\n  \n  it \"should adjust the number of buckets\" do\n    Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => 1, :total_buckets => 3)\n    strategy = Trebuchet.feature(@feature_name).strategy\n    strategy.total_buckets.should == 3\n    strategy.bucket.should == [1]\n    strategy.launch_at?(User.new(4)).should be_true\n    Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => 2, :total_buckets => 3)\n    Trebuchet.feature(@feature_name).strategy.launch_at?(User.new(4)).should be_false\n  end\n  \n  it \"should be mutually exclusive within experiments\" do\n    strategies = (1..10).map do |i|\n      Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => i)\n      Trebuchet.feature(@feature_name).strategy\n    end\n    user_ids = (1..100).to_a\n    launches = strategies.map do |strategy|\n      user_ids.select {|user_id| strategy.launch_at?(User.new(user_id))}\n    end\n    occurrences = user_ids.map do |user_id|\n      launches.select{|l| l.include?(user_id)}.size\n    end\n    # no user should be in more than one bucket\n    occurrences.select{|i| i > 1}.size.should == 0\n    # each user should be in one bucket\n    occurrences.select{|i| i < 1}.size.should == 0\n  end\n  \n  it \"should distribute users evenly\" do\n    Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => 1)\n    strategy = Trebuchet.feature(@feature_name).strategy\n    user_ids = (1..10_000).to_a\n    launches = user_ids.map {|user_id| strategy.launch_at?(User.new(user_id))}\n    # total should be around 20% (spread evenly across default of 5 buckets)\n    launch_count = launches.select{|l| l == true}.size\n    (launch_count * 100 / user_ids.size).round.should == 20\n  end\n  \n  it \"should have low overlap between experiments\" do\n    other_experiment_name = 'World Peace'\n    other_feature_name = 'Friendship Rings'\n    another_experiment_name = 'Space Tourism'\n    another_feature_name = 'Orbital Disneyland'\n    Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => 1)\n    Trebuchet.aim(other_feature_name, :experiment, :name => other_experiment_name, :bucket => 1)\n    Trebuchet.aim(another_feature_name, :experiment, :name => another_experiment_name, :bucket => 1)\n    strategies = [\n      Trebuchet.feature(@feature_name).strategy,\n      Trebuchet.feature(other_feature_name).strategy,\n      Trebuchet.feature(another_feature_name).strategy\n    ]\n    # find out which users match each strategy\n    user_ids = (1..10_000).to_a\n    launches = strategies.map do |strategy|\n      user_ids.select {|user_id| strategy.launch_at?(User.new(user_id))}\n    end\n    # intersect each set with the next\n    overlaps = []\n    (0..launches.size).each do |i|\n      j = (i + 1) % launches.size\n      overlaps << launches[i] & launches[j]\n    end\n    # each group should have about 20% overlap (for 5 buckets)\n    ((19..21) === (overlaps[0].size * 100 / user_ids.size).round).should be_true\n    ((19..21) === (overlaps[1].size * 100 / user_ids.size).round).should be_true\n    ((19..21) === (overlaps[2].size * 100 / user_ids.size).round).should be_true\n    # 1% or fewer of users should be in all three groups\n    total_overlap = (launches[0] & launches[1] & launches[2])    \n    (total_overlap.size * 100 / user_ids.size).round.should < 2\n  end\n  \n  it \"should return false for invalid parameters\" do\n    Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => 900)\n    Trebuchet.feature(@feature_name).strategy.should_not be_valid\n    Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => 1, :total_buckets => -17)\n    Trebuchet.feature(@feature_name).strategy.should_not be_valid\n    Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => 10, :total_buckets => 3)\n    Trebuchet.feature(@feature_name).strategy.should_not be_valid\n    Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name)\n    Trebuchet.feature(@feature_name).strategy.should_not be_valid\n    Trebuchet.aim(@feature_name, :experiment, :bucket => 1)\n    Trebuchet.feature(@feature_name).strategy.should_not be_valid\n  end\n   \nend"
  },
  {
    "path": "spec/feature_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe Trebuchet::Feature do\n  \n  def feature\n    Trebuchet.feature('some_feature')\n  end\n\n  def feature_names\n    Trebuchet.backend.get_feature_names\n  end\n\n  def archived_feature_names\n    Trebuchet.backend.get_archived_feature_names\n  end\n  \n  describe :aim do\n    \n    it \"should add one strategy\" do\n      feature.aim(:percent, 10)\n      feature.strategy.name.should be :percent\n    end\n    \n    it \"should add multiple strategies by chaining\" do\n      feature.aim(:default)\n      feature.strategy.name.should be :default\n      feature.aim(:percent, 10).aim(:users, 1)\n      feature.strategy.name.should be :multiple\n      strategy_names = feature.strategy.strategies.map{ |s| s.name }\n      strategy_names.should include(:percent)\n      strategy_names.should include(:users)\n    end\n    \n    it \"should obliterate chained strategies\" do\n      feature.aim(:percent, 10).aim(:users, 1)\n      feature.aim(:default)\n      feature.strategy.name.should be :default\n    end\n    \n  end\n  \n  describe :adjust do\n    \n    it \"should add one strategy\" do\n      feature.dismantle\n      feature.adjust(:percent, 10)\n      feature.strategy.name.should be :percent\n    end\n    \n    it \"should adjust a strategy in the chain\" do\n      feature.aim(:percent, 10).aim(:users, 1)\n      feature.strategy.name.should be :multiple\n      feature.adjust(:users, 2)\n      feature.strategy.name.should be :multiple\n      user_strategy = feature.strategy.strategies.detect { |s| s.name == :users }\n      user_strategy.user_ids.should include(2)\n      user_strategy.user_ids.should_not include(1)\n      feature.adjust(:percent, 20)\n      percent_strategy = feature.strategy.strategies.detect { |s| s.name == :percent }\n      percent_strategy.percentage.should == 20\n    end\n    \n    it \"should not obliterate chained strategies\" do\n      feature.aim(:percent, 10).aim(:users, 1)\n      feature.adjust(:default)\n      feature.strategy.name.should be :multiple\n      feature.strategy.strategies.map{ |s| s.name }.should include(:default)\n    end\n    \n  end\n  \n  describe :augment do\n    \n    it \"should add one strategy where none exist\" do\n      feature.dismantle\n      feature.augment(:percent, 10)\n      feature.strategy.name.should be :percent\n    end\n    \n    it \"should append a strategy if others exist\" do\n      feature.aim(:percent, 10)\n      feature.augment(:users, 1)\n      feature.strategy.name.should be :multiple\n      percent_strategy = feature.strategy.strategies.detect { |s| s.name == :percent }\n      percent_strategy.percentage.should == 10\n      user_strategy = feature.strategy.strategies.detect { |s| s.name == :users }\n      user_strategy.user_ids.should include(1)\n    end\n    \n    it \"should adjust an existing strategy with numeric\" do\n      feature.aim(:percent, 10)\n      feature.augment(:percent, 5)\n      feature.strategy.name.should be :percent\n      feature.strategy.percentage.should be 15\n      feature.augment(:percent, 5.0)\n      feature.strategy.percentage.should be 20\n    end\n    \n    it \"should adjust an existing strategy with set/array\" do\n      feature.aim(:users, [1])\n      feature.augment(:users, [2,3])\n      feature.strategy.name.should == :users\n      feature.strategy.user_ids.sort.should == [1,2,3]\n      feature.augment(:users, [4])\n      feature.strategy.user_ids.sort.should == [1,2,3,4]\n      # this probably should not be allowed\n      # feature.aim(:users, 1)\n      # feature.augment(:users, 2)\n      # feature.strategy.user_ids.sort.should == [1,2]\n    end\n    \n    it \"should adjust an existing strategy with hash\" do\n      Trebuchet.define_strategy(:role_percent) do |user, options|\n        percent = options[user.role].to_i\n        user.id % 100 < percent\n      end\n      old_percentages = {:admin => 30, :editor => 50, :publisher => 100}\n      feature.aim(:role_percent, old_percentages)\n      new_percentages = {:admin => 100, :reviewer => 10}\n      feature.augment(:role_percent, new_percentages)\n      feature.strategy.name.should be :custom\n      feature.strategy.custom_name.should be :role_percent\n      feature.strategy.options.should == (old_percentages.merge(new_percentages))\n    end\n    \n  end\n\n  describe :dismantle do\n\n    it \"should remove a feature and add it to archived features\" do\n      feature.aim(:users, [1])\n      feature_names.include?(feature.name).should be_true\n      feature.dismantle\n      feature_names.include?(feature.name).should be_false\n      archived_feature_names.include?(feature.name).should be_true\n    end\n\n    it \"should remove archived feature when dismantled feature is redefined\" do\n      feature.dismantle\n      feature_names.include?(feature.name).should be_false\n      archived_feature_names.include?(feature.name).should be_true\n      feature.aim(:percent, 5)\n      feature_names.include?(feature.name).should be_true\n      archived_feature_names.include?(feature.name).should be_false\n    end\n\n  end\n\nend\n"
  },
  {
    "path": "spec/logic_and_strategy_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::LogicAnd do\n\n  it \"passes the check only when all conditions are met - case 1\" do\n    Trebuchet.feature('pokemon').aim(\n      :logic_and,\n      {\n        percent: 0,\n        users: [30, 35],\n      },\n    )\n\n    t = Trebuchet.new(User.new(30))\n    t.launch?('pokemon').should === false\n    t = Trebuchet.new(User.new(111))\n    t.launch?('pokemon').should === false\n  end\n\n  it \"passes the check only when all conditions are met - case 2\" do\n    Trebuchet.feature('pokemon').aim(\n      :logic_and,\n      {\n        percent: 100,\n        users: [30, 35],\n      },\n    )\n\n    t = Trebuchet.new(User.new(30))\n    t.launch?('pokemon').should === true\n    t = Trebuchet.new(User.new(111))\n    t.launch?('pokemon').should === false\n  end\n\n  it \"nests well\" do\n    Trebuchet.feature('pokemon').aim(\n      :logic_and,\n      {\n        \"percent\" => 100,\n        \"logic_and\" => {\n          \"users\" => [90, 91, 92],\n          \"logic_not\" => {\n            \"users\" => [90],\n          },\n        },\n      },\n    )\n\n    [91, 92].each do |uid|\n      t = Trebuchet.new(User.new(uid))\n      t.launch?('pokemon').should === true\n    end\n    [1, 3, 5, 7, 90].each do |uid|\n      t = Trebuchet.new(User.new(uid))\n      t.launch?('pokemon').should === false\n    end\n  end\n\nend\n"
  },
  {
    "path": "spec/logic_base_strategy_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::LogicBase do\n\n  it \"should set @feature on sub-strategies\" do\n    feature = Trebuchet.feature('pokemon')\n    feature.aim(\n      :logic_and,\n      {\n        percent: 100,\n        users: [30, 35],\n      },\n    )\n    feature.strategy.feature.name.should == 'pokemon'\n    feature.strategy.strategies.first.feature.name.should == 'pokemon'\n    feature.strategy.strategies.last.feature.name.should == 'pokemon'\n  end\n\n  it \"should pass user and request to each strategy\" do\n    args = [:foo, 1]\n    user = mock \"User\"\n    request = mock \"Request\"\n    strategy = mock \"Strategy\"\n\n    Trebuchet::Strategy.should_receive(:find).with(*args).and_return(strategy)\n    strategy.should_receive(:launch_at?).with(user, request)\n    strategy.should_receive(:needs_user?).and_return(false)\n\n    s = Trebuchet::Strategy::LogicOr.new({foo: 1})\n    s.launch_at?(user, request)\n  end\n\nend\n"
  },
  {
    "path": "spec/logic_not_strategy_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::LogicNot do\n\n  it \"works expectedly as a not operator\" do\n    Trebuchet.feature('pokemon').aim(\n      :logic_not,\n      {\n        users: [30, 35],\n      },\n    )\n\n    t = Trebuchet.new(User.new(30))\n    t.launch?('pokemon').should === false\n    t = Trebuchet.new(User.new(111))\n    t.launch?('pokemon').should === true\n  end\n\nend\n"
  },
  {
    "path": "spec/logic_or_strategy_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::LogicOr do\n\n  it \"passes the check as long as one of the children strategies passes\" do\n    Trebuchet.feature('pokemon').aim(\n      :logic_or,\n      {\n        users: [30, 35],\n        everyone: nil,\n        nobody: nil,\n        percent: 30,\n      },\n    )\n\n    [1, 9, 91, 92, 2016].each do |uid|\n      t = Trebuchet.new(User.new(uid))\n      t.launch?('pokemon').should === true\n    end\n  end\n\n  it \"is not launching if no children strategy works\" do\n    Trebuchet.feature('pokemon').aim(\n      :logic_or,\n      {\n        percent: 0,\n        users: [30, 35],\n      },\n    )\n\n    t = Trebuchet.new(User.new(30))\n    t.launch?('pokemon').should === true\n    t = Trebuchet.new(User.new(111))\n    t.launch?('pokemon').should === false\n  end\n\n  it \"nests well\" do\n    Trebuchet.feature('pokemon').aim(\n      :logic_or,\n      {\n        users: [100],\n        logic_and: {\n          users: [90, 91, 92],\n          logic_not: {\n            users: [90],\n          },\n        },\n      },\n    )\n\n    [91, 92, 100].each do |uid|\n      t = Trebuchet.new(User.new(uid))\n      t.launch?('pokemon').should === true\n    end\n    [1, 3, 5, 7, 90].each do |uid|\n      t = Trebuchet.new(User.new(uid))\n      t.launch?('pokemon').should === false\n    end\n  end\n\nend\n"
  },
  {
    "path": "spec/multiple_strategy_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::Multiple do\n\n  it \"should support chaining strategies\" do\n    Trebuchet.feature('time_machine').aim(:percent, 5).aim(:users, [10, 11])\n    should_launch('time_machine', [31, 36, 10, 11, 197])\n    should_not_launch('time_machine', [49, 71])\n  end\n\n  it \"should always return booleans\" do\n    Trebuchet.feature('time_machine').aim(:percent, 0).aim(:users, [5])\n    t = Trebuchet.new User.new(5)\n    t.launch?('time_machine').should === true\n    t = Trebuchet.new User.new(117)\n    t.launch?('time_machine').should === false\n  end\n\n  it \"should set @feature on sub-strategies\" do\n    feature = Trebuchet.feature('time_machine')\n    feature.aim(:percent, 10).aim(:users, [5])\n    feature.strategy.feature.name == feature.name\n    feature.strategy.strategies.first.feature.name.should == feature.name\n    feature.strategy.strategies.last.feature.name.should == feature.name\n  end\n\n  it \"should pass user and request to each strategy\" do\n    args = [:foo, 1]\n    user = mock \"User\"\n    request = mock \"Request\"\n    strategy = mock \"Strategy\"\n\n    Trebuchet::Strategy.should_receive(:find).with(*args).and_return(strategy)\n    strategy.should_receive(:launch_at?).with(user, request)\n\n    multi = Trebuchet::Strategy::Multiple.new(args)\n    multi.launch_at?(user, request)\n  end\n\n  it \"should always return false for needs_user?\" do\n    s = Trebuchet::Strategy::Multiple.new [:default, nil, :invalid, nil]\n    s.needs_user?.should be_false\n    s = Trebuchet::Strategy::Multiple.new [:default, nil, :percent, 5]\n    s.needs_user?.should be_false\n  end\n\n  it \"should skip needs_user? sub-strategies if user not present\" do\n    s = Trebuchet::Strategy::Multiple.new [:hostname, 'abc', :users, [1,2,3]]\n    s.strategies.first.should_receive(:launch_at?)\n    s.strategies.last.should_not_receive(:launch_at?)\n    s.launch_at?(nil)\n  end\n\nend\n"
  },
  {
    "path": "spec/nobody_strategy_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::Nobody do\n\n  it \"should be named nobody\" do\n    Trebuchet::Strategy::Nobody.strategy_name.should == :nobody\n    Trebuchet.feature('time_machine').aim(:nobody)\n    Trebuchet.feature('time_machine').strategy.name.should == :nobody\n  end\n\n  it \"should always return false\" do\n    Trebuchet.feature('time_machine').aim(:nobody)\n    t = Trebuchet.new User.new(1)\n    t.launch?('time_machine').should === false\n  end\n\n  it \"should return false when missing user\" do\n    Trebuchet.feature('time_machine').aim(:nobody)\n    t = Trebuchet.new nil\n    t.launch?('time_machine').should === false\n  end\n\nend\n"
  },
  {
    "path": "spec/per_denomination_strategy_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::PerDenomination do\n  describe 'launch' do\n    before(:each) do\n      Trebuchet.aim('strategy', :per_denomination, { numerator: 50, denominator: 1000 })\n    end\n\n    it \"should not launch to unsaved users\" do\n      Trebuchet.new(nil).launch?('strategy').should be_false\n    end\n\n    it \"should not launch to users with no IDs\" do\n      Trebuchet.new(User.new(nil)).launch?('strategy').should be_false\n    end\n\n    it \"should launch to the correct per_denomination of users\" do\n      launched_users =\n        (1..10_000).select { |i| Trebuchet.new(User.new(i)).launch?('strategy') }.size\n\n      unlaunched_users =\n        (1..10_000).reject { |i| Trebuchet.new(User.new(i)).launch?('strategy') }.size\n\n      # If you run this to larger N it approaches .95 and 0.05. This is\n      # hash function dependent but it's a nice santity check.\n      launched_users.should be_within(200).of(500)\n      unlaunched_users.should be_within(200).of(9500)\n    end\n  end\n\n  it \"should not yank the feature from users when per_denomination is increased\" do\n    Trebuchet.aim('strategy', :per_denomination, { numerator: 10, denominator: 1000 })\n\n    first_launched_users =\n      (1..10_000).select { |i| Trebuchet.new(User.new(i)).launch?('strategy') }\n\n    Trebuchet.aim('strategy', :per_denomination, { numerator: 50, denominator: 1000 })\n\n    second_launched_users =\n      (1..10_000).select { |i| Trebuchet.new(User.new(i)).launch?('strategy') }\n\n    (second_launched_users & first_launched_users).should eq first_launched_users\n  end\n\n  it \"should distribute launches based on the feature name\" do\n    Trebuchet.aim('strategy1', :per_denomination, { numerator: 50, denominator: 1000 })\n    Trebuchet.aim('strategy2', :per_denomination, { numerator: 50, denominator: 1000 })\n\n    per_denomination1_launched_users =\n      (1..10_000).select { |i| Trebuchet.new(User.new(i)).launch?('strategy1') }\n    per_denomination2_launched_users =\n      (1..10_000).select { |i| Trebuchet.new(User.new(i)).launch?('strategy2') }\n\n    per_denomination1_launched_users.should_not eq per_denomination2_launched_users\n  end\n\n  it \"should always return booleans\" do\n    Trebuchet.aim('strategy', :per_denomination, { numerator: 50, denominator: 1000 })\n    (1..10_000).each do |i|\n      [true, false].should include(Trebuchet.new(User.new(i)).launch?('strategy'))\n    end\n  end\n\n  it \"should handle 0 numerator\" do\n    Trebuchet.aim('strategy', :per_denomination, { numerator: 0, denominator: 1000 })\n    should_not_launch 'strategy', (1..10_000).to_a\n  end\n\n  it \"should fail on 0 denominator\" do\n    Trebuchet.aim('strategy', :per_denomination, { numerator: 1000, denominator: 0 })\n    should_not_launch 'strategy', (1..10_000).to_a # trebuchet silently fails all invalid strategies\n  end\n\n  it \"should handle numerator == denominator\" do\n    Trebuchet.aim('strategy', :per_denomination, { numerator: 1000, denominator: 1000 })\n    should_launch 'strategy', (1..10_000).to_a\n  end\n\n  it \"should fail when numerator > denominator\" do\n    Trebuchet.aim('strategy', :per_denomination, { numerator: 1001, denominator: 1000 })\n    should_not_launch 'strategy', (1..10_000).to_a # trebuchet silently fails all invalid strategies\n  end\n\n  it \"should handle garbage arguments\" do\n    ids = (1..20_000).to_a\n    Trebuchet.feature('strategy').aim(:per_denomination, -1)\n    should_not_launch 'strategy', ids\n    Trebuchet.feature('strategy').aim(:per_denomination, -150)\n    should_not_launch 'strategy', ids\n    Trebuchet.feature('strategy').aim(:per_denomination, 'h')\n    should_not_launch 'strategy', ids\n    Trebuchet.feature('strategy').aim(:per_denomination, 'h')\n    should_not_launch 'strategy', ids\n    Trebuchet.feature('strategy').aim(:per_denomination, [5, 10])\n    should_not_launch 'strategy', ids\n    Trebuchet.feature('strategy').aim(:per_denomination, 20..50)\n    should_not_launch 'strategy', ids\n    Trebuchet.feature('strategy').aim(:per_denomination, nil)\n    should_not_launch 'strategy', ids\n    Trebuchet.feature('strategy').aim(:per_denomination, :from => 7)\n    should_not_launch 'strategy', ids\n    Trebuchet.feature('strategy').aim(:per_denomination, :to => 1)\n    should_not_launch 'strategy', ids\n  end\nend\n\n"
  },
  {
    "path": "spec/percent_deprecated_strategy_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::PercentDeprecated do\n\n  it \"should be deprecated\" do\n    Trebuchet::Feature.with_deprecated_strategies_enabled(false) do\n      expect {\n        Trebuchet.aim('percentage', :percent_deprecated, 5)\n      }.to raise_error(/deprecated/)\n    end\n  end\n\n  it \"should not launch to unsaved users, users with no IDs\" do\n    Trebuchet.aim('percentage', :percent_deprecated, 5)\n    Trebuchet.new(nil).launch?('percentage').should be_false\n    Trebuchet.new(User.new(nil)).launch?('percentage').should be_false\n  end\n\n  def should_only_launch_to_a_percentage_of_users(feature_name)\n    Trebuchet.aim('percentage', feature_name, 5)\n    offset = Trebuchet.feature('percentage').strategy.offset\n    should_launch 'percentage', offset_ids([0, 1, 2, 3, 4, 100, 101, 102, 103, 104], offset)\n    should_not_launch('percentage', [5, 6, 105, 106].map{|i| i - offset})\n  end\n\n  it \"should only launch to a percentage of users\" do\n    should_only_launch_to_a_percentage_of_users(:percent_deprecated)\n  end\n\n  it \"should not yank the feature from users when percentage is increased\" do\n    Trebuchet.aim('percentage', :percent_deprecated, 2)\n    offset = Trebuchet.feature('percentage').strategy.offset\n    should_launch 'percentage', offset_ids([0, 1], offset)\n    should_not_launch 'percentage', offset_ids([2, 3], offset)\n\n    Trebuchet.aim('percentage', :percent_deprecated, 4)\n    should_launch 'percentage', offset_ids([0, 1, 2, 3], offset)\n  end\n\n  it \"should create an offset based on the feature name\" do\n    Trebuchet.aim('percentage', :percent_deprecated, 1)\n    offset = Trebuchet.feature('percentage').strategy.offset\n    user_id = offset_ids([0], offset).first\n    should_launch 'percentage', user_id\n    should_not_launch 'percentage', [user_id - 1]\n    should_not_launch 'percentage', [user_id + 1]\n  end\n\n  it \"should always return booleans\" do\n    Trebuchet.feature('percentage').aim(:percent_deprecated, 1)\n    offset = Trebuchet.feature('percentage').strategy.offset\n    should_launch 'percentage', offset_ids([0], offset)\n    t = Trebuchet.new User.new offset_ids([0], offset).first\n    t.launch?('percentage').should === true\n    t = Trebuchet.new User.new(0 - (offset - 1))\n    t.launch?('percentage').should === false\n  end\n\n\n  def offset_ids(ids, offset)\n    ids.to_a.map {|id| id + offset % 100}\n  end\n\n\n  it \"should launch to the right users with percentage\" do\n    Trebuchet.feature('freedom').aim(:percent_deprecated, :percentage => 50)\n    offset = Trebuchet.feature('freedom').strategy.offset\n    should_launch 'freedom', offset_ids(0..49, offset)\n    should_not_launch 'freedom',  offset_ids(50..99, offset)\n    should_launch 'freedom',  offset_ids(100..149, offset)\n    should_not_launch 'freedom',  offset_ids(150..199, offset)\n    should_launch'freedom',  offset_ids([200, 201], offset)\n    should_not_launch 'freedom',  offset_ids([250, 251], offset)\n  end\n\n  it \"should launch to the right users with wrap\" do\n    Trebuchet.feature('freedom').aim(:percent_deprecated, :from => 99, :to => 1)\n    should_launch 'freedom', [0, 1, 99, 100, 101, 199, 200, 201, 299]\n    should_not_launch 'freedom', [97, 98, 102, 103, 198, 202, 298, 302]\n  end\n\n  it \"should launch to 1%\" do\n    Trebuchet.feature('freedom').aim(:percent_deprecated, :percentage => 1)\n    offset = Trebuchet.feature('freedom').strategy.offset\n    should_launch 'freedom', offset_ids([100, 200, 300, 0], offset)\n    should_not_launch 'freedom', offset_ids([1, 99, 101, 198, 202, 350], offset)\n  end\n\n  it \"should handle 0%\" do\n    Trebuchet.feature('freedom').aim(:percent_deprecated, 0)\n    should_not_launch 'freedom', (1..200).to_a\n  end\n\n  it \"should handle garbage arguments\" do\n    ids = (1..200).to_a\n    Trebuchet.feature('freedom').aim(:percent_deprecated, -1)\n    should_not_launch 'freedom', ids\n    Trebuchet.feature('freedom').aim(:percent_deprecated, -150)\n    should_not_launch 'freedom', ids\n    Trebuchet.feature('freedom').aim(:percent_deprecated, 'h')\n    should_not_launch 'freedom', ids\n    Trebuchet.feature('freedom').aim(:percent_deprecated, 'h')\n    should_not_launch 'freedom', ids\n    Trebuchet.feature('freedom').aim(:percent_deprecated, [5, 10])\n    should_not_launch 'freedom', ids\n    Trebuchet.feature('freedom').aim(:percent_deprecated, 20..50)\n    should_not_launch 'freedom', ids\n    Trebuchet.feature('freedom').aim(:percent_deprecated, nil)\n    should_not_launch 'freedom', ids\n    Trebuchet.feature('freedom').aim(:percent_deprecated, :from => 7)\n    should_not_launch 'freedom', ids\n    Trebuchet.feature('freedom').aim(:percent_deprecated, :to => 1)\n    should_not_launch 'freedom', ids\n  end\n\n  it \"works with from/to > 100\" do\n    # just documenting this side-effect\n    Trebuchet.feature('freedom').aim(:percent_deprecated, :from => 105, :to => 210)\n    should_launch 'freedom', [5, 106, 207, 308, 409, 1210]\n    should_not_launch 'freedom', [4, 99, 111, 150, 215]\n  end\n\nend\n"
  },
  {
    "path": "spec/percent_strategy_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::Percent do\n\n  it \"should not launch to unsaved users, users with no IDs\" do\n    Trebuchet.aim('percentage', :percent, 5)\n    Trebuchet.new(nil).launch?('percentage').should be_false\n    Trebuchet.new(User.new(nil)).launch?('percentage').should be_false\n  end\n\n  it \"should only launch to a percentage of users\" do\n    Trebuchet.aim('percentage', :percent, 5)\n    should_launch 'percentage', [7, 24, 74, 75]\n    should_not_launch('percentage', [47, 48, 49, 76, 78])\n  end\n\n  it \"should launch to the correct percentage of users\" do\n    Trebuchet.aim('percentage', :percent, 5)\n    map = Hash.new {|a,k| a[k] = 0 }\n    n = 20000\n    n.times do |i|\n      map[Trebuchet.new(User.new(i)).launch?('percentage')] +=  1\n    end\n    map.each {|k, v|\n      map[k] = v / n.to_f\n    }\n    # If you run this to larger N it approaches .95 and 0.05. This is\n    # hash function dependent but it's a nice santity check.\n    map[false].should be_within(0.02).of(0.95)\n    map[true].should be_within(0.02).of(0.05)\n  end\n\n  it \"should not yank the feature from users when percentage is increased\" do\n    Trebuchet.aim('percentage', :percent, 2)\n\n    should_launch 'percentage', [7, 74]\n    should_not_launch 'percentage', [24, 147]\n\n    Trebuchet.aim('percentage', :percent, 4)\n    should_launch 'percentage', [7, 74, 24, 147]\n  end\n\n  it \"should distribute launches based on the feature name\" do\n    Trebuchet.aim('percentage1', :percent, 1)\n    Trebuchet.aim('percentage2', :percent, 1)\n    should_launch 'percentage1', [9, 145, 186]\n    should_not_launch 'percentage2', [9, 145, 186]\n    should_launch 'percentage2', [21, 47]\n    should_not_launch 'percentage1', [21, 47]\n  end\n\n  it \"should always return booleans\" do\n    Trebuchet.aim('percentage1', :percent, 1)\n    t = Trebuchet.new(User.new(9))\n    t.launch?('percentage1').should === true\n    t = Trebuchet.new(User.new(21))\n    t.launch?('percentage1').should === false\n  end\n\n  it \"should handle 0%\" do\n    Trebuchet.feature('freedom').aim(:percent, 0)\n    should_not_launch 'freedom', (1..200).to_a\n  end\n\n  it \"should handle garbage arguments\" do\n    ids = (1..200).to_a\n    Trebuchet.feature('freedom').aim(:percent, -1)\n    should_not_launch 'freedom', ids\n    Trebuchet.feature('freedom').aim(:percent, -150)\n    should_not_launch 'freedom', ids\n    Trebuchet.feature('freedom').aim(:percent, 'h')\n    should_not_launch 'freedom', ids\n    Trebuchet.feature('freedom').aim(:percent, 'h')\n    should_not_launch 'freedom', ids\n    Trebuchet.feature('freedom').aim(:percent, [5, 10])\n    should_not_launch 'freedom', ids\n    Trebuchet.feature('freedom').aim(:percent, 20..50)\n    should_not_launch 'freedom', ids\n    Trebuchet.feature('freedom').aim(:percent, nil)\n    should_not_launch 'freedom', ids\n    Trebuchet.feature('freedom').aim(:percent, :from => 7)\n    should_not_launch 'freedom', ids\n    Trebuchet.feature('freedom').aim(:percent, :to => 1)\n    should_not_launch 'freedom', ids\n  end\n\nend\n\n"
  },
  {
    "path": "spec/redis_backend_spec.rb",
    "content": "require 'spec_helper'\nrequire 'mock_redis'\nrequire 'trebuchet/backend/redis'\n\n\ndescribe Trebuchet::Backend::Redis do\n\n  before(:all) do\n    @backend = Trebuchet.backend\n  end\n\n  before(:each) do\n    Trebuchet.set_backend :disabled\n  end\n\n  it \"should set backend to redis with defaults\" do\n    Trebuchet.backend = :redis\n    Trebuchet.backend.should be_a(Trebuchet::Backend::Redis)\n    Trebuchet.set_backend :redis\n    Trebuchet.backend.should be_a(Trebuchet::Backend::Redis)\n  end\n  \n  it \"should set redis client if passed in\" do\n    r = Redis.new\n    Redis.stub!(:new).and_return(nil)\n    lambda {Trebuchet.set_backend :redis}.should raise_error Trebuchet::BackendInitializationError\n    Trebuchet.backend.should be_a(Trebuchet::Backend::Disabled)\n    Trebuchet.backend.instance_variable_get(:@redis).should be_nil\n    Trebuchet.set_backend :redis, :client => r\n    Trebuchet.backend.instance_variable_get(:@redis).should eql r\n  end\n  \n  it \"should pass arguments to Redis.new\" do\n    r = Redis.new\n    Redis.should_receive(:new).with(:this => 'that', :other => true).and_return(r)\n    Trebuchet.set_backend :redis, :this => 'that', :other => true\n    args = [:seven, {8 => 'nine'}]\n    Redis.should_receive(:new).with(*args).and_return(r)\n    Trebuchet.set_backend :redis, *args\n  end\n  \n  after(:all) do\n    # cleanup\n    Trebuchet.backend = @backend\n  end\n \n\nend\n"
  },
  {
    "path": "spec/redis_hammerspaced_spec.rb",
    "content": "require 'spec_helper'\nrequire 'mock_redis'\nrequire 'trebuchet/backend/redis_hammerspaced'\n\n\ndescribe Trebuchet::Backend::RedisHammerspaced do\n\n  before(:all) do\n    @backend = Trebuchet.backend\n  end\n\n  before(:each) do\n    Trebuchet.set_backend :disabled\n  end\n\n  it \"should properly get empty feature names and strategies\" do\n    r = Redis.new\n    Redis.stub!(:new).and_return(nil)\n    Trebuchet.backend.should be_a(Trebuchet::Backend::Disabled)\n    Trebuchet.backend.instance_variable_get(:@redis).should be_nil\n    Trebuchet.set_backend :redis_hammerspaced,\n                          :client => r,\n                          :hammerspace => {},\n                          :skip_check => true\n    Trebuchet.backend.get_feature_names.should eq []\n    Trebuchet.backend.get_strategy(\"adfd\").should be_nil\n  end\n\n  it \"should properly get feature name list and non-empty strategies\" do\n    r = Redis.new\n    Redis.stub!(:new).and_return(nil)\n    Trebuchet.backend.should be_a(Trebuchet::Backend::Disabled)\n    Trebuchet.backend.instance_variable_get(:@redis).should be_nil\n    Trebuchet.set_backend :redis_hammerspaced,\n                          :client => r,\n                          :hammerspace => {\n                            \"trebuchet/feature-names\" => [\"foo\", \"bar\"].to_s, # stringified array\n                            \"trebuchet/features/foo\" => {\n                              \"everyone\" => [nil],\n                              \"users\" => [[1, 2, 3]],\n                            }.to_json, # stringified json\n                          },\n                          :skip_check => true\n    Trebuchet.backend.get_feature_names.should eq [\"foo\", \"bar\"]\n    Trebuchet.backend.get_strategy(\"foo\").should eq [:everyone, nil, :users, [1, 2, 3]]\n  end\n\n  it \"should properly invalidate local cache\" do\n    r = Redis.new\n    Redis.stub!(:new).and_return(nil)\n    hammerspace = {\n      \"trebuchet/feature-names\" => [\"foo\", \"bar\"].to_s,\n      \"trebuchet/features/foo\" => {\n        \"everyone\" => [nil],\n        \"users\" => [[1, 2, 3]],\n      }.to_json\n    }\n    def hammerspace.uid\n      @uid ||= Time.now().to_i\n      @uid = @uid + 1\n    end\n    def hammerspace.close\n    end\n    Trebuchet.set_backend :redis_hammerspaced,\n                          :client => r,\n                          :hammerspace => hammerspace,\n                          :skip_check => true\n    # Force to load strategy to local cache\n    Trebuchet.backend.get_strategy(\"foo\")\n    hammerspace[\"trebuchet/features/foo\"] = {\n      \"everyone\" => [nil],\n      \"users\" => [[1, 2]],\n    }.to_json\n    Trebuchet.backend.get_strategy(\"foo\").should eq [:everyone, nil, :users, [1, 2, 3]]\n    # after refresh we should have the up-to-date strategy\n    Trebuchet.backend.refresh\n    Trebuchet.backend.get_strategy(\"foo\").should eq [:everyone, nil, :users, [1, 2]]\n  end\n\n  it \"should properly generate hammerspace hash\" do\n    r = Redis.new\n    Redis.stub!(:new).and_return(nil)\n    Trebuchet.set_backend :redis_hammerspaced,\n                          :client => r,\n                          :hammerspace => {},\n                          :skip_check => true\n    feature_names = [\"foo\", \"bar\"]\n    features = [\n      [\"everyone\", \"[null]\", \"user\", \"[[1,2,3]]\"], # foo\n      [\"visitor_experiment\", \"[{\\\"name\\\":\\\"rate\\\",\\\"total_buckets\\\":5,\\\"bucket\\\":1}]\"] # bar\n    ]\n    foo_string = {\n      \"everyone\" => [nil],\n      \"user\" => [[1,2,3]],\n    }.to_json\n    bar_string = {\n      \"visitor_experiment\" => [\n        {\n          \"name\" => \"rate\",\n          \"total_buckets\" => 5,\n          \"bucket\" => 1,\n        }\n      ],\n    }.to_json\n    last_updated = Time.now.to_i\n    expected_hash = {\n      \"trebuchet/feature-names\" => feature_names.to_json,\n      \"trebuchet/features/foo\" => foo_string,\n      \"trebuchet/features/bar\" => bar_string,\n      \"trebuchet/last_updated\" => last_updated.to_s,\n    }\n    Trebuchet.backend.generate_hammerspace_hash(\n      feature_names,\n      features,\n      last_updated\n    ).should eq expected_hash\n  end\n\n  after(:all) do\n    # cleanup\n    Trebuchet.backend = @backend\n  end\n\nend\n"
  },
  {
    "path": "spec/spec_helper.rb",
    "content": "$:.unshift File.dirname(__FILE__) + '/../lib'\n\nrequire 'bundler'\n\n# Bundler breaks things\nBundler.require :default, :test\n\nrequire 'mock_redis'\nclass Redis < MockRedis ; end\n\n\nrequire 'trebuchet'\nrequire 'user'\n\nRSpec.configure do |config|\n  config.around(:each) { |ex|\n    Trebuchet::Feature.with_deprecated_strategies_enabled(&ex)\n  }\nend\n\n# # uncomment to run specs against Redis backend instead of Memory backend\n\n# require 'redis'\n# Trebuchet.set_backend :redis, Redis.new(:host => '127.0.0.1', :port => 6379)\n\ndef should_launch(feature, users)\n  should_or_should_not_launch(feature, users, be_true)\nend\n\ndef should_not_launch(feature, users)\n  should_or_should_not_launch(feature, users, be_false)\nend\n\ndef should_or_should_not_launch(feature, users, be_true_or_false)\n  Array(users).each do |user_or_user_id|\n    user = user_or_user_id.is_a?(User) ? user_or_user_id : User.new(user_or_user_id)\n    Trebuchet.new(user).launch?(feature).should be_true_or_false\n  end\nend\n\ndef mock_request(cookie = nil)\n  mock 'Request', :cookies => {:visitor => cookie}\nend\n"
  },
  {
    "path": "spec/stubbing_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe Trebuchet::Feature::Stubbing do\n\n  before do\n    Trebuchet.dismantle_stubs\n  end\n\n  describe '#stub' do\n\n    it \"should stub a feature as launched\" do\n      Trebuchet.feature('test').stub(:launched)\n      should_launch('test', [0])\n    end\n\n    it \"should stub a feature as not launched\" do\n      Trebuchet.feature('test').stub(:not_launched)\n      should_not_launch('test', [0])\n    end\n\n    it \"should employ the stub strategy for features stubbed launched\" do\n      Trebuchet.feature('test').stub(:launched)\n      Trebuchet.feature('test').strategy.is_a?(Trebuchet::Strategy::Stub)\n    end\n\n    it \"should employ the stub strategy for features stubbed not launched\" do\n      Trebuchet.feature('test').stub(:not_launched)\n      Trebuchet.feature('test').strategy.is_a?(Trebuchet::Strategy::Stub)\n    end\n\n  end\n\n  describe '#stubbed?' do\n\n    it \"should report when features are stubbed launched\" do\n      Trebuchet.feature('test').stub(:launched)\n      Trebuchet.feature('test').should be_stubbed\n    end\n\n    it \"should report when features are stubbed not launched\" do\n      Trebuchet.feature('test').stub(:not_launched)\n      Trebuchet.feature('test').should be_stubbed\n    end\n\n  end\n\n  describe '#dismantle_stubs' do\n\n    it \"should reset stubbed features\" do\n      Trebuchet.feature('test').stub(:launched)\n      Trebuchet.dismantle_stubs\n      Trebuchet::Feature.stubbed_features.should be_empty\n    end\n\n    it \"should restore them to their default state\" do\n      Trebuchet.feature('test').stub(:launched)\n      Trebuchet.dismantle_stubs\n      should_not_launch('test', [0])\n    end\n\n  end\n\n  describe '#stubbed_features' do\n\n    it \"should report when feature are stubbed\" do\n      Trebuchet.feature('test').stub(:launched)\n      Trebuchet::Feature.stubbed_features.count.should == 1\n    end\n\n    it \"should report when features are stubbed launched\" do\n      Trebuchet.feature('test').stub(:launched)\n      Trebuchet::Feature.stubbed_features['test'].should == :launched\n    end\n\n    it \"should report when features are stubbed not launched\" do\n      Trebuchet.feature('test').stub(:not_launched)\n      Trebuchet::Feature.stubbed_features['test'].should == :not_launched\n    end\n\n  end\n\nend"
  },
  {
    "path": "spec/trebuchet_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe Trebuchet do\n\n  describe \"launch?\" do\n\n    it \"should call launch_at? on feature\" do\n      Trebuchet::Feature.any_instance.should_receive(:launch_at?).once\n      Trebuchet.new(User.new(1)).launch?('highly_experimental')\n    end\n\n    it \"should  call launch_at? on feature even if missing user\" do\n      Trebuchet::Feature.any_instance.should_receive(:launch_at?).once\n      Trebuchet.new(nil).launch?('highly_experimental')\n    end\n\n    it \"caches value of launch_at?\" do\n      t = Trebuchet.new(nil)\n      Trebuchet.feature('highly_experimental').should_receive(:launch_at?).once\n      Trebuchet.feature('waste_of_time').should_receive(:launch_at?).once\n\n      t.launch?('highly_experimental')\n      t.launch?('waste_of_time')\n\n      t.launch?('highly_experimental')\n      t.launch?('waste_of_time')\n    end\n  end\n\n  describe \"launch\" do\n    it \"should execute a block\" do\n      times = 0\n      Trebuchet.aim('highly_experimental', :users, [1,2])\n      (Trebuchet.new(User.new(1)).launch('highly_experimental') { times += 1 }).should be_true\n      (Trebuchet.new(User.new(3)).launch('highly_experimental') { times += 1 }).should be_false\n      times.should == 1\n    end\n\n    it \"should not blow up if block is missing\" do\n      lambda do\n        Trebuchet.aim('highly_experimental', :users, [1,2])\n        Trebuchet.new(User.new(1)).launch('highly_experimental').should be_true\n        Trebuchet.new(User.new(3)).launch('highly_experimental').should be_false\n        Trebuchet.new(nil).launch('highly_experimental').should be_false\n      end.should_not raise_error(LocalJumpError)\n\n    end\n\n  end\n\n  describe \"logging\" do\n\n    before(:all) do\n      Trebuchet.aim('highly_experimental', :users, [1,2])\n      Trebuchet.aim('disused', :disabled)\n    end\n\n    before(:each) do\n      Trebuchet.initialize_logs\n    end\n\n    it \"should log\" do\n      Trebuchet.logs.should == {}\n      Trebuchet.new(User.new(1)).launch?('highly_experimental').should == true\n      Trebuchet.logs['highly_experimental'].should == true\n    end\n\n    it \"should log false/nil\" do\n      Trebuchet.logs['complely_fabricated'] == nil\n      Trebuchet.logs['disused'].should == nil\n      Trebuchet.new(User.new(1)).launch?('disused') #.should == false\n      Trebuchet.logs['disused'].should == false\n    end\n\n    it \"it should clear logs\" do\n      Trebuchet.new(User.new(1)).launch?('highly_experimental').should == true\n      Trebuchet.logs['highly_experimental'].should == true\n      Trebuchet.initialize_logs\n      Trebuchet.logs.should == {}\n    end\n\n    it \"should log from multiple trebuchet instances\" do\n      Trebuchet.new(User.new(1)).launch?('highly_experimental') #.should == true\n      Trebuchet.new(User.new(1)).launch?('disused') #.should == false\n      Trebuchet.new(nil).launch?('waste_of_time') #.should == false\n      Trebuchet.logs['highly_experimental'].should == true\n      Trebuchet.logs['disused'].should == false\n      Trebuchet.logs['waste_of_time'].should == false\n    end\n\n  end\n\n  describe \"exception handling\" do\n\n    before :all do\n      class BoomError < StandardError ; end\n      Trebuchet.define_strategy(:boom) do\n        raise BoomError.new \"BOOM!\"\n      end\n      Trebuchet.aim('optimism', :boom)\n    end\n\n    it \"should swallow exceptions if no exception_handler defined\" do\n      expect { Trebuchet.current.launch?(\"optimism\") }.to_not raise_exception\n    end\n\n    it \"should invoke exception_handler if defined\" do\n      @feature = nil\n      @exception = nil\n      Trebuchet.exception_handler = lambda { |e, f, t| @exception = e; @feature = f }\n      Trebuchet.current.launch?(\"optimism\")\n      @exception.should_not be_nil\n      @feature.should == 'optimism'\n    end\n\n    it \"should allow exception_handler to raise the exception\" do\n      Trebuchet.exception_handler = lambda { |e, f, t| raise e } # useful in development\n      expect { Trebuchet.current.launch?(\"optimism\") }.to raise_exception(BoomError)\n    end\n\n    it \"should accept 0 to 3 arguments\" do\n      Trebuchet.exception_handler = lambda { @last_arg = eval local_variables.last.to_s }\n      Trebuchet.current.launch?(\"optimism\")\n      @last_arg.should == nil\n\n      Trebuchet.exception_handler = lambda { |e| @last_arg = eval local_variables.last.to_s  }\n      Trebuchet.current.launch?(\"optimism\")\n      @last_arg.should be_a(StandardError)\n\n      Trebuchet.exception_handler = lambda { |e, f| @last_arg = eval local_variables.last.to_s  }\n      Trebuchet.current.launch?(\"optimism\")\n      @last_arg.should == \"optimism\"\n\n      Trebuchet.exception_handler = lambda { |e, f, t| @last_arg = eval local_variables.last.to_s  }\n      Trebuchet.current.launch?(\"optimism\")\n      @last_arg.should be_a(Trebuchet)\n\n      Trebuchet.exception_handler = lambda { |*args| @args = args }\n      Trebuchet.current.launch?(\"optimism\")\n      @args.size.should == 3\n      @args.last.should be_a(Trebuchet)\n    end\n\n    it \"should not blow up if exception_handler is not a proc\" do\n      Trebuchet.exception_handler = \"one of my shoes\"\n      expect { Trebuchet.current.launch?(\"optimism\") }.to_not raise_exception\n\n      Trebuchet.exception_handler = false\n      expect { Trebuchet.current.launch?(\"optimism\") }.to_not raise_exception\n\n      Trebuchet.exception_handler = true\n      expect { Trebuchet.current.launch?(\"optimism\") }.to_not raise_exception\n\n      Trebuchet.exception_handler = Trebuchet.current\n      expect { Trebuchet.current.launch?(\"optimism\") }.to_not raise_exception\n    end\n\n  end\n\n  it \"stores state variables\" do\n    %i{author admin_view admin_edit time_zone}.each do |key|\n      begin\n        Trebuchet.public_send :\"#{key}=\", 1\n        expect(Trebuchet.public_send(key)).to eq 1\n      ensure\n        Trebuchet.public_send :\"#{key}=\", nil\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "spec/user.rb",
    "content": "class User < Struct.new(:id, :role)\n\n  def has_role?(role)\n    self.role == role\n  end\n\nend\n"
  },
  {
    "path": "spec/user_id_strategy_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::UserId do\n\n  it \"should only launch to designated users\" do\n    Trebuchet.aim('highly_experimental', :users, [1, 2])\n    yes = [1, 2]\n    no  = [3, 4]\n\n    yes.each do |n|\n      Trebuchet.new(User.new(n)).launch?('highly_experimental').should be_true\n    end\n\n    no.each do |n|\n      Trebuchet.new(User.new(n)).launch?('highly_experimental').should be_false\n    end\n  end\n  \n  it \"should always return booleans\" do\n    Trebuchet.feature('time_machine').aim(:users, [1])\n    t = Trebuchet.new User.new(1)\n    t.launch?('time_machine').should === true\n    t = Trebuchet.new User.new(117)\n    t.launch?('time_machine').should === false\n  end\n\n  # this behavior should be deprecated -- causes problems with augment\n  it \"should not break if one id is passed instead of an array\" do\n    Trebuchet.feature('time_machine').aim(:users, 1)\n    t = Trebuchet.new User.new(1)\n    t.launch?('time_machine').should === true\n  end\n  \n  it \"should not break on missing user\" do\n    Trebuchet.feature(\"the chosen ones\").aim(:users, [1,2,3])\n    t = Trebuchet.new User.new(nil)\n    t.launch?(\"the chosen ones\").should be_false\n    t = Trebuchet.new nil\n    t.launch?(\"the chosen ones\").should be_false\n  end\n\nend\n"
  },
  {
    "path": "spec/visitor_experiment_strategy_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::VisitorExperiment do\n  \n  before do\n    @feature_name = \"Infrared Vision\"\n    @feature_name2 = \"Alligator Tail\"\n    @experiment_name = \"Superhumanity\"\n    @user = User.new(0)\n    @mock_request = mock_request('abcdef')\n    @trebuchet = Trebuchet.new(@user, @mock_request)\n  end\n\n  it \"should match a user in a bucket\" do\n    Trebuchet.aim(@feature_name, :visitor_experiment, :name => @experiment_name, :bucket => 1)\n    should_not_launch(@feature_name, (1..50).to_a) # should never launch without a request\n    # these values just happen to hash for the algorithm and experiment name\n    positive = [5, 14, 15, 198, 200, 549]\n    negative = [1, 2, 25, 550]\n    positive.each do |i|\n      Trebuchet.visitor_id = i\n      Trebuchet.feature(@feature_name).launch_at?(@user, @mock_request).should be_true\n    end\n    negative.each do |i|\n      Trebuchet.visitor_id = i\n      Trebuchet.feature(@feature_name).launch_at?(@user, @mock_request).should be_false\n    end\n  end\n  \n  it \"should launch nil request to no bucket\" do\n    Trebuchet.aim(@feature_name, :visitor_experiment, :name => @experiment_name, :total_buckets => 2, :bucket => 1)\n    Trebuchet.aim(@feature_name2, :visitor_experiment, :name => @experiment_name, :total_buckets => 2, :bucket => 2)\n    Trebuchet.visitor_id = nil\n    @trebuchet.launch?(@feature_name).should be_false\n    @trebuchet.launch?(@feature_name2).should be_false\n  end\n  \n\nend\n"
  },
  {
    "path": "spec/visitor_percent_deprecated_strategy_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::VisitorPercentDeprecated do\n\n  it \"should be deprecated\" do\n    Trebuchet::Feature.with_deprecated_strategies_enabled(false) do\n      expect {\n        Trebuchet.aim('some_feature', :visitor_percent_deprecated, 5)\n      }.to raise_error(/deprecated/)\n    end\n  end\n\n  it \"should not break if no visitor id is set\" do\n    Trebuchet.aim('some_feature', :visitor_percent_deprecated, 100)\n    t = Trebuchet.new(User.new(0))\n    t.launch?('some_feature').should == false\n  end\n\n  it \"should require a request\" do\n    Trebuchet.visitor_id = 1\n    Trebuchet.aim('some_feature', :visitor_percent_deprecated, 100)\n    t = Trebuchet.new(User.new(1), nil)\n    t.launch?('some_feature').should == false\n  end\n\n  it \"should not require a user\" do\n    Trebuchet.visitor_id = 1\n    Trebuchet.aim('some_feature', :visitor_percent_deprecated, 100)\n    t = Trebuchet.new(nil, mock_request('12345'))\n    t.launch?('some_feature').should == true\n  end\n\n  describe 'visitor id integer' do\n    before do\n      Trebuchet.visitor_id = 123\n    end\n\n    def should_launch_test(feature_name)\n      # offset of some_feature is 33\n      Trebuchet.aim('some_feature', feature_name, 100)\n      offset = Trebuchet.feature('some_feature').strategy.offset\n      user = User.new(0)\n      request = mock_request('12345')\n      Trebuchet.feature('some_feature').launch_at?(user, request).should == true\n      visitor_id = Trebuchet.visitor_id.call\n\n      Trebuchet.aim('some_feature', feature_name, 91) # 33 + 91 includes 123 % 100\n      Trebuchet.feature('some_feature').launch_at?(user, request).should == true\n\n      Trebuchet.aim('some_feature', feature_name, 90)\n      Trebuchet.feature('some_feature').launch_at?(user, request).should == false\n    end\n\n    it 'should launch' do\n      should_launch_test(:visitor_percent_deprecated)\n    end\n  end\n\n  describe 'visitor id proc' do\n    before do\n      Trebuchet.visitor_id = proc { |request| request && request.cookies[:visitor] && request.cookies[:visitor].hash }\n    end\n\n    it 'should not launch if no request is present' do\n      Trebuchet.aim('some_feature', :visitor_percent_deprecated, 100)\n      should_not_launch('some_feature', [1000])\n    end\n\n    it 'should launch to a valid session' do\n      Trebuchet.aim('some_feature', :visitor_percent_deprecated, 100)\n      t = Trebuchet.new(User.new(0), mock_request('abcdef'))\n      t.launch?('some_feature').should == true\n    end\n\n    it 'should not launch to a nil session ID' do\n      Trebuchet.aim('some_feature', :visitor_percent_deprecated, 100)\n      t = Trebuchet.new(User.new(0), mock_request(nil))\n      t.launch?('some_feature').should == false\n    end\n  end\n\n  describe 'visitor id invalid' do\n    it \"should handle nil\" do\n      Trebuchet.visitor_id = nil\n      Trebuchet.aim('some_feature', :visitor_percent_deprecated, 100)\n      t = Trebuchet.new(User.new(0), mock_request('abcdef'))\n      t.launch?('some_feature').should == false\n    end\n  end\n\n  describe 'percentable' do\n\n    before do\n      @feature = Trebuchet.feature(\"liberty\")\n      @user = User.new(0)\n      @request = mock_request('abcdef')\n      @trebuchet = Trebuchet.new(@user, @request)\n    end\n\n    it \"should use from and to\" do\n      @feature.aim(:visitor_percent_deprecated, :from => 5, :to => 10)\n      Trebuchet.feature(\"liberty\").strategy.offset.should == 0\n      Trebuchet.visitor_id = 10\n      Trebuchet.feature(\"liberty\").launch_at?(@user, @request).should == true\n      Trebuchet.visitor_id = 5\n      Trebuchet.feature(\"liberty\").launch_at?(@user, @request).should == true\n      Trebuchet.visitor_id = 4\n      Trebuchet.feature(\"liberty\").launch_at?(@user, @request).should == false\n      Trebuchet.visitor_id = 11\n      Trebuchet.feature(\"liberty\").launch_at?(@user, @request).should == false\n    end\n\n    it \"should use a percentage\" do\n      @feature.aim(:visitor_percent_deprecated, :percentage => 25)\n      offset = @feature.strategy.offset\n      offset.should == 90\n      Trebuchet.visitor_id = 24 + offset\n      Trebuchet.feature(\"liberty\").launch_at?(@user, @request).should == true\n      Trebuchet.visitor_id = 0 + offset\n      Trebuchet.feature(\"liberty\").launch_at?(@user, @request).should == true\n      Trebuchet.visitor_id = 5 + offset\n      Trebuchet.feature(\"liberty\").launch_at?(@user, @request).should == true\n      Trebuchet.visitor_id = 25 + offset\n      Trebuchet.feature(\"liberty\").launch_at?(@user, @request).should == false\n    end\n\n  end\n\nend\n"
  },
  {
    "path": "spec/visitor_percent_strategy_spec.rb",
    "content": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::VisitorPercent do\n\n  it \"should not break if no visitor id is set\" do\n    Trebuchet.aim('some_feature', :visitor_percent, 100)\n    t = Trebuchet.new(User.new(0))\n    t.launch?('some_feature').should == false\n  end\n\n  it \"should require a request\" do\n    Trebuchet.visitor_id = 1\n    Trebuchet.aim('some_feature', :visitor_percent, 100)\n    t = Trebuchet.new(User.new(1), nil)\n    t.launch?('some_feature').should == false\n  end\n\n  it \"should not require a user\" do\n    Trebuchet.visitor_id = 1\n    Trebuchet.aim('some_feature', :visitor_percent, 100)\n    t = Trebuchet.new(nil, mock_request('12345'))\n    t.launch?('some_feature').should == true\n  end\n\n  describe 'visitor id integer' do\n    before do\n      Trebuchet.visitor_id = 123\n    end\n\n    def should_launch_test(feature_name)\n    end\n\n    it 'should launch' do\n      Trebuchet.aim('some_feature', :visitor_percent, 100)\n      user = User.new(0)\n      request = mock_request('12345')\n\n      Trebuchet.feature('some_feature').launch_at?(user, request).should == true\n      visitor_id = Trebuchet.visitor_id.call\n\n      Trebuchet.aim('some_feature', :visitor_percent, 91)\n      Trebuchet.feature('some_feature').launch_at?(user, request).should == true\n\n      Trebuchet.aim('some_feature', :visitor_percent, 10)\n      Trebuchet.feature('some_feature').launch_at?(user, request).should == false\n    end\n  end\n\n  describe 'visitor id proc' do\n    before do\n      Trebuchet.visitor_id = proc { |request| request && request.cookies[:visitor] && request.cookies[:visitor].hash }\n    end\n\n    it 'should not launch if no request is present' do\n      Trebuchet.aim('some_feature', :visitor_percent, 100)\n      should_not_launch('some_feature', [1000])\n    end\n\n    it 'should launch to a valid session' do\n      Trebuchet.aim('some_feature', :visitor_percent, 100)\n      t = Trebuchet.new(User.new(0), mock_request('abcdef'))\n      t.launch?('some_feature').should == true\n    end\n\n    it 'should not launch to a nil session ID' do\n      Trebuchet.aim('some_feature', :visitor_percent, 100)\n      t = Trebuchet.new(User.new(0), mock_request(nil))\n      t.launch?('some_feature').should == false\n    end\n  end\n\n  describe 'visitor id invalid' do\n    it \"should handle nil\" do\n      Trebuchet.visitor_id = nil\n      Trebuchet.aim('some_feature', :visitor_percent, 100)\n      t = Trebuchet.new(User.new(0), mock_request('abcdef'))\n      t.launch?('some_feature').should == false\n    end\n  end\nend\n\n"
  },
  {
    "path": "trebuchet.gemspec",
    "content": "# -*- encoding: utf-8 -*-\n$:.push File.expand_path(\"../lib\", __FILE__)\nrequire 'trebuchet'\n\nGem::Specification.new do |s|\n  s.name        = \"trebuchet\"\n  s.version     = Trebuchet::VERSION\n  s.platform    = Gem::Platform::RUBY\n  s.authors     = [\"Justin Jones\", \"Tobi Knaup\", \"Ross Allen\"]\n  s.email       = [\"justin@airbnb.com\"]\n  s.homepage    = \"http://www.airbnb.com\"\n  s.summary     = %q{Trebuchet launches features at people}\n  s.description = %q{Wisely choose a strategy, aim, and launch!}\n\n  s.files         = `git ls-files`.split(\"\\n\")\n  s.test_files    = `git ls-files -- {test,spec,features}/*`.split(\"\\n\")\n  s.executables   = `git ls-files -- bin/*`.split(\"\\n\").map{ |f| File.basename(f) }\n  s.require_paths = [\"lib\"]\n\n  # redis and memcache are optional\n  s.add_dependency 'json'\n\n  s.add_development_dependency 'rspec', '~> 2.12.0'\n  s.add_development_dependency 'mock_redis', '~> 0.6.5'\n  s.add_development_dependency 'rake', '>= 10.0.3'\nend\n"
  }
]