Full Code of airbnb/trebuchet for AI

master d5cbd014fa6a cached
81 files
112.8 KB
33.7k tokens
309 symbols
1 requests
Download .txt
Repository: airbnb/trebuchet
Branch: master
Commit: d5cbd014fa6a
Files: 81
Total size: 112.8 KB

Directory structure:
gitextract_xbvggvpr/

├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── Gemfile
├── README.md
├── Rakefile
├── app/
│   ├── controllers/
│   │   └── trebuchet_rails/
│   │       └── features_controller.rb
│   ├── helpers/
│   │   └── trebuchet_helper.rb
│   └── views/
│       ├── layouts/
│       │   └── trebuchet.html.erb
│       └── trebuchet_rails/
│           ├── features/
│           │   ├── index.html.erb
│           │   └── timeline.html.erb
│           └── trebuchet.css
├── config/
│   └── routes.rb
├── init.rb
├── lib/
│   ├── trebuchet/
│   │   ├── action_controller.rb
│   │   ├── action_controller_filter.rb
│   │   ├── backend/
│   │   │   ├── disabled.rb
│   │   │   ├── memcached.rb
│   │   │   ├── memory.rb
│   │   │   ├── redis.rb
│   │   │   ├── redis_cached.rb
│   │   │   └── redis_hammerspaced.rb
│   │   ├── backend.rb
│   │   ├── error.rb
│   │   ├── feature/
│   │   │   └── stubbing.rb
│   │   ├── feature.rb
│   │   ├── state.rb
│   │   ├── strategy/
│   │   │   ├── base.rb
│   │   │   ├── custom.rb
│   │   │   ├── custom_request_aware.rb
│   │   │   ├── default.rb
│   │   │   ├── everyone.rb
│   │   │   ├── experiment.rb
│   │   │   ├── hostname.rb
│   │   │   ├── invalid.rb
│   │   │   ├── logic_and.rb
│   │   │   ├── logic_base.rb
│   │   │   ├── logic_not.rb
│   │   │   ├── logic_or.rb
│   │   │   ├── multiple.rb
│   │   │   ├── nobody.rb
│   │   │   ├── per_denomination.rb
│   │   │   ├── percent.rb
│   │   │   ├── percent_deprecated.rb
│   │   │   ├── stub.rb
│   │   │   ├── user_id.rb
│   │   │   ├── visitor_experiment.rb
│   │   │   ├── visitor_percent.rb
│   │   │   └── visitor_percent_deprecated.rb
│   │   ├── strategy.rb
│   │   └── version.rb
│   ├── trebuchet.rb
│   ├── trebuchet_rails/
│   │   └── engine.rb
│   └── trebuchet_rails.rb
├── spec/
│   ├── custom_request_aware_strategy_spec.rb
│   ├── custom_strategy_spec.rb
│   ├── default_strategy_spec.rb
│   ├── disabled_backend_spec.rb
│   ├── everyone_strategy_spec.rb
│   ├── experiment_strategy_spec.rb
│   ├── feature_spec.rb
│   ├── logic_and_strategy_spec.rb
│   ├── logic_base_strategy_spec.rb
│   ├── logic_not_strategy_spec.rb
│   ├── logic_or_strategy_spec.rb
│   ├── multiple_strategy_spec.rb
│   ├── nobody_strategy_spec.rb
│   ├── per_denomination_strategy_spec.rb
│   ├── percent_deprecated_strategy_spec.rb
│   ├── percent_strategy_spec.rb
│   ├── redis_backend_spec.rb
│   ├── redis_hammerspaced_spec.rb
│   ├── spec_helper.rb
│   ├── stubbing_spec.rb
│   ├── trebuchet_spec.rb
│   ├── user.rb
│   ├── user_id_strategy_spec.rb
│   ├── visitor_experiment_strategy_spec.rb
│   ├── visitor_percent_deprecated_strategy_spec.rb
│   └── visitor_percent_strategy_spec.rb
└── trebuchet.gemspec

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
*.gem
.bundle
Gemfile.lock
pkg/*
.idea
.rspec


================================================
FILE: .travis.yml
================================================
language: ruby
cache: bundler
sudo: false
rvm:
  - 1.9.3
  - 2.1
  - 2.2
  - 2.3
  - 2.4
  - 2.5
  - 2.6

before_install:
  - gem install bundler -v 1.17.3


================================================
FILE: CHANGELOG.md
================================================
## 0.12.0 (Nov 26, 2019)
 - Contain fiber-unsafe state of the main Trebuchet class into a state object and optionally store it as a thread/fiber local.

## 0.11.0 (Aug 15, 2019)
 - add Rails 5 compatibility

## 0.10.1 (Jul 13, 2018)
 - add PerDenomination strategy

## 0.10.0 (Nov 17, 2017)
  - Cache Trebuchet#launch?

## 0.9.18 (Sep 18, 2017)
  - Do not expose internal format

## 0.9.17 (Aug 15, 2017)
  - adds CustomRequestAware strategy

## 0.9.16 (Aug 9, 2017)
  - Don't raise an error if expiration date has not been previously set

## 0.9.15 (Aug 9, 2017)
  - Return payload for expiration date accessor

## 0.9.14 (Aug 9, 2017)
  - Expose expiration date accessor

## 0.9.13 (Aug 8, 2017)
  - Fix a typo

## 0.9.12 (Aug 3, 2017)
  - Add setter for expiration date

## 0.9.11 (Nov 16, 2016)
  - minor change: reduce number of times backend is hit in the launch methods

## 0.9.10 (Nov 14, 2016)
  - fix logical operators for logged out visitors

## 0.9.9 (Sep 15, 2016)
  - minor change: keep raw option hash in logic strategies for editing purpose

## 0.9.8 (July 27, 2016)
  - convert deserialized strategy names to symbols

## 0.9.7 (July 20, 2016)
  - logic operation strategies, e.g. AND, OR, and NOT

## 0.9.6 (April 5, 2016)
  - add :add_comment method to the Feature class

## 0.9.2 (October 11, 2015)
  - add :nobody and :everybody strategies
  - cache features
  - make :default strategy a singleton

## 0.9.1 (Jul 22, 2015)
  - fix last_update type in redis_hammerspaced

## 0.9.0 (Jul 22, 2015)
  - adds update_hammerspace method so we can sync redis into local hammerspace

## 0.8.1 (Jul 21, 2015)
  - update redis backend to update a sentinel to indicate modifications

## 0.8.0 (Jul 20, 2015)
  - adds a new backened redis_hammerspaced

## 0.6.3 (Sep 7, 2013)
  - reimplements the percent strategy
  - adds per-feature stubbing interface

## 0.0.4 (Dec 6, 2011)
  - add experiment strategy
  - fix percentage offset
  - fix Redis/Memcache dependency issue


================================================
FILE: Gemfile
================================================
source "http://rubygems.org"

# Specify your gem's dependencies in trebuchet.gemspec
gemspec

current_ruby = Gem::Version.new(RUBY_VERSION)
if current_ruby < Gem::Version.new("2.2")
  gem "rake", "< 12.3"
end


================================================
FILE: README.md
================================================
Trebuchet
=========

Trebuchet launches features at people. Wisely choose a strategy, aim, and launch!

Installation
------------


Trebuchet can be used with Rails or standalone.

To use with Rails:
gem 'trebuchet', :require => 'trebuchet_rails'

Setup
-----


Trebuchet defaults to storing data in memory, or can be used with Redis or Memcache as a data store:

    Trebuchet.set_backend :memcached
    Trebuchet.set_backend :redis, :client => Redis.new(:host => 'example.com')
    Trebuchet.set_backend :redis_cached, :client => Redis.new(:host => 'example.com')

A 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):

    Trebuchet.admin_view = proc { current_user.try(:admin?) } # /trebuchet admin interface access
    Trebuchet.time_zone = proc { current_user.time_zone } # or just "Mountain Time (US & Canada)"


Aim
---

Trebuchet can be aimed while your application is running. The syntax is:

    Trebuchet.aim('awesome_feature', :percent, 1)

Which will launch 'awesome_feature' to 1% of users.

Another builtin strategy allows launching to particular user IDs:

    Trebuchet.aim('awesome_feature', :users, [23, 42])

You can also combine multiple strategies, in which case the feature is launched if any of them is true:

    Trebuchet.feature('awesome_feature').aim(:percent, 1).aim(:users, [23, 42])

If you don't aim Trebuchet for a feature, the default action is not to launch it to anyone.


Launch
------

In a view, do this:

    <% trebuchet.launch('time_machine') do %>
        <p>Welcome to the future!</p>
    <% end %>

The code between do .. end will only run if the strategy for 'time_machine' allows launching to current_user.

You can also use it in a controller:

    def index
        trebuchet.launch('time_machine') do
            @time_machine = TimeMachine.new
        end
    end


Custom Strategies
-----------------

Trebuchet ships with a number of default strategies but you can also define your own custom strategies like so:

    Trebuchet.define_strategy(:admins) do |user|
        !!(user && user.has_role?(:admin))
    end

controller.current_user is yielded to the block and it should return true for users you want to launch to.
You can use parameters with custom strategies too:

    Trebuchet.define_strategy(:markets) do |user, markets|
        markets.include?(user.market)
    end

Like parameters for builtin strategies, these can be changed while the application is running. For example:

    Trebuchet.aim('time_machine', :markets, ['San Francisco', 'New York City'])

When using Trebuchet together with Rails, a good place to define custom strategies is in an initializer.


Visitor Strategy
----------------

Trebuchet can be used to launch to visitors (no user object present).
First, set the visitor id either directly (in a before filter) or as a proc:

    Trebuchet.visitor_id = 123

    Trebuchet.visitor_id = proc { |request| request && request.cookies[:visitor] && request.cookies[:visitor].hash }

If you're using a proc, Trebuchet passes in the request object. It expects that the proc returns an integer.
If it returns anything else, Trebuchet will not launch.

Fiber and Thread Safety
-------------

Trebuchet stores global state such as `Trebuchet.current` which is thread and fiber unsafe behavior. In order to use these
features in a fiber or threaded environment, `Trebuchet.threadsafe_state = true` will cause Trebuchet to store these values
in a thread-local state object instead. This is not the default for backward compatability reasons.


================================================
FILE: Rakefile
================================================
require 'bundler'
Bundler::GemHelper.install_tasks

task :spec do
  system 'rspec ./spec'
end

task :default => :spec

================================================
FILE: app/controllers/trebuchet_rails/features_controller.rb
================================================
module TrebuchetRails

  class FeaturesController < ApplicationController

    if Rails::VERSION::MAJOR >= 5
      before_action :control_access, :get_time_zone
    else
      before_filter :control_access, :get_time_zone
    end

    layout 'trebuchet'
    helper :trebuchet

    def index
      @features = Trebuchet::Feature.all
      @features.sort! {|x,y| x.name.downcase <=> y.name.downcase }
      
      @dismantled_features = Trebuchet::Feature.dismantled
      @dismantled_features.sort! {|x,y| x.name.downcase <=> y.name.downcase }
      
      respond_to do |wants|
        wants.html # index.html.erb
        wants.json { render :json => @features.map(&:export) }
      end
    end
    
    def timeline
      @history = []
      Trebuchet::Feature.all.each do |f|
        f.history.each do |timestamp, strategy|
          @history << { 
            :feature_name => f.name, 
            :timestamp => timestamp, 
            :strategy => strategy
          }
        end
      end
      @history = @history.sort_by { |h| h[:timestamp] }
      @history.reverse!
      respond_to do |wants|
        wants.html # index.html.erb
        wants.json do
           json_history = @history.map do |history|
            history.tap { |h| h[:strategy] = h[:strategy].export }
          end
          render :json => json_history
        end
      end
    end
    
    private
    def control_access
      allowed = if Trebuchet.admin_view.is_a?(Proc)
        begin
          instance_eval &(Trebuchet.admin_view)
        rescue
          false
        end
      else
        !!Trebuchet.admin_view
      end
      raise ActionController::RoutingError.new('Not Found') unless allowed
    end
    
    def get_time_zone
      @zone = if Trebuchet.time_zone
        if Trebuchet.time_zone.is_a?(Proc)
          Trebuchet.time_zone.call
        elsif Trebuchet.time_zone.is_a?(String)
          Trebuchet.time_zone
        else
          nil
        end
      end
      @zone = ActiveSupport::TimeZone.new(@zone || 'UTC')
    end
    
  end

end


================================================
FILE: app/helpers/trebuchet_helper.rb
================================================
module TrebuchetHelper

  def feature(feature)
    "<dd>#{feature.name}</dd>
    <dt><ul>#{strategy feature.strategy}</ul></dt>"
  end
  
  def strategy(strategy)
    html =  case strategy.name
            when :multiple
              strategy.strategies.map { |s| strategy s }.join().html_safe
            else
              strategy.to_s
            end
    strategy.name == :multiple ? html : content_tag(:li, html)
  end
  
  def trebuchet_css
    filename = File.expand_path(File.dirname(__FILE__) + "/../views/trebuchet_rails/trebuchet.css")
    return IO.read(filename)
  end
  
end

================================================
FILE: app/views/layouts/trebuchet.html.erb
================================================
<!DOCTYPE html>
<html>
  <head>
  	<title>Trebuchet</title>
  	<style type="text/css">
  		<%= trebuchet_css %>
  	</style>
  </head>

  <body>
    <%= yield %>
  </body>
</html>


================================================
FILE: app/views/trebuchet_rails/features/index.html.erb
================================================
<div id="trebuchet">
	<h1>Trebuchet Admin</h1>
	<table class="treb-features">
		<thead>
			<tr>
				<th>Feature Name</th>
				<th>Launch Strategy</th>
				<th>Strategy History</th>
			</tr>
		</thead>
		<tbody>
			<% @features.each do |feature| %>
				<%- has_history = feature.history.length > 0 %>
				<tr>
					<td><%= feature.name %></td>
					<td><%= strategy feature.strategy %></td>
					<% if has_history %>
						<td>
							<a class="history-toggle" href="#">History</a>
						</td>
					<% end %>
				</tr>
				<% if has_history %>
				<tr>
					<td class="unstyled collapsed treb-features-history-wrapper" colspan="2">
						<table class="treb-features treb-features-history well">
							<% feature.history.each do |time, strategy| %>
							  <tr><td><%= @zone.at(time.to_i).strftime("%x %X %Z") %><br /><%= strategy strategy %></td></tr>
							<% end %>
						</table>
					</td>
				</tr>
				<% end %>
			<% end %>
		</tbody>
	</table>
	<br />
	<br />
	<table class="treb-features">
		<thead>
			<tr>
				<th>Dismantled Features</th>
				<th>Strategy History</th>
			</tr>
		</thead>
		<tbody>
			<% @dismantled_features.each do |feature| %>
			  <%- has_history = feature.history.length > 0 %>
				<tr>
					<td style="text-decoration: line-through"><%= feature.name %></td>
					<% if has_history %>
						<td>
							<a class="history-toggle" href="#">History</a>
						</td>
					<% end %>
				</tr>
				<% if has_history %>
				<tr>
					<td class="unstyled collapsed treb-features-history-wrapper" colspan="1">
						<table class="treb-features treb-features-history well">
							<% feature.history.each do |time, strategy| %>
							  <tr><td><%= @zone.at(time.to_i).strftime("%x %X %Z") %><br /><%= strategy strategy %></td></tr>
							<% end %>
						</table>
					</td>
				</tr>
				<% end %>
			<% end %>
		</tbody>
	</table>
</div>

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js"></script>
<script>
	(function(undefined) {
		var trebFeatures = $('.treb-features');
		trebFeatures.on('click', '.history-toggle', function(e) {
			var $target = $(e.target);

			$target.parents('tr').next().
				find('.treb-features-history-wrapper').toggleClass('collapsed');
			e.preventDefault();
		});
	})();
</script>


================================================
FILE: app/views/trebuchet_rails/features/timeline.html.erb
================================================
<div id="trebuchet">
	<h1>Trebuchet Timeline</h1>
	<table class="treb-features">
		<thead>
			<tr>
			  <th style="min-width: 15em">Timestamp</th>
				<th>Feature Name</th>
				<th>Launch Strategy</th>
			</tr>
		</thead>
		<tbody>
			<% @history.each do |h| %>
				<tr>
				  <td><%= @zone.at(h[:timestamp].to_i).strftime("%x %X %Z") %></td>
					<td><%= h[:feature_name] %></td>
					<td><%= strategy h[:strategy] %></td>
				</tr>
			<% end %>
		</tbody>
	</table>
</div>

================================================
FILE: app/views/trebuchet_rails/trebuchet.css
================================================
#trebuchet {
	font-family: HelveticaNeue, 'Helvetica Neue', HelveticaNeueRoman, HelveticaNeue-Roman, 'Helvetica Neue Roman', TeXGyreHerosRegular, Helvetica, Tahoma, Geneva, Arial, sans-serif;
}

.well {
	-webkit-box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.2), 0 0 0 black;
	box-shadow: inset 0 0 15px rgba(0, 0, 0, 0.2), 0 0 0 black;
}

.collapsed {
	display: none;
}

.treb-features {
	border-collapse: collapse;
	border: 1px solid #c9c9c9;
	border-top-color: #000000;
}

.treb-features-history {
	border: none;
	font-size: 0.8em;
	width: 100%;
}

.treb-features thead {
	background: -webkit-gradient(linear, center top, center bottom, from(#575a5b), to(#393c3d));
	background: -moz-linear-gradient(top, #575a5b, #393c3d);
	color: #ffffff;
	text-shadow: -1px -1px 1px black;
}

.treb-features td,
.treb-features th {
	border: 1px solid #ccc;
	padding: 1em 1.5em;
	text-align: left;
}

.treb-features-history tr:first-child td {
	border-top: none;
}

.treb-features-history tr:last-child td {
	border-bottom: none;
}

.treb-features td {
	background-color: #f5f5f5;
}

.treb-features-history td {
	background: transparent;
	border-left: none;
	border-right: none;
}

.treb-features th {
	border-left-color: #525657;
	border-right-color: #74787a;
	font-weight: normal;
}

.treb-features .unstyled {
	padding: 0;
}


================================================
FILE: config/routes.rb
================================================
# TrebuchetRails::Engine.routes do

routes_block = lambda do
  scope "trebuchet", :module => "trebuchet_rails" do
    get '/' => "features#index"
    get 'timeline' => "features#timeline"
  end
end

if Rails::VERSION::MAJOR == 3
  case Rails::VERSION::MINOR
  when 0
    Rails.application.routes.draw &routes_block
  when 1
    Rails.application.routes.prepend &routes_block
  when 2
    Rails.application.routes &routes_block
  end
end


================================================
FILE: init.rb
================================================
require 'trebuchet'

# FIXME: happens too early
# def set_trebuchet_namespace(app_name)
#   if Trebuchet.backend.respond_to?(:namespace=)
#     Trebuchet.backend.namespace = "trebuchet-#{app_name}/"
#   end
# end

if defined? Rails
  Trebuchet.use_with_rails!
  
  if Rails.respond_to?(:version) && Rails.version =~ /^3/
    # Rails 3.x
    # no other setup needed  
  else
    # Rails 2.x
    load_paths.each do |path|
       ActiveSupport::Dependencies.load_once_paths.delete(path)
    end if config.environment == 'development'
    # set_trebuchet_namespace "#{Rails.root.basename}-#{Rails.env}" 
  end
end

================================================
FILE: lib/trebuchet/action_controller.rb
================================================
require 'trebuchet/action_controller_filter'

module Trebuchet::ActionController

  def self.included(base)
    base.helper_method :trebuchet
    base.class_eval do
      around_filter Trebuchet::ActionControllerFilter
    end
  end

  def trebuchet
    Trebuchet.current
  end

end


================================================
FILE: lib/trebuchet/action_controller_filter.rb
================================================
class Trebuchet::ActionControllerFilter

  def self.before(controller)
    Trebuchet.initialize_logs

    if Trebuchet.backend.respond_to?(:refresh)
      Trebuchet.backend.refresh
    end

    Trebuchet.current_block = Proc.new {
      Trebuchet.new(controller.send(:current_user), controller.request)
    }
  end

  def self.after(controller)
    Trebuchet.current_block = nil
    Trebuchet.reset_current! # very important
  end

end


================================================
FILE: lib/trebuchet/backend/disabled.rb
================================================
# This backend stores nothing and returns empty/false data (launch? will always be false)
# It can be used to disable all Trebuchet features (especially if Trebuchet fails to connect to it's normal data store)

class Trebuchet::Backend::Disabled

  def initialize(*args)
  end

  def get_strategy(feature_name)
    [:default]
  end

  def set_strategy(feature, strategy, options = nil)
    false
  end

  def append_strategy(feature, strategy, options = nil)
    false
  end
  
  def get_feature_names
    []
  end

end


================================================
FILE: lib/trebuchet/backend/memcached.rb
================================================
require 'memcache'

class Trebuchet::Backend::Memcached

  attr_accessor :namespace

  def initialize(*args)
    @memcache = MemCache.new(*args)
    @namespace = 'trebuchet/'
  end

  def get_strategy(feature_name)
    @memcache.get(key(feature_name))
  end

  def set_strategy(feature, strategy, options = nil)
    @memcache.set(key(feature), [strategy, options])
  end

  def append_strategy(feature, strategy, options = nil)
    @memcache.set(key(feature), get_strategy(feature) + [strategy, options])
  end
  
  def get_feature_names
    [] # TODO: store all key names
  end

  private

  def key(feature_name)
    "#{namespace}#{feature_name}"
  end

end


================================================
FILE: lib/trebuchet/backend/memory.rb
================================================
class Trebuchet::Backend::Memory

  def initialize(*args)
    @hash = {}
    @archived = []
  end

  def get_strategy(feature_name)
    @hash.fetch(feature_name, nil) || []
  end

  def set_strategy(feature, strategy, options = nil)
    @archived.delete(feature)
    @hash.store(feature, [strategy, options])
  end

  def append_strategy(feature, strategy, options = nil)
    @archived.delete(feature)
    strategies = get_strategy(feature) || []
    if i = strategies.index(strategy)
      strategies.delete_at(i) # remove strategy_name
      strategies.delete_at(i) # remove options
    end
    strategies += [strategy, options]
    @hash.store(feature, strategies)
  end
  
  def remove_strategy(feature)
    @hash.delete(feature)
  end
  
  def get_feature_names
    @hash.keys
  end
  
  def remove_feature(feature)
    @hash.delete(feature)
    @archived << feature
    @archived.uniq!
  end
  
  def get_archived_feature_names
    @archived
  end

end


================================================
FILE: lib/trebuchet/backend/redis.rb
================================================
require 'redis' unless defined?(Redis)
require 'json'

class Trebuchet::Backend::Redis

  attr_accessor :namespace

  def initialize(*args)
    @namespace = 'trebuchet/'
    begin
      if args.first.is_a?(Hash) && (client = args.first[:client]) && (client.is_a?(Redis) || client.is_a?(MockRedis))
        # ignore other args and use provided Redis connection
        @options = args.first
        @redis = args.first[:client]
      else
        @redis = Redis.new(*args)
      end
      unless @options && @options[:skip_check]
        # raise error if not connectedUncaught ReferenceError: google is not defined
        @redis.exists(feature_names_key) # @redis.info is slow and @redis.client.connected? is NOT reliable
      end
    rescue Exception => e
      raise Trebuchet::BackendInitializationError, e.message
    end
  end

  def get_strategy(feature_name)
    return nil unless h = @redis.hgetall(feature_key(feature_name))
    unpack_strategy(h)
  end

  def unpack_strategy(options)
    return nil unless options.is_a?(Hash)
    [].tap do |a|
      options.each do |k, v|
        begin
          key = k.to_sym
          value = JSON.load(v).first # unpack from array
          a << key
          a << value
        rescue
          # if it can't parse the JSON, skip it
        end
      end
    end
  end

  def set_strategy(feature_name, strategy, options = nil)
    remove_strategy(feature_name)
    append_strategy(feature_name, strategy, options)
    update_sentinel
  end

  def append_strategy(feature_name, strategy, options = nil)
    @redis.srem(archived_feature_names_key, feature_name)
    @redis.hset(feature_key(feature_name), strategy, [options].to_json) # have to put options in container for json
    @redis.sadd(feature_names_key, feature_name)
    store_history(feature_name)
    update_sentinel
  end

  def remove_strategy(feature_name)
    @redis.del(feature_key(feature_name))
    update_sentinel
  end

  def get_feature_names
    @redis.smembers(feature_names_key)
  end

  def get_archived_feature_names
    @redis.smembers(archived_feature_names_key)
  end

  def remove_feature(feature_name)
    @redis.del(feature_key(feature_name))
    @redis.srem(feature_names_key, feature_name)
    @redis.sadd(archived_feature_names_key, feature_name)
    update_sentinel
  end

  def store_history(feature_name)
    timestamp = Time.now.to_i
    h = @redis.hgetall(feature_key(feature_name))
    @redis.hmset(feature_history_key(feature_name, timestamp), *h.to_a.flatten)
    @redis.sadd(feature_history_key(feature_name), timestamp) # subtle
  end

  def get_history(feature_name)
    [].tap do |history|
      @redis.smembers(feature_history_key(feature_name)).sort.each do |timestamp|
        h = @redis.hgetall(feature_history_key(feature_name, timestamp))
        history << [timestamp.to_i, unpack_strategy(h)]
      end
    end
  end

  def get_all_history(include_archived = false)
    history = []

    features = @redis.smembers(feature_names_key)
    features += @redis.smembers(archived_feature_names_key) if include_archived

    result = @redis.pipelined do
      features.each do |feature_name|
        @redis.smembers(feature_history_key(feature_name))
      end
    end

    features.zip(result).each do |feature_name, timestamps|
      timestamps.each do |timestamp|
        history << [timestamp.to_i, feature_name]
      end
    end

    # sort in reverse timestamp order
    history.sort! { |x,y| y.first <=> x.first }
  end

  def update_sentinel
    @redis.set(sentinel_key, Time.now.to_i)
  end

  def get_sentinel
    @redis.get(sentinel_key) || Time.now.to_i
  end

  private

  def archived_feature_names_key
    "#{namespace}archived-feature-names"
  end

  def feature_names_key
    "#{namespace}feature-names"
  end

  def feature_key(feature_name)
    "#{namespace}features/#{feature_name}"
  end

  def feature_history_key(feature_name, timestamp = nil)
    key = "#{namespace}feature-history/#{feature_name}"
    key = "#{key}/#{timestamp}" if timestamp
    key
  end

  def sentinel_key
    "#{namespace}last_updated"
  end

end


================================================
FILE: lib/trebuchet/backend/redis_cached.rb
================================================
require 'trebuchet/backend/redis'

class Trebuchet::Backend::RedisCached < Trebuchet::Backend::Redis

  # cache strategies in memory until clear_cached_strategies is called

  def get_strategy(feature_name)
    if cached_strategies.has_key?(feature_name)
      # use cached if available (even if value is nil)
      cached_strategies[feature_name]
    else
      # or call Trebuchet::Backend::Redis#get_strategy
      # which will fetch from Redis and unpack json
      # and then cache it for next time
      cache_strategy feature_name, super(feature_name)
    end
  end

  def append_strategy(feature_name, strategy, options = nil)
    # though we can't clear the strategy for all active instances
    # this will clear the cache in the console environment to show current settings
    clear_cached_strategies
    super(feature_name, strategy, options)
  end

  def cache_strategy(feature_name, strategy)
    cached_strategies[feature_name] = strategy
  end

  def cached_strategies
    @cached_strategies ||= Hash.new
  end

  def cache_cleared_at
    @cache_cleared_at ||= Time.now
  end

  def clear_cached_strategies
    @cache_cleared_at = Time.now
    @cached_strategies = nil
  end

  def refresh
    clear_cached_strategies if Time.now > cache_cleared_at + 60.seconds
  end

end


================================================
FILE: lib/trebuchet/backend/redis_hammerspaced.rb
================================================
require 'trebuchet/backend/redis'
require 'json'

class Trebuchet::Backend::RedisHammerspaced < Trebuchet::Backend::Redis

  # This class will rely on a cron job to sync all trebuchet features
  # to local hammerspace thus this class never directly updates hammerspace
  # We also cache in memory the features and rely on before_filter
  # to lazily invalidate local cache

  attr_accessor :namespace

  def initialize(*args)
    # args.first must be a hash
    super(*args)
    @hammerspace = args.first[:hammerspace]
  end

  def get_strategy(feature_name)
    if cached_strategies.has_key?(feature_name)
      # use cached if available (even if value is nil)
      cached_strategies[feature_name]
    else
      # call to hammerspace
      cache_strategy feature_name, get_strategy_hammerspace(feature_name)
    end
  end

  def get_strategy_hammerspace(feature_name)
    # Read from hammerspace
    h = @hammerspace[feature_key(feature_name)]
    return nil unless h
    # h will be a string, we need to convert it back to Hash
    begin
      h = JSON.load(h)
    rescue
      return nil
    end
    unpack_strategy_hammerspace(h)
  end

  def unpack_strategy_hammerspace(options)
    # We don't need to further convert values
    # because it's already taken care of
    # by the refresh cron job
    # assumption here is that v will be an array and we
    # are using the first element for now
    # This makes the format compatible with redis backend
    return nil unless options.is_a?(Hash)
    [].tap do |a|
      options.each do |k, v|
        key = k.to_sym
        a << key
        a << v.first
      end
    end
  end

  def get_feature_names
    # Read from hammerspace
    return [] unless @hammerspace.has_key?(feature_names_key)
    JSON.load(@hammerspace[feature_names_key])
  end

  def append_strategy(feature_name, strategy, options = nil)
    # though we can't clear the strategy for all active instances
    # this will clear the cache in the console environment to show current settings
    clear_cached_strategies
    super(feature_name, strategy, options)
  end

  def cache_strategy(feature_name, strategy)
    cached_strategies[feature_name] = strategy
  end

  def cached_strategies
    @cached_strategies ||= Hash.new
  end

  def clear_cached_strategies
    @cached_strategies = nil
  end

  def refresh
    # We close and reopen hammerspace to see if we need to invalidate local cache
    uid = @hammerspace.uid
    @hammerspace.close
    if @hammerspace.uid != uid
      clear_cached_strategies
    end
  end

  def update_hammerspace(forced = false)
    last_updated = get_sentinel

    return if !forced && last_updated == @hammerspace[sentinel_key]

    feature_names = @redis.smembers(feature_names_key)

    features = @redis.pipelined do
      feature_names.each do |feature_name|
        @redis.hgetall(feature_key(feature_name))
      end
    end

    hash = generate_hammerspace_hash(feature_names, features, last_updated)

    @hammerspace.replace(hash)
    @hammerspace.close
    clear_cached_strategies
  end

  # feature_names is an array of strings
  # features is an array of strategies
  # Each strategy is of form ["<key1>", "<value1>", "<key2>", "<value2>"...]
  # We need to decode the values because they are in string form (not actual hash)
  def generate_hammerspace_hash(feature_names, features, last_updated)
    hash = {
      sentinel_key => last_updated.to_s,
      feature_names_key => feature_names.to_json,
    }

    feature_names.zip(features) do |feature_name, feature|
      h = {}
      feature.each_slice(2) {|k,v| h[k]=JSON.load(v)}
      hash[feature_key(feature_name)] = h.to_json
    end
    hash
  end

end


================================================
FILE: lib/trebuchet/backend.rb
================================================
module Trebuchet::Backend

  def self.lookup(name)
    # From ActiveSupport::Inflector.camelize
    const_name = name.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }

    if const_defined?(const_name)
      const_get(const_name)
    else
      raise ArgumentError.new("Unknown backend type #{name}")
    end
  end

end


================================================
FILE: lib/trebuchet/error.rb
================================================
class Trebuchet::Error < StandardError ; end
class Trebuchet::BackendInitializationError < Trebuchet::Error ; end
class Trebuchet::BackendError < Trebuchet::Error ; end

================================================
FILE: lib/trebuchet/feature/stubbing.rb
================================================
class Trebuchet
  class Feature
    module Stubbing

      def stub(state)
        self.class.stubbed_features[name] = state
      end

      def stubbed?
        !!self.class.stubbed_features[name]
      end

      def self.included(base)
        base.extend(ClassMethods)
      end

      module ClassMethods

        def dismantle_stubs
          @stubbed_features = nil
        end

        def stubbed_features
          @stubbed_features ||= {}
        end

      end

    end
  end
end

================================================
FILE: lib/trebuchet/feature.rb
================================================
require 'trebuchet/feature/stubbing'

class Trebuchet::Feature
  include Stubbing

  @@deprecated_strategies_enabled = true
  @@features = {}

  attr_accessor :name

  def initialize(name)
    @name = name
  end

  def self.find(name)
    feature = @@features[name]
    if !feature
      feature = new(name)
      @@features[name] = feature
    end

    feature.reset

    feature
  end

  def reset
    @chained = false
  end

  def self.all
    Trebuchet.backend.get_feature_names.map{|name| new(name)}
  end

  def self.dismantled
    Trebuchet.backend.get_archived_feature_names.map{|name| new(name)}
  end

  def self.exist?(name)
    !!all.detect{|feature| feature.name == name }
  end

  # Runs the block with deprecated features enabled so that various methods
  # do not raise exceptions. This was added to allow specs to test
  # deprecated features. Not thread safe.
  def self.with_deprecated_strategies_enabled(value=true, &block)
    original_value = @@deprecated_strategies_enabled
    begin
      @@deprecated_strategies_enabled = value
      block.call()
    ensure
      @@deprecated_strategies_enabled = original_value
    end
  end

  def strategy
    Trebuchet::Strategy.for_feature(self)
  end

  def valid?
    strategy.name != :invalid
  end

  def launch_at?(user, request = nil)
    # Store strategy so that only one call to the backend is needed.
    s = strategy
    (!s.needs_user? || !user.nil?) && s.launch_at?(user, request)
  end

  def aim(strategy_name, options = nil)
    if !@@deprecated_strategies_enabled &&
       Trebuchet::Strategy.deprecated_strategy_names.include?(strategy_name)
      raise "The #{strategy_name} strategy is deprecated."
    end
    if chained?
      Trebuchet.backend.append_strategy(self.name, strategy_name, options)
    else
      Trebuchet.backend.set_strategy(self.name, strategy_name, options)
    end
    @chained = true
    self
  end

  # add/edit just one strategy without affecting other chained strategies
  def adjust(strategy_name, options = nil)
    Trebuchet.backend.append_strategy(self.name, strategy_name, options)
    self
  end

  # add to the options of a strategy (if it is an integer, hash or array)
  def augment(strategy_name, new_options)
    # get old options if any
    strategy_array = Trebuchet.backend.get_strategy(self.name)
    i = strategy_array.index(strategy_name)
    old_options = i ? strategy_array[i+1] : nil
    # augment them carefully
    options = if old_options == nil
      new_options
    elsif old_options.is_a?(Array) && new_options.is_a?(Array)
      old_options + new_options
    elsif old_options.is_a?(Hash) && new_options.is_a?(Hash)
      old_options.merge(new_options)
    elsif old_options.is_a?(Numeric) && new_options.is_a?(Numeric)
      old_options + new_options
    else # otherwise, change nothing
      old_options
    end
    # adjust that strategy
    self.adjust(strategy_name, options)
  end

  def dismantle
    Trebuchet.backend.remove_feature(self.name)
  end

  # add comments for a feature, as a place to hold change logs for example, to supported backends
  def add_comment(comment)
    if Trebuchet.backend.respond_to?(:add_comment)
      Trebuchet.backend.add_comment(self.name, comment)
    end
  end

  # Retrieve the expiration date of the feature.
  # Return nil if the feature does not have an expiration date.
  def expiration_date
    return unless Trebuchet.backend.respond_to?(:expiration_date)
    Trebuchet.backend.expiration_date(self.name)
  end

  # Set the expiration date of the feature.
  def set_expiration_date(expiration_date)
    return unless Trebuchet.backend.respond_to?(:set_expiration_date)
    Trebuchet.backend.set_expiration_date(self.name, expiration_date)
  end

  def history
    return [] unless Trebuchet.backend.respond_to?(:get_history)
    Trebuchet.backend.get_history(self.name).map do |row|
      [Time.at(row.first), Trebuchet::Strategy.find(*row.last)]
    end
  end

  def feature_id
    begin
      @feature_id ||= Trebuchet::SHA1.hexdigest(@name).to_i(16)
    rescue
      return 0
    end
  end

  def as_json(options = {})
    {:name => @name, :strategy => strategy.export}
  end

  def to_s
    str = "name: \"#{@name}\", "
    str << "#{strategy.name == :multiple ? 'strategies' : 'strategy'}: #{strategy}"
  end

  def inspect
    "#<#{self.class.name} #{self}>"
  end

  def export
    {:feature_name => name, :strategy => strategy.export}
  end

  private

  def chained?
    @chained
  end

end


================================================
FILE: lib/trebuchet/state.rb
================================================
# Represents the internal, global and thread-unsafe state of Trebuchet
Trebuchet::State = Struct.new(
  :visitor_id, :current, :current_block,
  :logs, :admin_view, :admin_edit, :time_zone,
  :author
)


================================================
FILE: lib/trebuchet/strategy/base.rb
================================================
require 'digest/sha1'

class Trebuchet::Strategy::Base

  attr_accessor :feature

  def name
    self.class.strategy_name
  end

  def feature_id
    feature.feature_id
  end

  def needs_user?
    true
  end

  def self.strategy_name
    Trebuchet::Strategy.name_for_class(self)
  end

  def as_json(options = {})
    excluded = [:feature, :block]
    {:name => name}.tap do |h|
      instance_variables.map do |v|
        key = v.to_s.gsub('@','').to_sym
        h[key] = instance_variable_get(v) unless excluded.include?(key)
      end
    end
  end

  def inspect
    self.to_s
  end

  def export(options = nil)
    {:name => self.name, :options => options}
  end

end


================================================
FILE: lib/trebuchet/strategy/custom.rb
================================================
class Trebuchet::Strategy::Custom < Trebuchet::Strategy::Base

  attr_reader :options, :custom_name

  @@custom_strategies = {}

  def initialize(name, options = nil)
    @custom_name = name
    @options = options
    @block = @@custom_strategies[name]
  end

  def launch_at?(user, request = nil)
    !!(options ? @block.call(user, options) : @block.call(user))
  end

  def self.define(name, block)
    @@custom_strategies[name] = block
  end

  def self.exists?(name)
    @@custom_strategies.has_key?(name)
  end

  def needs_user?
    false
    # re-enable after adding  { |options, user, request| }
    # if block = @@custom_strategies[custom_name]
    #   block.arity > 0
    # else
    #   false
    # end
  end

  def as_json(options = {})
    {:custom_name => @custom_name, :options => @options}
  end

  def to_s
    "#{custom_name} (custom) #{options.inspect if options}"
  end

  def export
    super as_json
  end

end


================================================
FILE: lib/trebuchet/strategy/custom_request_aware.rb
================================================
class Trebuchet::Strategy::CustomRequestAware < Trebuchet::Strategy::Custom
  @@custom_request_aware_strategies = {}

  def initialize(name, options = nil)
    @custom_name = name
    @options = options
    @block = @@custom_request_aware_strategies[name]
  end

  def self.define(name, block)
    @@custom_request_aware_strategies[name] = block
  end

  def self.exists?(name)
    @@custom_request_aware_strategies.has_key?(name)
  end

  def launch_at?(user, request = nil)
    request ||= {}
    !!(options ? @block.call(user, request, options) : @block.call(user, request))
  end

  def to_s
    "#{custom_name} (custom_request_aware) #{options.inspect if options}"
  end
end


================================================
FILE: lib/trebuchet/strategy/default.rb
================================================
require 'singleton'

# Default is to not launch the feature to anyone
class Trebuchet::Strategy::Default < Trebuchet::Strategy::Base
  include Singleton

  def initialize(options = nil)
    # ignore options
  end

  def name
    :default
  end

  def launch_at?(user, request = nil)
    false
  end

  def needs_user?
    false
  end

  def to_s
    "not launched (default)"
  end

end


================================================
FILE: lib/trebuchet/strategy/everyone.rb
================================================
require 'singleton'
# Everyone is to launch the feature to everyone
class Trebuchet::Strategy::Everyone < Trebuchet::Strategy::Base
  include Singleton

  def initialize(options = nil)
    # ignore options
  end

  def name
    :everyone
  end

  def launch_at?(user, request = nil)
    true
  end

  def needs_user?
    false
  end

  def to_s
    "launched to everyone"
  end

end


================================================
FILE: lib/trebuchet/strategy/experiment.rb
================================================
# require 'digest/sha1'

class Trebuchet::Strategy::Experiment < Trebuchet::Strategy::Base
  
  include Trebuchet::Strategy::Experimentable  

  def initialize(options = {})
    initialize_experiment(options)
  end

  def launch_at?(user, request = nil)
    return false unless user && user.id
    value_in_bucket?(user.id)
  end

  # def to_s from experimentable

  # def export from experimentable

end

================================================
FILE: lib/trebuchet/strategy/hostname.rb
================================================
class Trebuchet::Strategy::Hostname < Trebuchet::Strategy::Base

  attr_reader :hostnames

  def initialize(hostnames)
    @hostnames = if hostnames.is_a?(Array)
      hostnames
    else
      [hostnames]
    end
  end

  def launch_at?(user, request = nil)
    return false if request.nil?
    self.hostnames.include?(request.host)
  end

  def needs_user?
    false
  end

  def to_s
    "hostnames (#{hostnames.empty? ? 'none' : hostnames.join(', ')})"
  end

  def export
    super @hostnames
  end

end


================================================
FILE: lib/trebuchet/strategy/invalid.rb
================================================
# Default is to not launch the feature to anyone
class Trebuchet::Strategy::Invalid < Trebuchet::Strategy::Base

  attr_reader :invalid_name, :options

  def initialize(name, options = nil)
    @invalid_name = name
    @options = options
  end

  def name
    :invalid
  end

  def launch_at?(user, request = nil)
    false
  end
  
  def needs_user?
    false
  end

  def to_s
    "#{invalid_name} (invalid) #{options.inspect if options}"
  end
  
end

================================================
FILE: lib/trebuchet/strategy/logic_and.rb
================================================
require 'trebuchet/strategy/logic_base'

class Trebuchet::Strategy::LogicAnd < Trebuchet::Strategy::LogicBase

  def launch_at?(user, request = nil)
    @strategies
      .all? { |s| (!s.needs_user? || !user.nil?) && s.launch_at?(user, request) }
  end

end

================================================
FILE: lib/trebuchet/strategy/logic_base.rb
================================================
class Trebuchet::Strategy::LogicBase < Trebuchet::Strategy::Base

  attr_reader :strategies
  attr_reader :options

  def initialize(options = {})
    @options = options
    @strategies = []
    options.each do |strategy_name, strategy_options|
      @strategies << Trebuchet::Strategy.find(strategy_name.to_sym, strategy_options)
    end
  end

  # Override feature setter so that @feature gets set on @strategies as well
  def feature=(f)
    @feature = f
    @strategies.each { |s| s.feature = f }
  end

  def launch_at?(user, request = nil)
    false # To be overriden in implementation classes.
  end

  def needs_user?
    false
  end

end

================================================
FILE: lib/trebuchet/strategy/logic_not.rb
================================================
require 'trebuchet/strategy/logic_base'

class Trebuchet::Strategy::LogicNot < Trebuchet::Strategy::LogicBase

  def launch_at?(user, request = nil)
    @strategies
      .none? { |s| (!s.needs_user? || !user.nil?) && s.launch_at?(user, request) }
  end

end

================================================
FILE: lib/trebuchet/strategy/logic_or.rb
================================================
require 'trebuchet/strategy/logic_base'

class Trebuchet::Strategy::LogicOr < Trebuchet::Strategy::LogicBase

  def launch_at?(user, request = nil)
    @strategies
      .any? { |s| (!s.needs_user? || !user.nil?) && s.launch_at?(user, request) }
  end

end

================================================
FILE: lib/trebuchet/strategy/multiple.rb
================================================
class Trebuchet::Strategy::Multiple < Trebuchet::Strategy::Base

  attr_reader :strategies

  def initialize(args)
    @strategies = []
    args.each_slice(2) do |pair|
      @strategies << Trebuchet::Strategy.find(*pair)
    end
  end

  # override setter so that @feature gets set on @strategies as well
  def feature=(f)
    @feature = f
    @strategies.each {|s| s.feature = f}
  end

  def launch_at?(user, request = nil)
    !!(strategies.select{|s| !user.nil? || !s.needs_user?}.find { |s| s.launch_at?(user, request) })
    # !!(strategies.find { |s| s.launch_at?(user, request) })
  end

  def as_json(options = {})
    @strategies
  end

  def needs_user?
    false # assume some of the strategies may not need user
    # could change this so it calls only the strategies that don't need a user when none is present
    # strategies.any? { |s| s.needs_user? }
  end

  def export
    super :strategies => strategies.map(&:export)
  end

end


================================================
FILE: lib/trebuchet/strategy/nobody.rb
================================================
require 'singleton'
# Nobody is to launch the feature to nobody
class Trebuchet::Strategy::Nobody < Trebuchet::Strategy::Base
  include Singleton

  def initialize(options = nil)
    # ignore options
  end

  def name
    :nobody
  end

  def launch_at?(user, request = nil)
    false
  end

  def needs_user?
    false
  end

  def to_s
    "launched to nobody"
  end

end


================================================
FILE: lib/trebuchet/strategy/per_denomination.rb
================================================
class Trebuchet::Strategy::PerDenomination < Trebuchet::Strategy::Base
  include Trebuchet::Strategy::PerDenominationable

  def set_range_from_options(options = {})
    numerator = options['numerator'] || options[:numerator] || 0
    denominator = options['denominator'] || options[:denominator] || 0

    super(numerator: numerator, denominator: denominator)
  end

  def launch_at?(user, request = nil)
    return false unless user && user.id
    value_in_range?(user.id.to_i)
  end

  # def to_s from PerDenominationable

  # def export from PerDenominationable
end


================================================
FILE: lib/trebuchet/strategy/percent.rb
================================================
class Trebuchet::Strategy::Percent < Trebuchet::Strategy::Base

  include Trebuchet::Strategy::Percentable

  def initialize(options)
    set_range_from_options(options)
  end

  def launch_at?(user, request = nil)
    return false unless user && user.id
    value_in_range?(user.id.to_i)
  end

  # def to_s from percentable

  # def export from percentable

end


================================================
FILE: lib/trebuchet/strategy/percent_deprecated.rb
================================================
class Trebuchet::Strategy::PercentDeprecated < Trebuchet::Strategy::Base

  include Trebuchet::Strategy::PercentableDeprecated

  def initialize(options)
    set_range_from_options(options)
  end

  def launch_at?(user, request = nil)
    return false unless user && user.id
    value_in_range?(user.id.to_i)
  end

  # def to_s from percentable

  # def export from percentable

end



================================================
FILE: lib/trebuchet/strategy/stub.rb
================================================
class Trebuchet
  module Strategy

    class Stub < Trebuchet::Strategy::Base
      attr_reader :state

      def initialize(state)
        @state = state
      end

      def launch_at?(user, request = nil)
        state == :launched
      end

      def needs_user?
        false
      end

      def to_s
        "stub (#{state}}"
      end

      def export
        super state
      end

    end

  end
end

================================================
FILE: lib/trebuchet/strategy/user_id.rb
================================================
class Trebuchet::Strategy::UserId < Trebuchet::Strategy::Base

  attr_reader :user_ids

  def initialize(user_ids)
    user_ids = Array(user_ids).flatten
    @user_ids = Set.new(user_ids)
  end

  def launch_at?(user, request = nil)
    @user_ids.include?(user.id)
  end

  def to_s
    "user ids (#{user_ids.empty? ? 'none' : user_ids.to_a.join(', ')})"
  end

  def export
    super @user_ids.to_a
  end

end

================================================
FILE: lib/trebuchet/strategy/visitor_experiment.rb
================================================
class Trebuchet::Strategy::VisitorExperiment < Trebuchet::Strategy::Base

  include Trebuchet::Strategy::Experimentable

  def initialize(options = {})
    initialize_experiment(options)
  end

  def launch_at?(user, request = nil)
   if Trebuchet.visitor_id.respond_to?(:call)
      visitor_id = Trebuchet.visitor_id.call(request)
    else
      visitor_id = nil
    end
    return false if visitor_id.nil?
    value_in_bucket?(visitor_id)
  end

  def needs_user?
    false
  end

  # def to_s from experimentable

  # def export from experimentable

end

================================================
FILE: lib/trebuchet/strategy/visitor_percent.rb
================================================
class Trebuchet::Strategy::VisitorPercent < Trebuchet::Strategy::Base

  include Trebuchet::Strategy::Percentable

  def initialize(options)
    set_range_from_options(options)
  end

  def launch_at?(user, request = nil)
    return false if request.nil?
    if Trebuchet.visitor_id.respond_to?(:call)
      visitor_id = Trebuchet.visitor_id.call(request)
    else
      visitor_id = nil
    end
    return false if visitor_id.nil?
    value_in_range?(visitor_id.to_i)
  end

  def needs_user?
    false
  end

  # def to_s from percentable

end


================================================
FILE: lib/trebuchet/strategy/visitor_percent_deprecated.rb
================================================
class Trebuchet::Strategy::VisitorPercentDeprecated < Trebuchet::Strategy::Base

  include Trebuchet::Strategy::PercentableDeprecated

  def initialize(options)
    set_range_from_options(options)
  end

  def launch_at?(user, request = nil)
    return false if request.nil?
    if Trebuchet.visitor_id.respond_to?(:call)
      visitor_id = Trebuchet.visitor_id.call(request)
    else
      visitor_id = nil
    end
    return false if visitor_id.nil?
    value_in_range?(visitor_id.to_i)
  end

  def needs_user?
    false
  end

  # def to_s from percentable

end



================================================
FILE: lib/trebuchet/strategy.rb
================================================
require 'digest/sha1'

module Trebuchet::Strategy

  def self.for_feature(feature)
    stub_state = Trebuchet::Feature.stubbed_features[feature.name]
    if stub_state
      Stub.new(stub_state)
    else
      strategy_args = Trebuchet.backend.get_strategy(feature.name)
      find(*strategy_args).tap {|s| s.feature = feature }
    end
  end

  def self.find(*args)
    strategy_name, options = args

    if args.size > 2
      Multiple.new(args)
    elsif strategy_name.nil? || strategy_name == :default
      # Strategy hasn't been defined yet
      Default.instance
    elsif strategy_name == :everyone
      Everyone.instance
    elsif strategy_name == :nobody
      Nobody.instance
    elsif CustomRequestAware.exists?(strategy_name)
      CustomRequestAware.new(strategy_name, options)
    elsif Custom.exists?(strategy_name)
      Custom.new(strategy_name, options)
    elsif klass = class_for_name(strategy_name)
      # percent, users
      klass.new(options)
    else
      Invalid.new(strategy_name, options)
    end
  end

  # The stub strategy purposely left out of this list as it should be
  # accessible via the testing interface only and not externally.
  def self.name_class_map
    [
      [:visitor_percent_deprecated, VisitorPercentDeprecated],
      [:percent_deprecated, PercentDeprecated],
      [:percent, Percent],
      [:per_denomination, PerDenomination],
      [:users, UserId],
      [:default, Default],
      [:everyone, Everyone],
      [:nobody, Nobody],
      [:custom, Custom],
      [:multiple, Multiple],
      [:experiment, Experiment],
      [:visitor_percent, VisitorPercent],
      [:hostname, Hostname],
      [:visitor_experiment, VisitorExperiment],
      [:logic_and, LogicAnd],
      [:logic_or, LogicOr],
      [:logic_not, LogicNot],
    ]
  end

  def self.deprecated_strategy_names
    [
      :percent_deprecated,
      :visitor_percent_deprecated
    ]
  end

  def self.class_for_name(name)
    classes = Hash[name_class_map]
    classes[name]
  end

  def self.name_for_class(klass)
    names = Hash[name_class_map.map(&:reverse)]
    names[klass]
  end

  module PerDenominationable
    attr_reader :numerator, :denominator

    def initialize(options)
      set_range_from_options(options)
    end

    # must be called from initialize
    def set_range_from_options(options)
      raise ArgumentError, "Missing required input numerator" unless options[:numerator]
      raise ArgumentError, "Missing required input denominator" unless options[:denominator]

      @numerator = options[:numerator].to_i
      @denominator = options[:denominator].to_i

      raise ArgumentError, "Invalid denominator #{@denominator}" if @denominator <= 0
      if @numerator > @denominator
        raise ArgumentError, "Invalid numerator #{@numerator} > denominator #{@denominator}"
      end
    end

    def value_in_range?(value)
      bucket = Trebuchet::SHA1.hexdigest("#{@feature.name}|#{value}").to_i(16) % denominator

      bucket < numerator
    end

    def to_s
      "#{numerator} / #{denominator} of users"
    end

    def export
      super({ numerator: numerator, denominator: denominator })
    end
  end

  ### Percentable module standardizes logic for percentage-based strategies

  module Percentable
    include PerDenominationable

    alias_method :percentage, :numerator

    # must be called from initialize
    def set_range_from_options(options)
      numerator =
        if options == nil || options.is_a?(Numeric)
          options.to_i
        else
          0
        end

      super(numerator: numerator, denominator: 100)
    end

    def to_s
      kind = self.name == :visitor_percent ? "visitors" : "users"
      percentage_str = "#{percentage}% of #{kind}"
      "#{percentage_str}"
    end

    def export
      super(percentage)
    end
  end


  # This module is deprecated because the implementation is such that it's
  # not possible to trust per-feature analysis if multiple features are
  # using the PercentableDeprecated based strategies because the same
  # visitors will tend to get the same features (even with the offset).
  module PercentableDeprecated

    def initialize(options)
      set_range_from_options(options)
    end

    # must be called from initialize
    def set_range_from_options(options)
      if options == nil || options.is_a?(Numeric)
        @from = 0
        @to = options.to_i - 1
        @style = :percentage
      elsif options.is_a?(Hash) && (p = options['percentage'] || options[:percentage])
        @from = 0
        @to = p.to_i - 1
        @style = :percentage
      elsif options.is_a?(Hash)
        @from = options['from'] || options[:from]
        @to = options['to'] || options[:to]
        @style = :range
      else
        @from = 0
        @to = -1
      end
    end

    def offset
      if @style == :percentage
        feature_id % 100
      else
        0
      end
    end

    def percentage
      return 0 unless @to.is_a?(Integer) && @from.is_a?(Integer)
      return 0 if @to < 0
      ((@to - @from) + 100) % 100 + 1
    end

    # call from launch_at? and pass in user id or another integer
    def value_in_range?(value)
      return false unless @from && @to
      return false if @from.to_i < 0 || @to.to_i < 0
      return false if value == nil || !value.is_a?(Numeric)
      cutoff = percentage
      value = ((value - @from) + 200 - offset) % 100
      !!(value < cutoff)
    end

    def offset_from
      (@from + offset) % 100
    end

    def offset_to
      (@to + offset) % 100
    end

    def to_s
      kind = self.name == :visitor_percent ? "visitors" : "users"
      percentage_str = "#{percentage}% of #{kind}"
      range_str = if @to < 0
        "nobody"
      else
        str = ''
        str << "user id ending with " if kind != "visitors"
        str << "#{offset_from.to_s.rjust(2, '0')}"
        str << " to #{offset_to.to_s.rjust(2, '0')}" if @to != @from
        str
      end
      @style == :range ? "#{range_str} (#{percentage_str})" : "#{percentage_str} (#{range_str})"
    end

    def export
      if @style == :percentage
        super :percentage => @to
      else
        super :from => @from, :to => @to
      end
    end

  end

  module Experimentable

    attr_reader :bucket, :total_buckets, :experiment_name

    def initialize_experiment(options)
      options.keys.each {|k| options[k.to_sym] = options.delete(k)} # cheap symbolize_keys
      @experiment_name = options[:name]
      @bucket = [ options[:bucket] ].flatten # always treat as an array
      @total_buckets = options[:total_buckets] || 5
    end

    def value_in_bucket?(value)
      return false if value == nil || !value.is_a?(Numeric)
      return false unless self.valid?
      # must hash feature name and value together to ensure uniform distribution
      b = Trebuchet::SHA1.hexdigest("experiment: #{@experiment_name.downcase} user: #{value}").to_i(16) % total_buckets
      !!@bucket.include?(b + 1) # is user in this bucket?
    end

    def valid?
      experiment_name && total_buckets > 0 && bucket.max <= total_buckets && (1..total_buckets).include?(bucket.min)
    rescue
      false
    end

    def type
      "#{name == :experiment ? "user" : "visitor"} experiment"
    end

    def as_json(options = {})
      {
        :name => experiment_name,
        :bucket => bucket,
        :total_buckets => total_buckets,
        :type => self.type
      }
    end

    def to_s
      str = "buckets (#{bucket.join(', ')}) of total: #{total_buckets}"
      str << " for #{self.type} experiment: #{experiment_name}"
    end

    def export
      super :name => experiment_name, :bucket => bucket, :total_buckets => total_buckets
    end

    def inspect
      "#<#{self.class.name} #{self}>"
    end

  end

end


================================================
FILE: lib/trebuchet/version.rb
================================================
class Trebuchet

  VERSION = "0.12.1".freeze

end


================================================
FILE: lib/trebuchet.rb
================================================
require 'digest/sha1'
require 'forwardable'
class Trebuchet
  # initialize a single one to save object allocations
  # Todo perhaps choose a better hash instead of sha1
  SHA1 = Digest::SHA1.new

  class << self
    extend Forwardable

    def_delegators :state, :current=, :current_block=, :current_block,
      :logs, :admin_view, :admin_view=, :admin_edit, :admin_edit=,
      :time_zone, :time_zone=, :author=, :author, :visitor_id

    attr_accessor :exception_handler
    attr_accessor :threadsafe_state

    def backend
      self.backend = :memory unless @backend
      @backend
    end

    def set_backend(backend_type, *args)
      if backend_type.is_a?(Symbol)
        require "trebuchet/backend/#{backend_type}"
        @backend = Backend.lookup(backend_type).new(*args)
      elsif backend_type.class.name =~ /Trebuchet::Backend/
        @backend = backend_type
      end
    end

    # this only works with additional args, e.g.: Trebuchet.backend = :memory
    alias_method :backend=, :set_backend

    alias_method :threadsafe_state?, :threadsafe_state

    # Logging done at class level
    # TODO: split by user identifier so instance can return scoped to one user
    # (in case multiple users have user.trebuchet called)
    def initialize_logs
      state.logs = {}
    end

    def log(feature_name, result)
      initialize_logs if state.logs.nil?
      logs[feature_name] = result
    end

    def logs
      state.logs
    end

    def current=(other)
      state.current = other
    end

    def current
      state.current ||= current_block.call if current_block.respond_to?(:call)
      state.current || new(nil) # return an blank Trebuchet instance if @current is not set
    end

    def reset_current!
      self.current = nil
    end

    def thread_local_key
      :trebuchet_state
    end

    # state is a representation of the current context of Trebuchet such as current and
    # current_proc, which are expected to be different between threads or fibers.
    # exception_handler and backend are not included in this state object as they are
    # not expected to change from fiber to fiber or request to request, therefore they
    # must be thread/fibersafe on their own accord.
    def state
      if threadsafe_state?
        Thread.current[thread_local_key] ||= State.new
      else
        @state ||= State.new
      end
    end

    def state=(new_state)
      if threadsafe_state?
        Thread.current[thread_local_key] = new_state
      else
        @state = new_state
      end
    end

    def threadsafe_state?
      threadsafe_state
    end
  end

  def self.aim(feature_name, *args)
    Feature.find(feature_name).aim(*args)
  end

  def self.dismantle(feature_name)
    Feature.find(feature_name).dismantle
  end

  def self.dismantle_stubs
    Feature.dismantle_stubs
  end

  def self.define_strategy(name, &block)
    Strategy::Custom.define(name, block)
  end

  def self.define_request_aware_strategy(name, &block)
    Strategy::CustomRequestAware.define(name, block)
  end

  def self.visitor_id=(id_or_proc)
    if id_or_proc.is_a?(Proc)
      state.visitor_id = id_or_proc
    elsif id_or_proc.is_a?(Integer)
      state.visitor_id = proc { |request| id_or_proc }
    else
      state.visitor_id = nil
    end
  end

  def self.use_with_rails!
    if defined?(ActionController::Base)
      ActionController::Base.send(:include, Trebuchet::ActionController)
    end
  end

  def self.feature(name)
    Feature.find(name)
  end

  def initialize(current_user, request = nil)
    @current_user = current_user
    @request = request
    @result_cache = {}
  end

  def launch(feature, &block)
    if launch?(feature)
      yield if block_given?
    end
  end

  def launch?(feature)
    result = @result_cache[feature]

    if result.nil?
      result = @result_cache[feature] =
        !!Feature.find(feature).launch_at?(@current_user, @request)
      Trebuchet.log(feature, result)
    end

    result
  rescue => e
    handle_exception(e, feature)
    return false
  end

  def handle_exception(exception, feature = nil)
    if self.class.exception_handler.is_a?(Proc)
      argc = self.class.exception_handler.arity
      argc = 3 if argc < 0
      self.class.exception_handler.call *[exception, feature, self][0,argc]
    end
  end

  def self.export
    {}.tap do |features|
      Trebuchet.backend.get_feature_names.map do |fn|
        features[fn] = self.feature(fn).strategy.export
      end
    end
  end

  def self.history(include_archived = false)
    return [] unless Trebuchet.backend.respond_to?(:get_all_history)
    Trebuchet.backend.get_all_history(include_archived).map do |row|
      [Time.at(row.first), Feature.find(row.last)]
    end
  end

end

require 'set'
require 'trebuchet/version'
require 'trebuchet/error'
require 'trebuchet/backend'
require 'trebuchet/backend/disabled'
# load other backends on demand so their dependencies can load first
require 'trebuchet/state'
require 'trebuchet/feature'
require 'trebuchet/strategy'
require 'trebuchet/strategy/base'
require 'trebuchet/strategy/custom'
require 'trebuchet/strategy/custom_request_aware'
require 'trebuchet/strategy/default'
require 'trebuchet/strategy/everyone'
require 'trebuchet/strategy/experiment'
require 'trebuchet/strategy/hostname'
require 'trebuchet/strategy/invalid'
require 'trebuchet/strategy/logic_and'
require 'trebuchet/strategy/logic_not'
require 'trebuchet/strategy/logic_or'
require 'trebuchet/strategy/multiple'
require 'trebuchet/strategy/nobody'
require 'trebuchet/strategy/percent'
require 'trebuchet/strategy/percent_deprecated'
require 'trebuchet/strategy/per_denomination'
require 'trebuchet/strategy/user_id'
require 'trebuchet/strategy/visitor_experiment'
require 'trebuchet/strategy/visitor_percent'
require 'trebuchet/strategy/visitor_percent_deprecated'

require 'trebuchet/strategy/stub'
require 'trebuchet/action_controller'


================================================
FILE: lib/trebuchet_rails/engine.rb
================================================
require 'rails'
require File.expand_path(File.dirname(__FILE__) + "/../trebuchet")


module TrebuchetRails
  class Engine < Rails::Engine
    isolate_namespace TrebuchetRails if respond_to?(:isolate_namespace)
  end
end


Trebuchet.use_with_rails!

================================================
FILE: lib/trebuchet_rails.rb
================================================
require 'trebuchet_rails/engine'

require 'trebuchet'

================================================
FILE: spec/custom_request_aware_strategy_spec.rb
================================================
require 'spec_helper'

describe Trebuchet::Strategy::CustomRequestAware do
  it "should launch according to the custom strategy with options" do
    Trebuchet.define_request_aware_strategy(:ip_address_strategy) do |current_user, request, ip_address|
      request[:ip_address] == ip_address
    end

    Trebuchet.aim('ip_limited_feature', :ip_address_strategy, '1.1.1.1')

    Trebuchet.new(User.new, { :ip_address => '1.1.1.1' }).launch?('ip_limited_feature')
      .should be_true
    Trebuchet.new(User.new, { :ip_address => '2.2.2.2' }).launch?('ip_limited_feature')
      .should be_false
  end

  it "should launch according to the custom strategy without options" do
    Trebuchet.define_request_aware_strategy(:ip_address_strategy) do |current_user, request|
      request[:ip_address] == '1.1.1.1'
    end

    Trebuchet.aim('ip_limited_feature', :ip_address_strategy)

    Trebuchet.new(User.new, { :ip_address => '1.1.1.1' }).launch?('ip_limited_feature')
      .should be_true
    Trebuchet.new(User.new, { :ip_address => '2.2.2.2' }).launch?('ip_limited_feature')
      .should be_false
  end

  it "should not explode when request is nil" do
    Trebuchet.define_request_aware_strategy(:ip_address_strategy) do |current_user, request|
      request[:ip_address] == '1.1.1.1'
    end

    Trebuchet.aim('ip_limited_feature', :ip_address_strategy)

    Trebuchet.new(User.new).launch?('ip_limited_feature').should be_false
  end
end


================================================
FILE: spec/custom_strategy_spec.rb
================================================
require 'spec_helper'

describe Trebuchet::Strategy::Custom do

  it "should launch according to the custom strategy" do
    Trebuchet.define_strategy(:admins) do |current_user|
      current_user.has_role?(:admin)
    end

    Trebuchet.aim('admin_feature', :admins)

    Trebuchet.new(User.new(1, :admin)).launch?('admin_feature').should be_true
    Trebuchet.new(User.new(1, :user)).launch?('admin_feature').should be_false
  end

  it "should pass arguments to the custom strategy" do
    Trebuchet.define_strategy(:role) do |current_user, role|
      current_user.has_role?(role.to_sym)
    end

    Trebuchet.aim('power_feature', :role, :power_user)

    Trebuchet.new(User.new(1, :power_user)).launch?('power_feature').should be_true
    Trebuchet.new(User.new(1, :user)).launch?('power_feature').should be_false
  end

  it "should allow an always-on strategy" do
    Trebuchet.define_strategy(:yes) { |user| true }
    Trebuchet.aim("perma-feature", :yes)
    Trebuchet.new(User.new 999).launch?("perma-feature").should be_true
    Trebuchet.new(User.new nil).launch?("perma-feature").should be_true
  end

  it "should needs_user? based on block arity" do
    # still a good idea to nilcheck within block however
    pending "re-enable after switching to  { |options, user, request| }"
    Trebuchet.define_strategy(:yes) { |user| true }
    Trebuchet.define_strategy(:heck_yeah) { |user, request| true }
    Trebuchet.define_strategy(:never) { false }
    Trebuchet.define_strategy(:always) { true }
    Trebuchet::Strategy::Custom.new(:yes).needs_user?.should be_true
    Trebuchet::Strategy::Custom.new(:heck_yeah).needs_user?.should be_true
    Trebuchet::Strategy::Custom.new(:never).needs_user?.should be_false
    Trebuchet::Strategy::Custom.new(:always).needs_user?.should be_false
  end

end


================================================
FILE: spec/default_strategy_spec.rb
================================================
require 'spec_helper'

describe Trebuchet::Strategy::Default do

  it "should not launch if no strategy was defined" do
    Trebuchet.new(User.new(rand(2 << 32))).launch?('default').should be_false
  end
  
  it "should be named default" do
    Trebuchet::Strategy::Default.strategy_name.should == :default
    Trebuchet.feature('whatever').strategy.name.should == :default
  end
  
  it "should always return false" do
    Trebuchet.feature('time_machine').aim(:default)
    t = Trebuchet.new User.new(1)
    t.launch?('time_machine').should === false
  end
  
  it "should return false when missing user" do
    Trebuchet.feature('time_machine').aim(:default)
    t = Trebuchet.new nil
    t.launch?('time_machine').should === false
  end

end


================================================
FILE: spec/disabled_backend_spec.rb
================================================
require 'spec_helper'

describe Trebuchet::Backend::Disabled do
  
  before do
    Trebuchet.backend = :disabled
    Trebuchet.backend.should be_a(Trebuchet::Backend::Disabled)
  end

  it "should not store features" do
    Trebuchet.feature('thing').aim(:users, [5]).aim(:percent, 9)
    Trebuchet.feature('thing').strategy.should be_a(Trebuchet::Strategy::Default)
    Trebuchet::Feature.all.should eql []
  end
  
  it "should always return false" do
    Trebuchet.feature('time_machine').aim(:disabled)
    t = Trebuchet.new User.new(1)
    t.launch?('time_machine').should === false
  end
  
  after do
    Trebuchet.backend = :memory
  end
 

end


================================================
FILE: spec/everyone_strategy_spec.rb
================================================
require 'spec_helper'

describe Trebuchet::Strategy::Everyone do

  it "should be named everyone" do
    Trebuchet::Strategy::Everyone.strategy_name.should == :everyone
    Trebuchet.feature('time_machine').aim(:everyone)
    Trebuchet.feature('time_machine').strategy.name.should == :everyone
  end

  it "should always return true" do
    Trebuchet.feature('time_machine').aim(:everyone)
    t = Trebuchet.new User.new(1)
    t.launch?('time_machine').should === true
  end

  it "should return true when missing user" do
    Trebuchet.feature('time_machine').aim(:everyone)
    t = Trebuchet.new nil
    t.launch?('time_machine').should === true
  end

end


================================================
FILE: spec/experiment_strategy_spec.rb
================================================
require 'spec_helper'

describe Trebuchet::Strategy::Experiment do
  
  before do
    @feature_name = "Photographic Memory"
    @experiment_name = "Superhumanity"
  end
  
  it "should match a user in a bucket" do
    Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => 1)
    strategy = Trebuchet.feature(@feature_name).strategy
    # these values just happen to hash for the algorithm and experiment name
    strategy.launch_at?(User.new(5)).should be_true
    strategy.launch_at?(User.new(4)).should be_false
  end
  
  it "should adjust the number of buckets" do
    Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => 1, :total_buckets => 3)
    strategy = Trebuchet.feature(@feature_name).strategy
    strategy.total_buckets.should == 3
    strategy.bucket.should == [1]
    strategy.launch_at?(User.new(4)).should be_true
    Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => 2, :total_buckets => 3)
    Trebuchet.feature(@feature_name).strategy.launch_at?(User.new(4)).should be_false
  end
  
  it "should be mutually exclusive within experiments" do
    strategies = (1..10).map do |i|
      Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => i)
      Trebuchet.feature(@feature_name).strategy
    end
    user_ids = (1..100).to_a
    launches = strategies.map do |strategy|
      user_ids.select {|user_id| strategy.launch_at?(User.new(user_id))}
    end
    occurrences = user_ids.map do |user_id|
      launches.select{|l| l.include?(user_id)}.size
    end
    # no user should be in more than one bucket
    occurrences.select{|i| i > 1}.size.should == 0
    # each user should be in one bucket
    occurrences.select{|i| i < 1}.size.should == 0
  end
  
  it "should distribute users evenly" do
    Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => 1)
    strategy = Trebuchet.feature(@feature_name).strategy
    user_ids = (1..10_000).to_a
    launches = user_ids.map {|user_id| strategy.launch_at?(User.new(user_id))}
    # total should be around 20% (spread evenly across default of 5 buckets)
    launch_count = launches.select{|l| l == true}.size
    (launch_count * 100 / user_ids.size).round.should == 20
  end
  
  it "should have low overlap between experiments" do
    other_experiment_name = 'World Peace'
    other_feature_name = 'Friendship Rings'
    another_experiment_name = 'Space Tourism'
    another_feature_name = 'Orbital Disneyland'
    Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => 1)
    Trebuchet.aim(other_feature_name, :experiment, :name => other_experiment_name, :bucket => 1)
    Trebuchet.aim(another_feature_name, :experiment, :name => another_experiment_name, :bucket => 1)
    strategies = [
      Trebuchet.feature(@feature_name).strategy,
      Trebuchet.feature(other_feature_name).strategy,
      Trebuchet.feature(another_feature_name).strategy
    ]
    # find out which users match each strategy
    user_ids = (1..10_000).to_a
    launches = strategies.map do |strategy|
      user_ids.select {|user_id| strategy.launch_at?(User.new(user_id))}
    end
    # intersect each set with the next
    overlaps = []
    (0..launches.size).each do |i|
      j = (i + 1) % launches.size
      overlaps << launches[i] & launches[j]
    end
    # each group should have about 20% overlap (for 5 buckets)
    ((19..21) === (overlaps[0].size * 100 / user_ids.size).round).should be_true
    ((19..21) === (overlaps[1].size * 100 / user_ids.size).round).should be_true
    ((19..21) === (overlaps[2].size * 100 / user_ids.size).round).should be_true
    # 1% or fewer of users should be in all three groups
    total_overlap = (launches[0] & launches[1] & launches[2])    
    (total_overlap.size * 100 / user_ids.size).round.should < 2
  end
  
  it "should return false for invalid parameters" do
    Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => 900)
    Trebuchet.feature(@feature_name).strategy.should_not be_valid
    Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => 1, :total_buckets => -17)
    Trebuchet.feature(@feature_name).strategy.should_not be_valid
    Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name, :bucket => 10, :total_buckets => 3)
    Trebuchet.feature(@feature_name).strategy.should_not be_valid
    Trebuchet.aim(@feature_name, :experiment, :name => @experiment_name)
    Trebuchet.feature(@feature_name).strategy.should_not be_valid
    Trebuchet.aim(@feature_name, :experiment, :bucket => 1)
    Trebuchet.feature(@feature_name).strategy.should_not be_valid
  end
   
end

================================================
FILE: spec/feature_spec.rb
================================================
require 'spec_helper'

describe Trebuchet::Feature do
  
  def feature
    Trebuchet.feature('some_feature')
  end

  def feature_names
    Trebuchet.backend.get_feature_names
  end

  def archived_feature_names
    Trebuchet.backend.get_archived_feature_names
  end
  
  describe :aim do
    
    it "should add one strategy" do
      feature.aim(:percent, 10)
      feature.strategy.name.should be :percent
    end
    
    it "should add multiple strategies by chaining" do
      feature.aim(:default)
      feature.strategy.name.should be :default
      feature.aim(:percent, 10).aim(:users, 1)
      feature.strategy.name.should be :multiple
      strategy_names = feature.strategy.strategies.map{ |s| s.name }
      strategy_names.should include(:percent)
      strategy_names.should include(:users)
    end
    
    it "should obliterate chained strategies" do
      feature.aim(:percent, 10).aim(:users, 1)
      feature.aim(:default)
      feature.strategy.name.should be :default
    end
    
  end
  
  describe :adjust do
    
    it "should add one strategy" do
      feature.dismantle
      feature.adjust(:percent, 10)
      feature.strategy.name.should be :percent
    end
    
    it "should adjust a strategy in the chain" do
      feature.aim(:percent, 10).aim(:users, 1)
      feature.strategy.name.should be :multiple
      feature.adjust(:users, 2)
      feature.strategy.name.should be :multiple
      user_strategy = feature.strategy.strategies.detect { |s| s.name == :users }
      user_strategy.user_ids.should include(2)
      user_strategy.user_ids.should_not include(1)
      feature.adjust(:percent, 20)
      percent_strategy = feature.strategy.strategies.detect { |s| s.name == :percent }
      percent_strategy.percentage.should == 20
    end
    
    it "should not obliterate chained strategies" do
      feature.aim(:percent, 10).aim(:users, 1)
      feature.adjust(:default)
      feature.strategy.name.should be :multiple
      feature.strategy.strategies.map{ |s| s.name }.should include(:default)
    end
    
  end
  
  describe :augment do
    
    it "should add one strategy where none exist" do
      feature.dismantle
      feature.augment(:percent, 10)
      feature.strategy.name.should be :percent
    end
    
    it "should append a strategy if others exist" do
      feature.aim(:percent, 10)
      feature.augment(:users, 1)
      feature.strategy.name.should be :multiple
      percent_strategy = feature.strategy.strategies.detect { |s| s.name == :percent }
      percent_strategy.percentage.should == 10
      user_strategy = feature.strategy.strategies.detect { |s| s.name == :users }
      user_strategy.user_ids.should include(1)
    end
    
    it "should adjust an existing strategy with numeric" do
      feature.aim(:percent, 10)
      feature.augment(:percent, 5)
      feature.strategy.name.should be :percent
      feature.strategy.percentage.should be 15
      feature.augment(:percent, 5.0)
      feature.strategy.percentage.should be 20
    end
    
    it "should adjust an existing strategy with set/array" do
      feature.aim(:users, [1])
      feature.augment(:users, [2,3])
      feature.strategy.name.should == :users
      feature.strategy.user_ids.sort.should == [1,2,3]
      feature.augment(:users, [4])
      feature.strategy.user_ids.sort.should == [1,2,3,4]
      # this probably should not be allowed
      # feature.aim(:users, 1)
      # feature.augment(:users, 2)
      # feature.strategy.user_ids.sort.should == [1,2]
    end
    
    it "should adjust an existing strategy with hash" do
      Trebuchet.define_strategy(:role_percent) do |user, options|
        percent = options[user.role].to_i
        user.id % 100 < percent
      end
      old_percentages = {:admin => 30, :editor => 50, :publisher => 100}
      feature.aim(:role_percent, old_percentages)
      new_percentages = {:admin => 100, :reviewer => 10}
      feature.augment(:role_percent, new_percentages)
      feature.strategy.name.should be :custom
      feature.strategy.custom_name.should be :role_percent
      feature.strategy.options.should == (old_percentages.merge(new_percentages))
    end
    
  end

  describe :dismantle do

    it "should remove a feature and add it to archived features" do
      feature.aim(:users, [1])
      feature_names.include?(feature.name).should be_true
      feature.dismantle
      feature_names.include?(feature.name).should be_false
      archived_feature_names.include?(feature.name).should be_true
    end

    it "should remove archived feature when dismantled feature is redefined" do
      feature.dismantle
      feature_names.include?(feature.name).should be_false
      archived_feature_names.include?(feature.name).should be_true
      feature.aim(:percent, 5)
      feature_names.include?(feature.name).should be_true
      archived_feature_names.include?(feature.name).should be_false
    end

  end

end


================================================
FILE: spec/logic_and_strategy_spec.rb
================================================
require 'spec_helper'

describe Trebuchet::Strategy::LogicAnd do

  it "passes the check only when all conditions are met - case 1" do
    Trebuchet.feature('pokemon').aim(
      :logic_and,
      {
        percent: 0,
        users: [30, 35],
      },
    )

    t = Trebuchet.new(User.new(30))
    t.launch?('pokemon').should === false
    t = Trebuchet.new(User.new(111))
    t.launch?('pokemon').should === false
  end

  it "passes the check only when all conditions are met - case 2" do
    Trebuchet.feature('pokemon').aim(
      :logic_and,
      {
        percent: 100,
        users: [30, 35],
      },
    )

    t = Trebuchet.new(User.new(30))
    t.launch?('pokemon').should === true
    t = Trebuchet.new(User.new(111))
    t.launch?('pokemon').should === false
  end

  it "nests well" do
    Trebuchet.feature('pokemon').aim(
      :logic_and,
      {
        "percent" => 100,
        "logic_and" => {
          "users" => [90, 91, 92],
          "logic_not" => {
            "users" => [90],
          },
        },
      },
    )

    [91, 92].each do |uid|
      t = Trebuchet.new(User.new(uid))
      t.launch?('pokemon').should === true
    end
    [1, 3, 5, 7, 90].each do |uid|
      t = Trebuchet.new(User.new(uid))
      t.launch?('pokemon').should === false
    end
  end

end


================================================
FILE: spec/logic_base_strategy_spec.rb
================================================
require 'spec_helper'

describe Trebuchet::Strategy::LogicBase do

  it "should set @feature on sub-strategies" do
    feature = Trebuchet.feature('pokemon')
    feature.aim(
      :logic_and,
      {
        percent: 100,
        users: [30, 35],
      },
    )
    feature.strategy.feature.name.should == 'pokemon'
    feature.strategy.strategies.first.feature.name.should == 'pokemon'
    feature.strategy.strategies.last.feature.name.should == 'pokemon'
  end

  it "should pass user and request to each strategy" do
    args = [:foo, 1]
    user = mock "User"
    request = mock "Request"
    strategy = mock "Strategy"

    Trebuchet::Strategy.should_receive(:find).with(*args).and_return(strategy)
    strategy.should_receive(:launch_at?).with(user, request)
    strategy.should_receive(:needs_user?).and_return(false)

    s = Trebuchet::Strategy::LogicOr.new({foo: 1})
    s.launch_at?(user, request)
  end

end


================================================
FILE: spec/logic_not_strategy_spec.rb
================================================
require 'spec_helper'

describe Trebuchet::Strategy::LogicNot do

  it "works expectedly as a not operator" do
    Trebuchet.feature('pokemon').aim(
      :logic_not,
      {
        users: [30, 35],
      },
    )

    t = Trebuchet.new(User.new(30))
    t.launch?('pokemon').should === false
    t = Trebuchet.new(User.new(111))
    t.launch?('pokemon').should === true
  end

end


================================================
FILE: spec/logic_or_strategy_spec.rb
================================================
require 'spec_helper'

describe Trebuchet::Strategy::LogicOr do

  it "passes the check as long as one of the children strategies passes" do
    Trebuchet.feature('pokemon').aim(
      :logic_or,
      {
        users: [30, 35],
        everyone: nil,
        nobody: nil,
        percent: 30,
      },
    )

    [1, 9, 91, 92, 2016].each do |uid|
      t = Trebuchet.new(User.new(uid))
      t.launch?('pokemon').should === true
    end
  end

  it "is not launching if no children strategy works" do
    Trebuchet.feature('pokemon').aim(
      :logic_or,
      {
        percent: 0,
        users: [30, 35],
      },
    )

    t = Trebuchet.new(User.new(30))
    t.launch?('pokemon').should === true
    t = Trebuchet.new(User.new(111))
    t.launch?('pokemon').should === false
  end

  it "nests well" do
    Trebuchet.feature('pokemon').aim(
      :logic_or,
      {
        users: [100],
        logic_and: {
          users: [90, 91, 92],
          logic_not: {
            users: [90],
          },
        },
      },
    )

    [91, 92, 100].each do |uid|
      t = Trebuchet.new(User.new(uid))
      t.launch?('pokemon').should === true
    end
    [1, 3, 5, 7, 90].each do |uid|
      t = Trebuchet.new(User.new(uid))
      t.launch?('pokemon').should === false
    end
  end

end


================================================
FILE: spec/multiple_strategy_spec.rb
================================================
require 'spec_helper'

describe Trebuchet::Strategy::Multiple do

  it "should support chaining strategies" do
    Trebuchet.feature('time_machine').aim(:percent, 5).aim(:users, [10, 11])
    should_launch('time_machine', [31, 36, 10, 11, 197])
    should_not_launch('time_machine', [49, 71])
  end

  it "should always return booleans" do
    Trebuchet.feature('time_machine').aim(:percent, 0).aim(:users, [5])
    t = Trebuchet.new User.new(5)
    t.launch?('time_machine').should === true
    t = Trebuchet.new User.new(117)
    t.launch?('time_machine').should === false
  end

  it "should set @feature on sub-strategies" do
    feature = Trebuchet.feature('time_machine')
    feature.aim(:percent, 10).aim(:users, [5])
    feature.strategy.feature.name == feature.name
    feature.strategy.strategies.first.feature.name.should == feature.name
    feature.strategy.strategies.last.feature.name.should == feature.name
  end

  it "should pass user and request to each strategy" do
    args = [:foo, 1]
    user = mock "User"
    request = mock "Request"
    strategy = mock "Strategy"

    Trebuchet::Strategy.should_receive(:find).with(*args).and_return(strategy)
    strategy.should_receive(:launch_at?).with(user, request)

    multi = Trebuchet::Strategy::Multiple.new(args)
    multi.launch_at?(user, request)
  end

  it "should always return false for needs_user?" do
    s = Trebuchet::Strategy::Multiple.new [:default, nil, :invalid, nil]
    s.needs_user?.should be_false
    s = Trebuchet::Strategy::Multiple.new [:default, nil, :percent, 5]
    s.needs_user?.should be_false
  end

  it "should skip needs_user? sub-strategies if user not present" do
    s = Trebuchet::Strategy::Multiple.new [:hostname, 'abc', :users, [1,2,3]]
    s.strategies.first.should_receive(:launch_at?)
    s.strategies.last.should_not_receive(:launch_at?)
    s.launch_at?(nil)
  end

end


================================================
FILE: spec/nobody_strategy_spec.rb
================================================
require 'spec_helper'

describe Trebuchet::Strategy::Nobody do

  it "should be named nobody" do
    Trebuchet::Strategy::Nobody.strategy_name.should == :nobody
    Trebuchet.feature('time_machine').aim(:nobody)
    Trebuchet.feature('time_machine').strategy.name.should == :nobody
  end

  it "should always return false" do
    Trebuchet.feature('time_machine').aim(:nobody)
    t = Trebuchet.new User.new(1)
    t.launch?('time_machine').should === false
  end

  it "should return false when missing user" do
    Trebuchet.feature('time_machine').aim(:nobody)
    t = Trebuchet.new nil
    t.launch?('time_machine').should === false
  end

end


================================================
FILE: spec/per_denomination_strategy_spec.rb
================================================
require 'spec_helper'

describe Trebuchet::Strategy::PerDenomination do
  describe 'launch' do
    before(:each) do
      Trebuchet.aim('strategy', :per_denomination, { numerator: 50, denominator: 1000 })
    end

    it "should not launch to unsaved users" do
      Trebuchet.new(nil).launch?('strategy').should be_false
    end

    it "should not launch to users with no IDs" do
      Trebuchet.new(User.new(nil)).launch?('strategy').should be_false
    end

    it "should launch to the correct per_denomination of users" do
      launched_users =
        (1..10_000).select { |i| Trebuchet.new(User.new(i)).launch?('strategy') }.size

      unlaunched_users =
        (1..10_000).reject { |i| Trebuchet.new(User.new(i)).launch?('strategy') }.size

      # If you run this to larger N it approaches .95 and 0.05. This is
      # hash function dependent but it's a nice santity check.
      launched_users.should be_within(200).of(500)
      unlaunched_users.should be_within(200).of(9500)
    end
  end

  it "should not yank the feature from users when per_denomination is increased" do
    Trebuchet.aim('strategy', :per_denomination, { numerator: 10, denominator: 1000 })

    first_launched_users =
      (1..10_000).select { |i| Trebuchet.new(User.new(i)).launch?('strategy') }

    Trebuchet.aim('strategy', :per_denomination, { numerator: 50, denominator: 1000 })

    second_launched_users =
      (1..10_000).select { |i| Trebuchet.new(User.new(i)).launch?('strategy') }

    (second_launched_users & first_launched_users).should eq first_launched_users
  end

  it "should distribute launches based on the feature name" do
    Trebuchet.aim('strategy1', :per_denomination, { numerator: 50, denominator: 1000 })
    Trebuchet.aim('strategy2', :per_denomination, { numerator: 50, denominator: 1000 })

    per_denomination1_launched_users =
      (1..10_000).select { |i| Trebuchet.new(User.new(i)).launch?('strategy1') }
    per_denomination2_launched_users =
      (1..10_000).select { |i| Trebuchet.new(User.new(i)).launch?('strategy2') }

    per_denomination1_launched_users.should_not eq per_denomination2_launched_users
  end

  it "should always return booleans" do
    Trebuchet.aim('strategy', :per_denomination, { numerator: 50, denominator: 1000 })
    (1..10_000).each do |i|
      [true, false].should include(Trebuchet.new(User.new(i)).launch?('strategy'))
    end
  end

  it "should handle 0 numerator" do
    Trebuchet.aim('strategy', :per_denomination, { numerator: 0, denominator: 1000 })
    should_not_launch 'strategy', (1..10_000).to_a
  end

  it "should fail on 0 denominator" do
    Trebuchet.aim('strategy', :per_denomination, { numerator: 1000, denominator: 0 })
    should_not_launch 'strategy', (1..10_000).to_a # trebuchet silently fails all invalid strategies
  end

  it "should handle numerator == denominator" do
    Trebuchet.aim('strategy', :per_denomination, { numerator: 1000, denominator: 1000 })
    should_launch 'strategy', (1..10_000).to_a
  end

  it "should fail when numerator > denominator" do
    Trebuchet.aim('strategy', :per_denomination, { numerator: 1001, denominator: 1000 })
    should_not_launch 'strategy', (1..10_000).to_a # trebuchet silently fails all invalid strategies
  end

  it "should handle garbage arguments" do
    ids = (1..20_000).to_a
    Trebuchet.feature('strategy').aim(:per_denomination, -1)
    should_not_launch 'strategy', ids
    Trebuchet.feature('strategy').aim(:per_denomination, -150)
    should_not_launch 'strategy', ids
    Trebuchet.feature('strategy').aim(:per_denomination, 'h')
    should_not_launch 'strategy', ids
    Trebuchet.feature('strategy').aim(:per_denomination, 'h')
    should_not_launch 'strategy', ids
    Trebuchet.feature('strategy').aim(:per_denomination, [5, 10])
    should_not_launch 'strategy', ids
    Trebuchet.feature('strategy').aim(:per_denomination, 20..50)
    should_not_launch 'strategy', ids
    Trebuchet.feature('strategy').aim(:per_denomination, nil)
    should_not_launch 'strategy', ids
    Trebuchet.feature('strategy').aim(:per_denomination, :from => 7)
    should_not_launch 'strategy', ids
    Trebuchet.feature('strategy').aim(:per_denomination, :to => 1)
    should_not_launch 'strategy', ids
  end
end



================================================
FILE: spec/percent_deprecated_strategy_spec.rb
================================================
require 'spec_helper'

describe Trebuchet::Strategy::PercentDeprecated do

  it "should be deprecated" do
    Trebuchet::Feature.with_deprecated_strategies_enabled(false) do
      expect {
        Trebuchet.aim('percentage', :percent_deprecated, 5)
      }.to raise_error(/deprecated/)
    end
  end

  it "should not launch to unsaved users, users with no IDs" do
    Trebuchet.aim('percentage', :percent_deprecated, 5)
    Trebuchet.new(nil).launch?('percentage').should be_false
    Trebuchet.new(User.new(nil)).launch?('percentage').should be_false
  end

  def should_only_launch_to_a_percentage_of_users(feature_name)
    Trebuchet.aim('percentage', feature_name, 5)
    offset = Trebuchet.feature('percentage').strategy.offset
    should_launch 'percentage', offset_ids([0, 1, 2, 3, 4, 100, 101, 102, 103, 104], offset)
    should_not_launch('percentage', [5, 6, 105, 106].map{|i| i - offset})
  end

  it "should only launch to a percentage of users" do
    should_only_launch_to_a_percentage_of_users(:percent_deprecated)
  end

  it "should not yank the feature from users when percentage is increased" do
    Trebuchet.aim('percentage', :percent_deprecated, 2)
    offset = Trebuchet.feature('percentage').strategy.offset
    should_launch 'percentage', offset_ids([0, 1], offset)
    should_not_launch 'percentage', offset_ids([2, 3], offset)

    Trebuchet.aim('percentage', :percent_deprecated, 4)
    should_launch 'percentage', offset_ids([0, 1, 2, 3], offset)
  end

  it "should create an offset based on the feature name" do
    Trebuchet.aim('percentage', :percent_deprecated, 1)
    offset = Trebuchet.feature('percentage').strategy.offset
    user_id = offset_ids([0], offset).first
    should_launch 'percentage', user_id
    should_not_launch 'percentage', [user_id - 1]
    should_not_launch 'percentage', [user_id + 1]
  end

  it "should always return booleans" do
    Trebuchet.feature('percentage').aim(:percent_deprecated, 1)
    offset = Trebuchet.feature('percentage').strategy.offset
    should_launch 'percentage', offset_ids([0], offset)
    t = Trebuchet.new User.new offset_ids([0], offset).first
    t.launch?('percentage').should === true
    t = Trebuchet.new User.new(0 - (offset - 1))
    t.launch?('percentage').should === false
  end


  def offset_ids(ids, offset)
    ids.to_a.map {|id| id + offset % 100}
  end


  it "should launch to the right users with percentage" do
    Trebuchet.feature('freedom').aim(:percent_deprecated, :percentage => 50)
    offset = Trebuchet.feature('freedom').strategy.offset
    should_launch 'freedom', offset_ids(0..49, offset)
    should_not_launch 'freedom',  offset_ids(50..99, offset)
    should_launch 'freedom',  offset_ids(100..149, offset)
    should_not_launch 'freedom',  offset_ids(150..199, offset)
    should_launch'freedom',  offset_ids([200, 201], offset)
    should_not_launch 'freedom',  offset_ids([250, 251], offset)
  end

  it "should launch to the right users with wrap" do
    Trebuchet.feature('freedom').aim(:percent_deprecated, :from => 99, :to => 1)
    should_launch 'freedom', [0, 1, 99, 100, 101, 199, 200, 201, 299]
    should_not_launch 'freedom', [97, 98, 102, 103, 198, 202, 298, 302]
  end

  it "should launch to 1%" do
    Trebuchet.feature('freedom').aim(:percent_deprecated, :percentage => 1)
    offset = Trebuchet.feature('freedom').strategy.offset
    should_launch 'freedom', offset_ids([100, 200, 300, 0], offset)
    should_not_launch 'freedom', offset_ids([1, 99, 101, 198, 202, 350], offset)
  end

  it "should handle 0%" do
    Trebuchet.feature('freedom').aim(:percent_deprecated, 0)
    should_not_launch 'freedom', (1..200).to_a
  end

  it "should handle garbage arguments" do
    ids = (1..200).to_a
    Trebuchet.feature('freedom').aim(:percent_deprecated, -1)
    should_not_launch 'freedom', ids
    Trebuchet.feature('freedom').aim(:percent_deprecated, -150)
    should_not_launch 'freedom', ids
    Trebuchet.feature('freedom').aim(:percent_deprecated, 'h')
    should_not_launch 'freedom', ids
    Trebuchet.feature('freedom').aim(:percent_deprecated, 'h')
    should_not_launch 'freedom', ids
    Trebuchet.feature('freedom').aim(:percent_deprecated, [5, 10])
    should_not_launch 'freedom', ids
    Trebuchet.feature('freedom').aim(:percent_deprecated, 20..50)
    should_not_launch 'freedom', ids
    Trebuchet.feature('freedom').aim(:percent_deprecated, nil)
    should_not_launch 'freedom', ids
    Trebuchet.feature('freedom').aim(:percent_deprecated, :from => 7)
    should_not_launch 'freedom', ids
    Trebuchet.feature('freedom').aim(:percent_deprecated, :to => 1)
    should_not_launch 'freedom', ids
  end

  it "works with from/to > 100" do
    # just documenting this side-effect
    Trebuchet.feature('freedom').aim(:percent_deprecated, :from => 105, :to => 210)
    should_launch 'freedom', [5, 106, 207, 308, 409, 1210]
    should_not_launch 'freedom', [4, 99, 111, 150, 215]
  end

end


================================================
FILE: spec/percent_strategy_spec.rb
================================================
require 'spec_helper'

describe Trebuchet::Strategy::Percent do

  it "should not launch to unsaved users, users with no IDs" do
    Trebuchet.aim('percentage', :percent, 5)
    Trebuchet.new(nil).launch?('percentage').should be_false
    Trebuchet.new(User.new(nil)).launch?('percentage').should be_false
  end

  it "should only launch to a percentage of users" do
    Trebuchet.aim('percentage', :percent, 5)
    should_launch 'percentage', [7, 24, 74, 75]
    should_not_launch('percentage', [47, 48, 49, 76, 78])
  end

  it "should launch to the correct percentage of users" do
    Trebuchet.aim('percentage', :percent, 5)
    map = Hash.new {|a,k| a[k] = 0 }
    n = 20000
    n.times do |i|
      map[Trebuchet.new(User.new(i)).launch?('percentage')] +=  1
    end
    map.each {|k, v|
      map[k] = v / n.to_f
    }
    # If you run this to larger N it approaches .95 and 0.05. This is
    # hash function dependent but it's a nice santity check.
    map[false].should be_within(0.02).of(0.95)
    map[true].should be_within(0.02).of(0.05)
  end

  it "should not yank the feature from users when percentage is increased" do
    Trebuchet.aim('percentage', :percent, 2)

    should_launch 'percentage', [7, 74]
    should_not_launch 'percentage', [24, 147]

    Trebuchet.aim('percentage', :percent, 4)
    should_launch 'percentage', [7, 74, 24, 147]
  end

  it "should distribute launches based on the feature name" do
    Trebuchet.aim('percentage1', :percent, 1)
    Trebuchet.aim('percentage2', :percent, 1)
    should_launch 'percentage1', [9, 145, 186]
    should_not_launch 'percentage2', [9, 145, 186]
    should_launch 'percentage2', [21, 47]
    should_not_launch 'percentage1', [21, 47]
  end

  it "should always return booleans" do
    Trebuchet.aim('percentage1', :percent, 1)
    t = Trebuchet.new(User.new(9))
    t.launch?('percentage1').should === true
    t = Trebuchet.new(User.new(21))
    t.launch?('percentage1').should === false
  end

  it "should handle 0%" do
    Trebuchet.feature('freedom').aim(:percent, 0)
    should_not_launch 'freedom', (1..200).to_a
  end

  it "should handle garbage arguments" do
    ids = (1..200).to_a
    Trebuchet.feature('freedom').aim(:percent, -1)
    should_not_launch 'freedom', ids
    Trebuchet.feature('freedom').aim(:percent, -150)
    should_not_launch 'freedom', ids
    Trebuchet.feature('freedom').aim(:percent, 'h')
    should_not_launch 'freedom', ids
    Trebuchet.feature('freedom').aim(:percent, 'h')
    should_not_launch 'freedom', ids
    Trebuchet.feature('freedom').aim(:percent, [5, 10])
    should_not_launch 'freedom', ids
    Trebuchet.feature('freedom').aim(:percent, 20..50)
    should_not_launch 'freedom', ids
    Trebuchet.feature('freedom').aim(:percent, nil)
    should_not_launch 'freedom', ids
    Trebuchet.feature('freedom').aim(:percent, :from => 7)
    should_not_launch 'freedom', ids
    Trebuchet.feature('freedom').aim(:percent, :to => 1)
    should_not_launch 'freedom', ids
  end

end



================================================
FILE: spec/redis_backend_spec.rb
================================================
require 'spec_helper'
require 'mock_redis'
require 'trebuchet/backend/redis'


describe Trebuchet::Backend::Redis do

  before(:all) do
    @backend = Trebuchet.backend
  end

  before(:each) do
    Trebuchet.set_backend :disabled
  end

  it "should set backend to redis with defaults" do
    Trebuchet.backend = :redis
    Trebuchet.backend.should be_a(Trebuchet::Backend::Redis)
    Trebuchet.set_backend :redis
    Trebuchet.backend.should be_a(Trebuchet::Backend::Redis)
  end
  
  it "should set redis client if passed in" do
    r = Redis.new
    Redis.stub!(:new).and_return(nil)
    lambda {Trebuchet.set_backend :redis}.should raise_error Trebuchet::BackendInitializationError
    Trebuchet.backend.should be_a(Trebuchet::Backend::Disabled)
    Trebuchet.backend.instance_variable_get(:@redis).should be_nil
    Trebuchet.set_backend :redis, :client => r
    Trebuchet.backend.instance_variable_get(:@redis).should eql r
  end
  
  it "should pass arguments to Redis.new" do
    r = Redis.new
    Redis.should_receive(:new).with(:this => 'that', :other => true).and_return(r)
    Trebuchet.set_backend :redis, :this => 'that', :other => true
    args = [:seven, {8 => 'nine'}]
    Redis.should_receive(:new).with(*args).and_return(r)
    Trebuchet.set_backend :redis, *args
  end
  
  after(:all) do
    # cleanup
    Trebuchet.backend = @backend
  end
 

end


================================================
FILE: spec/redis_hammerspaced_spec.rb
================================================
require 'spec_helper'
require 'mock_redis'
require 'trebuchet/backend/redis_hammerspaced'


describe Trebuchet::Backend::RedisHammerspaced do

  before(:all) do
    @backend = Trebuchet.backend
  end

  before(:each) do
    Trebuchet.set_backend :disabled
  end

  it "should properly get empty feature names and strategies" do
    r = Redis.new
    Redis.stub!(:new).and_return(nil)
    Trebuchet.backend.should be_a(Trebuchet::Backend::Disabled)
    Trebuchet.backend.instance_variable_get(:@redis).should be_nil
    Trebuchet.set_backend :redis_hammerspaced,
                          :client => r,
                          :hammerspace => {},
                          :skip_check => true
    Trebuchet.backend.get_feature_names.should eq []
    Trebuchet.backend.get_strategy("adfd").should be_nil
  end

  it "should properly get feature name list and non-empty strategies" do
    r = Redis.new
    Redis.stub!(:new).and_return(nil)
    Trebuchet.backend.should be_a(Trebuchet::Backend::Disabled)
    Trebuchet.backend.instance_variable_get(:@redis).should be_nil
    Trebuchet.set_backend :redis_hammerspaced,
                          :client => r,
                          :hammerspace => {
                            "trebuchet/feature-names" => ["foo", "bar"].to_s, # stringified array
                            "trebuchet/features/foo" => {
                              "everyone" => [nil],
                              "users" => [[1, 2, 3]],
                            }.to_json, # stringified json
                          },
                          :skip_check => true
    Trebuchet.backend.get_feature_names.should eq ["foo", "bar"]
    Trebuchet.backend.get_strategy("foo").should eq [:everyone, nil, :users, [1, 2, 3]]
  end

  it "should properly invalidate local cache" do
    r = Redis.new
    Redis.stub!(:new).and_return(nil)
    hammerspace = {
      "trebuchet/feature-names" => ["foo", "bar"].to_s,
      "trebuchet/features/foo" => {
        "everyone" => [nil],
        "users" => [[1, 2, 3]],
      }.to_json
    }
    def hammerspace.uid
      @uid ||= Time.now().to_i
      @uid = @uid + 1
    end
    def hammerspace.close
    end
    Trebuchet.set_backend :redis_hammerspaced,
                          :client => r,
                          :hammerspace => hammerspace,
                          :skip_check => true
    # Force to load strategy to local cache
    Trebuchet.backend.get_strategy("foo")
    hammerspace["trebuchet/features/foo"] = {
      "everyone" => [nil],
      "users" => [[1, 2]],
    }.to_json
    Trebuchet.backend.get_strategy("foo").should eq [:everyone, nil, :users, [1, 2, 3]]
    # after refresh we should have the up-to-date strategy
    Trebuchet.backend.refresh
    Trebuchet.backend.get_strategy("foo").should eq [:everyone, nil, :users, [1, 2]]
  end

  it "should properly generate hammerspace hash" do
    r = Redis.new
    Redis.stub!(:new).and_return(nil)
    Trebuchet.set_backend :redis_hammerspaced,
                          :client => r,
                          :hammerspace => {},
                          :skip_check => true
    feature_names = ["foo", "bar"]
    features = [
      ["everyone", "[null]", "user", "[[1,2,3]]"], # foo
      ["visitor_experiment", "[{\"name\":\"rate\",\"total_buckets\":5,\"bucket\":1}]"] # bar
    ]
    foo_string = {
      "everyone" => [nil],
      "user" => [[1,2,3]],
    }.to_json
    bar_string = {
      "visitor_experiment" => [
        {
          "name" => "rate",
          "total_buckets" => 5,
          "bucket" => 1,
        }
      ],
    }.to_json
    last_updated = Time.now.to_i
    expected_hash = {
      "trebuchet/feature-names" => feature_names.to_json,
      "trebuchet/features/foo" => foo_string,
      "trebuchet/features/bar" => bar_string,
      "trebuchet/last_updated" => last_updated.to_s,
    }
    Trebuchet.backend.generate_hammerspace_hash(
      feature_names,
      features,
      last_updated
    ).should eq expected_hash
  end

  after(:all) do
    # cleanup
    Trebuchet.backend = @backend
  end

end


================================================
FILE: spec/spec_helper.rb
================================================
$:.unshift File.dirname(__FILE__) + '/../lib'

require 'bundler'

# Bundler breaks things
Bundler.require :default, :test

require 'mock_redis'
class Redis < MockRedis ; end


require 'trebuchet'
require 'user'

RSpec.configure do |config|
  config.around(:each) { |ex|
    Trebuchet::Feature.with_deprecated_strategies_enabled(&ex)
  }
end

# # uncomment to run specs against Redis backend instead of Memory backend

# require 'redis'
# Trebuchet.set_backend :redis, Redis.new(:host => '127.0.0.1', :port => 6379)

def should_launch(feature, users)
  should_or_should_not_launch(feature, users, be_true)
end

def should_not_launch(feature, users)
  should_or_should_not_launch(feature, users, be_false)
end

def should_or_should_not_launch(feature, users, be_true_or_false)
  Array(users).each do |user_or_user_id|
    user = user_or_user_id.is_a?(User) ? user_or_user_id : User.new(user_or_user_id)
    Trebuchet.new(user).launch?(feature).should be_true_or_false
  end
end

def mock_request(cookie = nil)
  mock 'Request', :cookies => {:visitor => cookie}
end


================================================
FILE: spec/stubbing_spec.rb
================================================
require 'spec_helper'

describe Trebuchet::Feature::Stubbing do

  before do
    Trebuchet.dismantle_stubs
  end

  describe '#stub' do

    it "should stub a feature as launched" do
      Trebuchet.feature('test').stub(:launched)
      should_launch('test', [0])
    end

    it "should stub a feature as not launched" do
      Trebuchet.feature('test').stub(:not_launched)
      should_not_launch('test', [0])
    end

    it "should employ the stub strategy for features stubbed launched" do
      Trebuchet.feature('test').stub(:launched)
      Trebuchet.feature('test').strategy.is_a?(Trebuchet::Strategy::Stub)
    end

    it "should employ the stub strategy for features stubbed not launched" do
      Trebuchet.feature('test').stub(:not_launched)
      Trebuchet.feature('test').strategy.is_a?(Trebuchet::Strategy::Stub)
    end

  end

  describe '#stubbed?' do

    it "should report when features are stubbed launched" do
      Trebuchet.feature('test').stub(:launched)
      Trebuchet.feature('test').should be_stubbed
    end

    it "should report when features are stubbed not launched" do
      Trebuchet.feature('test').stub(:not_launched)
      Trebuchet.feature('test').should be_stubbed
    end

  end

  describe '#dismantle_stubs' do

    it "should reset stubbed features" do
      Trebuchet.feature('test').stub(:launched)
      Trebuchet.dismantle_stubs
      Trebuchet::Feature.stubbed_features.should be_empty
    end

    it "should restore them to their default state" do
      Trebuchet.feature('test').stub(:launched)
      Trebuchet.dismantle_stubs
      should_not_launch('test', [0])
    end

  end

  describe '#stubbed_features' do

    it "should report when feature are stubbed" do
      Trebuchet.feature('test').stub(:launched)
      Trebuchet::Feature.stubbed_features.count.should == 1
    end

    it "should report when features are stubbed launched" do
      Trebuchet.feature('test').stub(:launched)
      Trebuchet::Feature.stubbed_features['test'].should == :launched
    end

    it "should report when features are stubbed not launched" do
      Trebuchet.feature('test').stub(:not_launched)
      Trebuchet::Feature.stubbed_features['test'].should == :not_launched
    end

  end

end

================================================
FILE: spec/trebuchet_spec.rb
================================================
require 'spec_helper'

describe Trebuchet do

  describe "launch?" do

    it "should call launch_at? on feature" do
      Trebuchet::Feature.any_instance.should_receive(:launch_at?).once
      Trebuchet.new(User.new(1)).launch?('highly_experimental')
    end

    it "should  call launch_at? on feature even if missing user" do
      Trebuchet::Feature.any_instance.should_receive(:launch_at?).once
      Trebuchet.new(nil).launch?('highly_experimental')
    end

    it "caches value of launch_at?" do
      t = Trebuchet.new(nil)
      Trebuchet.feature('highly_experimental').should_receive(:launch_at?).once
      Trebuchet.feature('waste_of_time').should_receive(:launch_at?).once

      t.launch?('highly_experimental')
      t.launch?('waste_of_time')

      t.launch?('highly_experimental')
      t.launch?('waste_of_time')
    end
  end

  describe "launch" do
    it "should execute a block" do
      times = 0
      Trebuchet.aim('highly_experimental', :users, [1,2])
      (Trebuchet.new(User.new(1)).launch('highly_experimental') { times += 1 }).should be_true
      (Trebuchet.new(User.new(3)).launch('highly_experimental') { times += 1 }).should be_false
      times.should == 1
    end

    it "should not blow up if block is missing" do
      lambda do
        Trebuchet.aim('highly_experimental', :users, [1,2])
        Trebuchet.new(User.new(1)).launch('highly_experimental').should be_true
        Trebuchet.new(User.new(3)).launch('highly_experimental').should be_false
        Trebuchet.new(nil).launch('highly_experimental').should be_false
      end.should_not raise_error(LocalJumpError)

    end

  end

  describe "logging" do

    before(:all) do
      Trebuchet.aim('highly_experimental', :users, [1,2])
      Trebuchet.aim('disused', :disabled)
    end

    before(:each) do
      Trebuchet.initialize_logs
    end

    it "should log" do
      Trebuchet.logs.should == {}
      Trebuchet.new(User.new(1)).launch?('highly_experimental').should == true
      Trebuchet.logs['highly_experimental'].should == true
    end

    it "should log false/nil" do
      Trebuchet.logs['complely_fabricated'] == nil
      Trebuchet.logs['disused'].should == nil
      Trebuchet.new(User.new(1)).launch?('disused') #.should == false
      Trebuchet.logs['disused'].should == false
    end

    it "it should clear logs" do
      Trebuchet.new(User.new(1)).launch?('highly_experimental').should == true
      Trebuchet.logs['highly_experimental'].should == true
      Trebuchet.initialize_logs
      Trebuchet.logs.should == {}
    end

    it "should log from multiple trebuchet instances" do
      Trebuchet.new(User.new(1)).launch?('highly_experimental') #.should == true
      Trebuchet.new(User.new(1)).launch?('disused') #.should == false
      Trebuchet.new(nil).launch?('waste_of_time') #.should == false
      Trebuchet.logs['highly_experimental'].should == true
      Trebuchet.logs['disused'].should == false
      Trebuchet.logs['waste_of_time'].should == false
    end

  end

  describe "exception handling" do

    before :all do
      class BoomError < StandardError ; end
      Trebuchet.define_strategy(:boom) do
        raise BoomError.new "BOOM!"
      end
      Trebuchet.aim('optimism', :boom)
    end

    it "should swallow exceptions if no exception_handler defined" do
      expect { Trebuchet.current.launch?("optimism") }.to_not raise_exception
    end

    it "should invoke exception_handler if defined" do
      @feature = nil
      @exception = nil
      Trebuchet.exception_handler = lambda { |e, f, t| @exception = e; @feature = f }
      Trebuchet.current.launch?("optimism")
      @exception.should_not be_nil
      @feature.should == 'optimism'
    end

    it "should allow exception_handler to raise the exception" do
      Trebuchet.exception_handler = lambda { |e, f, t| raise e } # useful in development
      expect { Trebuchet.current.launch?("optimism") }.to raise_exception(BoomError)
    end

    it "should accept 0 to 3 arguments" do
      Trebuchet.exception_handler = lambda { @last_arg = eval local_variables.last.to_s }
      Trebuchet.current.launch?("optimism")
      @last_arg.should == nil

      Trebuchet.exception_handler = lambda { |e| @last_arg = eval local_variables.last.to_s  }
      Trebuchet.current.launch?("optimism")
      @last_arg.should be_a(StandardError)

      Trebuchet.exception_handler = lambda { |e, f| @last_arg = eval local_variables.last.to_s  }
      Trebuchet.current.launch?("optimism")
      @last_arg.should == "optimism"

      Trebuchet.exception_handler = lambda { |e, f, t| @last_arg = eval local_variables.last.to_s  }
      Trebuchet.current.launch?("optimism")
      @last_arg.should be_a(Trebuchet)

      Trebuchet.exception_handler = lambda { |*args| @args = args }
      Trebuchet.current.launch?("optimism")
      @args.size.should == 3
      @args.last.should be_a(Trebuchet)
    end

    it "should not blow up if exception_handler is not a proc" do
      Trebuchet.exception_handler = "one of my shoes"
      expect { Trebuchet.current.launch?("optimism") }.to_not raise_exception

      Trebuchet.exception_handler = false
      expect { Trebuchet.current.launch?("optimism") }.to_not raise_exception

      Trebuchet.exception_handler = true
      expect { Trebuchet.current.launch?("optimism") }.to_not raise_exception

      Trebuchet.exception_handler = Trebuchet.current
      expect { Trebuchet.current.launch?("optimism") }.to_not raise_exception
    end

  end

  it "stores state variables" do
    %i{author admin_view admin_edit time_zone}.each do |key|
      begin
        Trebuchet.public_send :"#{key}=", 1
        expect(Trebuchet.public_send(key)).to eq 1
      ensure
        Trebuchet.public_send :"#{key}=", nil
      end
    end
  end
end


================================================
FILE: spec/user.rb
================================================
class User < Struct.new(:id, :role)

  def has_role?(role)
    self.role == role
  end

end


================================================
FILE: spec/user_id_strategy_spec.rb
================================================
require 'spec_helper'

describe Trebuchet::Strategy::UserId do

  it "should only launch to designated users" do
    Trebuchet.aim('highly_experimental', :users, [1, 2])
    yes = [1, 2]
    no  = [3, 4]

    yes.each do |n|
      Trebuchet.new(User.new(n)).launch?('highly_experimental').should be_true
    end

    no.each do |n|
      Trebuchet.new(User.new(n)).launch?('highly_experimental').should be_false
    end
  end
  
  it "should always return booleans" do
    Trebuchet.feature('time_machine').aim(:users, [1])
    t = Trebuchet.new User.new(1)
    t.launch?('time_machine').should === true
    t = Trebuchet.new User.new(117)
    t.launch?('time_machine').should === false
  end

  # this behavior should be deprecated -- causes problems with augment
  it "should not break if one id is passed instead of an array" do
    Trebuchet.feature('time_machine').aim(:users, 1)
    t = Trebuchet.new User.new(1)
    t.launch?('time_machine').should === true
  end
  
  it "should not break on missing user" do
    Trebuchet.feature("the chosen ones").aim(:users, [1,2,3])
    t = Trebuchet.new User.new(nil)
    t.launch?("the chosen ones").should be_false
    t = Trebuchet.new nil
    t.launch?("the chosen ones").should be_false
  end

end


================================================
FILE: spec/visitor_experiment_strategy_spec.rb
================================================
require 'spec_helper'

describe Trebuchet::Strategy::VisitorExperiment do
  
  before do
    @feature_name = "Infrared Vision"
    @feature_name2 = "Alligator Tail"
    @experiment_name = "Superhumanity"
    @user = User.new(0)
    @mock_request = mock_request('abcdef')
    @trebuchet = Trebuchet.new(@user, @mock_request)
  end

  it "should match a user in a bucket" do
    Trebuchet.aim(@feature_name, :visitor_experiment, :name => @experiment_name, :bucket => 1)
    should_not_launch(@feature_name, (1..50).to_a) # should never launch without a request
    # these values just happen to hash for the algorithm and experiment name
    positive = [5, 14, 15, 198, 200, 549]
    negative = [1, 2, 25, 550]
    positive.each do |i|
      Trebuchet.visitor_id = i
      Trebuchet.feature(@feature_name).launch_at?(@user, @mock_request).should be_true
    end
    negative.each do |i|
      Trebuchet.visitor_id = i
      Trebuchet.feature(@feature_name).launch_at?(@user, @mock_request).should be_false
    end
  end
  
  it "should launch nil request to no bucket" do
    Trebuchet.aim(@feature_name, :visitor_experiment, :name => @experiment_name, :total_buckets => 2, :bucket => 1)
    Trebuchet.aim(@feature_name2, :visitor_experiment, :name => @experiment_name, :total_buckets => 2, :bucket => 2)
    Trebuchet.visitor_id = nil
    @trebuchet.launch?(@feature_name).should be_false
    @trebuchet.launch?(@feature_name2).should be_false
  end
  

end


================================================
FILE: spec/visitor_percent_deprecated_strategy_spec.rb
================================================
require 'spec_helper'

describe Trebuchet::Strategy::VisitorPercentDeprecated do

  it "should be deprecated" do
    Trebuchet::Feature.with_deprecated_strategies_enabled(false) do
      expect {
        Trebuchet.aim('some_feature', :visitor_percent_deprecated, 5)
      }.to raise_error(/deprecated/)
    end
  end

  it "should not break if no visitor id is set" do
    Trebuchet.aim('some_feature', :visitor_percent_deprecated, 100)
    t = Trebuchet.new(User.new(0))
    t.launch?('some_feature').should == false
  end

  it "should require a request" do
    Trebuchet.visitor_id = 1
    Trebuchet.aim('some_feature', :visitor_percent_deprecated, 100)
    t = Trebuchet.new(User.new(1), nil)
    t.launch?('some_feature').should == false
  end

  it "should not require a user" do
    Trebuchet.visitor_id = 1
    Trebuchet.aim('some_feature', :visitor_percent_deprecated, 100)
    t = Trebuchet.new(nil, mock_request('12345'))
    t.launch?('some_feature').should == true
  end

  describe 'visitor id integer' do
    before do
      Trebuchet.visitor_id = 123
    end

    def should_launch_test(feature_name)
      # offset of some_feature is 33
      Trebuchet.aim('some_feature', feature_name, 100)
      offset = Trebuchet.feature('some_feature').strategy.offset
      user = User.new(0)
      request = mock_request('12345')
      Trebuchet.feature('some_feature').launch_at?(user, request).should == true
      visitor_id = Trebuchet.visitor_id.call

      Trebuchet.aim('some_feature', feature_name, 91) # 33 + 91 includes 123 % 100
      Trebuchet.feature('some_feature').launch_at?(user, request).should == true

      Trebuchet.aim('some_feature', feature_name, 90)
      Trebuchet.feature('some_feature').launch_at?(user, request).should == false
    end

    it 'should launch' do
      should_launch_test(:visitor_percent_deprecated)
    end
  end

  describe 'visitor id proc' do
    before do
      Trebuchet.visitor_id = proc { |request| request && request.cookies[:visitor] && request.cookies[:visitor].hash }
    end

    it 'should not launch if no request is present' do
      Trebuchet.aim('some_feature', :visitor_percent_deprecated, 100)
      should_not_launch('some_feature', [1000])
    end

    it 'should launch to a valid session' do
      Trebuchet.aim('some_feature', :visitor_percent_deprecated, 100)
      t = Trebuchet.new(User.new(0), mock_request('abcdef'))
      t.launch?('some_feature').should == true
    end

    it 'should not launch to a nil session ID' do
      Trebuchet.aim('some_feature', :visitor_percent_deprecated, 100)
      t = Trebuchet.new(User.new(0), mock_request(nil))
      t.launch?('some_feature').should == false
    end
  end

  describe 'visitor id invalid' do
    it "should handle nil" do
      Trebuchet.visitor_id = nil
      Trebuchet.aim('some_feature', :visitor_percent_deprecated, 100)
      t = Trebuchet.new(User.new(0), mock_request('abcdef'))
      t.launch?('some_feature').should == false
    end
  end

  describe 'percentable' do

    before do
      @feature = Trebuchet.feature("liberty")
      @user = User.new(0)
      @request = mock_request('abcdef')
      @trebuchet = Trebuchet.new(@user, @request)
    end

    it "should use from and to" do
      @feature.aim(:visitor_percent_deprecated, :from => 5, :to => 10)
      Trebuchet.feature("liberty").strategy.offset.should == 0
      Trebuchet.visitor_id = 10
      Trebuchet.feature("liberty").launch_at?(@user, @request).should == true
      Trebuchet.visitor_id = 5
      Trebuchet.feature("liberty").launch_at?(@user, @request).should == true
      Trebuchet.visitor_id = 4
      Trebuchet.feature("liberty").launch_at?(@user, @request).should == false
      Trebuchet.visitor_id = 11
      Trebuchet.feature("liberty").launch_at?(@user, @request).should == false
    end

    it "should use a percentage" do
      @feature.aim(:visitor_percent_deprecated, :percentage => 25)
      offset = @feature.strategy.offset
      offset.should == 90
      Trebuchet.visitor_id = 24 + offset
      Trebuchet.feature("liberty").launch_at?(@user, @request).should == true
      Trebuchet.visitor_id = 0 + offset
      Trebuchet.feature("liberty").launch_at?(@user, @request).should == true
      Trebuchet.visitor_id = 5 + offset
      Trebuchet.feature("liberty").launch_at?(@user, @request).should == true
      Trebuchet.visitor_id = 25 + offset
      Trebuchet.feature("liberty").launch_at?(@user, @request).should == false
    end

  end

end


================================================
FILE: spec/visitor_percent_strategy_spec.rb
================================================
require 'spec_helper'

describe Trebuchet::Strategy::VisitorPercent do

  it "should not break if no visitor id is set" do
    Trebuchet.aim('some_feature', :visitor_percent, 100)
    t = Trebuchet.new(User.new(0))
    t.launch?('some_feature').should == false
  end

  it "should require a request" do
    Trebuchet.visitor_id = 1
    Trebuchet.aim('some_feature', :visitor_percent, 100)
    t = Trebuchet.new(User.new(1), nil)
    t.launch?('some_feature').should == false
  end

  it "should not require a user" do
    Trebuchet.visitor_id = 1
    Trebuchet.aim('some_feature', :visitor_percent, 100)
    t = Trebuchet.new(nil, mock_request('12345'))
    t.launch?('some_feature').should == true
  end

  describe 'visitor id integer' do
    before do
      Trebuchet.visitor_id = 123
    end

    def should_launch_test(feature_name)
    end

    it 'should launch' do
      Trebuchet.aim('some_feature', :visitor_percent, 100)
      user = User.new(0)
      request = mock_request('12345')

      Trebuchet.feature('some_feature').launch_at?(user, request).should == true
      visitor_id = Trebuchet.visitor_id.call

      Trebuchet.aim('some_feature', :visitor_percent, 91)
      Trebuchet.feature('some_feature').launch_at?(user, request).should == true

      Trebuchet.aim('some_feature', :visitor_percent, 10)
      Trebuchet.feature('some_feature').launch_at?(user, request).should == false
    end
  end

  describe 'visitor id proc' do
    before do
      Trebuchet.visitor_id = proc { |request| request && request.cookies[:visitor] && request.cookies[:visitor].hash }
    end

    it 'should not launch if no request is present' do
      Trebuchet.aim('some_feature', :visitor_percent, 100)
      should_not_launch('some_feature', [1000])
    end

    it 'should launch to a valid session' do
      Trebuchet.aim('some_feature', :visitor_percent, 100)
      t = Trebuchet.new(User.new(0), mock_request('abcdef'))
      t.launch?('some_feature').should == true
    end

    it 'should not launch to a nil session ID' do
      Trebuchet.aim('some_feature', :visitor_percent, 100)
      t = Trebuchet.new(User.new(0), mock_request(nil))
      t.launch?('some_feature').should == false
    end
  end

  describe 'visitor id invalid' do
    it "should handle nil" do
      Trebuchet.visitor_id = nil
      Trebuchet.aim('some_feature', :visitor_percent, 100)
      t = Trebuchet.new(User.new(0), mock_request('abcdef'))
      t.launch?('some_feature').should == false
    end
  end
end



================================================
FILE: trebuchet.gemspec
================================================
# -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
require 'trebuchet'

Gem::Specification.new do |s|
  s.name        = "trebuchet"
  s.version     = Trebuchet::VERSION
  s.platform    = Gem::Platform::RUBY
  s.authors     = ["Justin Jones", "Tobi Knaup", "Ross Allen"]
  s.email       = ["justin@airbnb.com"]
  s.homepage    = "http://www.airbnb.com"
  s.summary     = %q{Trebuchet launches features at people}
  s.description = %q{Wisely choose a strategy, aim, and launch!}

  s.files         = `git ls-files`.split("\n")
  s.test_files    = `git ls-files -- {test,spec,features}/*`.split("\n")
  s.executables   = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
  s.require_paths = ["lib"]

  # redis and memcache are optional
  s.add_dependency 'json'

  s.add_development_dependency 'rspec', '~> 2.12.0'
  s.add_development_dependency 'mock_redis', '~> 0.6.5'
  s.add_development_dependency 'rake', '>= 10.0.3'
end
Download .txt
gitextract_xbvggvpr/

├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── Gemfile
├── README.md
├── Rakefile
├── app/
│   ├── controllers/
│   │   └── trebuchet_rails/
│   │       └── features_controller.rb
│   ├── helpers/
│   │   └── trebuchet_helper.rb
│   └── views/
│       ├── layouts/
│       │   └── trebuchet.html.erb
│       └── trebuchet_rails/
│           ├── features/
│           │   ├── index.html.erb
│           │   └── timeline.html.erb
│           └── trebuchet.css
├── config/
│   └── routes.rb
├── init.rb
├── lib/
│   ├── trebuchet/
│   │   ├── action_controller.rb
│   │   ├── action_controller_filter.rb
│   │   ├── backend/
│   │   │   ├── disabled.rb
│   │   │   ├── memcached.rb
│   │   │   ├── memory.rb
│   │   │   ├── redis.rb
│   │   │   ├── redis_cached.rb
│   │   │   └── redis_hammerspaced.rb
│   │   ├── backend.rb
│   │   ├── error.rb
│   │   ├── feature/
│   │   │   └── stubbing.rb
│   │   ├── feature.rb
│   │   ├── state.rb
│   │   ├── strategy/
│   │   │   ├── base.rb
│   │   │   ├── custom.rb
│   │   │   ├── custom_request_aware.rb
│   │   │   ├── default.rb
│   │   │   ├── everyone.rb
│   │   │   ├── experiment.rb
│   │   │   ├── hostname.rb
│   │   │   ├── invalid.rb
│   │   │   ├── logic_and.rb
│   │   │   ├── logic_base.rb
│   │   │   ├── logic_not.rb
│   │   │   ├── logic_or.rb
│   │   │   ├── multiple.rb
│   │   │   ├── nobody.rb
│   │   │   ├── per_denomination.rb
│   │   │   ├── percent.rb
│   │   │   ├── percent_deprecated.rb
│   │   │   ├── stub.rb
│   │   │   ├── user_id.rb
│   │   │   ├── visitor_experiment.rb
│   │   │   ├── visitor_percent.rb
│   │   │   └── visitor_percent_deprecated.rb
│   │   ├── strategy.rb
│   │   └── version.rb
│   ├── trebuchet.rb
│   ├── trebuchet_rails/
│   │   └── engine.rb
│   └── trebuchet_rails.rb
├── spec/
│   ├── custom_request_aware_strategy_spec.rb
│   ├── custom_strategy_spec.rb
│   ├── default_strategy_spec.rb
│   ├── disabled_backend_spec.rb
│   ├── everyone_strategy_spec.rb
│   ├── experiment_strategy_spec.rb
│   ├── feature_spec.rb
│   ├── logic_and_strategy_spec.rb
│   ├── logic_base_strategy_spec.rb
│   ├── logic_not_strategy_spec.rb
│   ├── logic_or_strategy_spec.rb
│   ├── multiple_strategy_spec.rb
│   ├── nobody_strategy_spec.rb
│   ├── per_denomination_strategy_spec.rb
│   ├── percent_deprecated_strategy_spec.rb
│   ├── percent_strategy_spec.rb
│   ├── redis_backend_spec.rb
│   ├── redis_hammerspaced_spec.rb
│   ├── spec_helper.rb
│   ├── stubbing_spec.rb
│   ├── trebuchet_spec.rb
│   ├── user.rb
│   ├── user_id_strategy_spec.rb
│   ├── visitor_experiment_strategy_spec.rb
│   ├── visitor_percent_deprecated_strategy_spec.rb
│   └── visitor_percent_strategy_spec.rb
└── trebuchet.gemspec
Download .txt
SYMBOL INDEX (309 symbols across 48 files)

FILE: app/controllers/trebuchet_rails/features_controller.rb
  type TrebuchetRails (line 1) | module TrebuchetRails
    class FeaturesController (line 3) | class FeaturesController < ApplicationController
      method index (line 14) | def index
      method timeline (line 27) | def timeline
      method control_access (line 52) | def control_access
      method get_time_zone (line 65) | def get_time_zone

FILE: app/helpers/trebuchet_helper.rb
  type TrebuchetHelper (line 1) | module TrebuchetHelper
    function feature (line 3) | def feature(feature)
    function strategy (line 8) | def strategy(strategy)
    function trebuchet_css (line 18) | def trebuchet_css

FILE: lib/trebuchet.rb
  class Trebuchet (line 3) | class Trebuchet
    method backend (line 18) | def backend
    method set_backend (line 23) | def set_backend(backend_type, *args)
    method initialize_logs (line 40) | def initialize_logs
    method log (line 44) | def log(feature_name, result)
    method logs (line 49) | def logs
    method current= (line 53) | def current=(other)
    method current (line 57) | def current
    method reset_current! (line 62) | def reset_current!
    method thread_local_key (line 66) | def thread_local_key
    method state (line 75) | def state
    method state= (line 83) | def state=(new_state)
    method threadsafe_state? (line 91) | def threadsafe_state?
    method aim (line 96) | def self.aim(feature_name, *args)
    method dismantle (line 100) | def self.dismantle(feature_name)
    method dismantle_stubs (line 104) | def self.dismantle_stubs
    method define_strategy (line 108) | def self.define_strategy(name, &block)
    method define_request_aware_strategy (line 112) | def self.define_request_aware_strategy(name, &block)
    method visitor_id= (line 116) | def self.visitor_id=(id_or_proc)
    method use_with_rails! (line 126) | def self.use_with_rails!
    method feature (line 132) | def self.feature(name)
    method initialize (line 136) | def initialize(current_user, request = nil)
    method launch (line 142) | def launch(feature, &block)
    method launch? (line 148) | def launch?(feature)
    method handle_exception (line 163) | def handle_exception(exception, feature = nil)
    method export (line 171) | def self.export
    method history (line 179) | def self.history(include_archived = false)

FILE: lib/trebuchet/action_controller.rb
  type Trebuchet::ActionController (line 3) | module Trebuchet::ActionController
    function included (line 5) | def self.included(base)
    function trebuchet (line 12) | def trebuchet

FILE: lib/trebuchet/action_controller_filter.rb
  class Trebuchet::ActionControllerFilter (line 1) | class Trebuchet::ActionControllerFilter
    method before (line 3) | def self.before(controller)
    method after (line 15) | def self.after(controller)

FILE: lib/trebuchet/backend.rb
  type Trebuchet::Backend (line 1) | module Trebuchet::Backend
    function lookup (line 3) | def self.lookup(name)

FILE: lib/trebuchet/backend/disabled.rb
  class Trebuchet::Backend::Disabled (line 4) | class Trebuchet::Backend::Disabled
    method initialize (line 6) | def initialize(*args)
    method get_strategy (line 9) | def get_strategy(feature_name)
    method set_strategy (line 13) | def set_strategy(feature, strategy, options = nil)
    method append_strategy (line 17) | def append_strategy(feature, strategy, options = nil)
    method get_feature_names (line 21) | def get_feature_names

FILE: lib/trebuchet/backend/memcached.rb
  class Trebuchet::Backend::Memcached (line 3) | class Trebuchet::Backend::Memcached
    method initialize (line 7) | def initialize(*args)
    method get_strategy (line 12) | def get_strategy(feature_name)
    method set_strategy (line 16) | def set_strategy(feature, strategy, options = nil)
    method append_strategy (line 20) | def append_strategy(feature, strategy, options = nil)
    method get_feature_names (line 24) | def get_feature_names
    method key (line 30) | def key(feature_name)

FILE: lib/trebuchet/backend/memory.rb
  class Trebuchet::Backend::Memory (line 1) | class Trebuchet::Backend::Memory
    method initialize (line 3) | def initialize(*args)
    method get_strategy (line 8) | def get_strategy(feature_name)
    method set_strategy (line 12) | def set_strategy(feature, strategy, options = nil)
    method append_strategy (line 17) | def append_strategy(feature, strategy, options = nil)
    method remove_strategy (line 28) | def remove_strategy(feature)
    method get_feature_names (line 32) | def get_feature_names
    method remove_feature (line 36) | def remove_feature(feature)
    method get_archived_feature_names (line 42) | def get_archived_feature_names

FILE: lib/trebuchet/backend/redis.rb
  class Trebuchet::Backend::Redis (line 4) | class Trebuchet::Backend::Redis
    method initialize (line 8) | def initialize(*args)
    method get_strategy (line 27) | def get_strategy(feature_name)
    method unpack_strategy (line 32) | def unpack_strategy(options)
    method set_strategy (line 48) | def set_strategy(feature_name, strategy, options = nil)
    method append_strategy (line 54) | def append_strategy(feature_name, strategy, options = nil)
    method remove_strategy (line 62) | def remove_strategy(feature_name)
    method get_feature_names (line 67) | def get_feature_names
    method get_archived_feature_names (line 71) | def get_archived_feature_names
    method remove_feature (line 75) | def remove_feature(feature_name)
    method store_history (line 82) | def store_history(feature_name)
    method get_history (line 89) | def get_history(feature_name)
    method get_all_history (line 98) | def get_all_history(include_archived = false)
    method update_sentinel (line 120) | def update_sentinel
    method get_sentinel (line 124) | def get_sentinel
    method archived_feature_names_key (line 130) | def archived_feature_names_key
    method feature_names_key (line 134) | def feature_names_key
    method feature_key (line 138) | def feature_key(feature_name)
    method feature_history_key (line 142) | def feature_history_key(feature_name, timestamp = nil)
    method sentinel_key (line 148) | def sentinel_key

FILE: lib/trebuchet/backend/redis_cached.rb
  class Trebuchet::Backend::RedisCached (line 3) | class Trebuchet::Backend::RedisCached < Trebuchet::Backend::Redis
    method get_strategy (line 7) | def get_strategy(feature_name)
    method append_strategy (line 19) | def append_strategy(feature_name, strategy, options = nil)
    method cache_strategy (line 26) | def cache_strategy(feature_name, strategy)
    method cached_strategies (line 30) | def cached_strategies
    method cache_cleared_at (line 34) | def cache_cleared_at
    method clear_cached_strategies (line 38) | def clear_cached_strategies
    method refresh (line 43) | def refresh

FILE: lib/trebuchet/backend/redis_hammerspaced.rb
  class Trebuchet::Backend::RedisHammerspaced (line 4) | class Trebuchet::Backend::RedisHammerspaced < Trebuchet::Backend::Redis
    method initialize (line 13) | def initialize(*args)
    method get_strategy (line 19) | def get_strategy(feature_name)
    method get_strategy_hammerspace (line 29) | def get_strategy_hammerspace(feature_name)
    method unpack_strategy_hammerspace (line 42) | def unpack_strategy_hammerspace(options)
    method get_feature_names (line 59) | def get_feature_names
    method append_strategy (line 65) | def append_strategy(feature_name, strategy, options = nil)
    method cache_strategy (line 72) | def cache_strategy(feature_name, strategy)
    method cached_strategies (line 76) | def cached_strategies
    method clear_cached_strategies (line 80) | def clear_cached_strategies
    method refresh (line 84) | def refresh
    method update_hammerspace (line 93) | def update_hammerspace(forced = false)
    method generate_hammerspace_hash (line 117) | def generate_hammerspace_hash(feature_names, features, last_updated)

FILE: lib/trebuchet/error.rb
  class Trebuchet::Error (line 1) | class Trebuchet::Error < StandardError ; end
  class Trebuchet::BackendInitializationError (line 2) | class Trebuchet::BackendInitializationError < Trebuchet::Error ; end
  class Trebuchet::BackendError (line 3) | class Trebuchet::BackendError < Trebuchet::Error ; end

FILE: lib/trebuchet/feature.rb
  class Trebuchet::Feature (line 3) | class Trebuchet::Feature
    method initialize (line 11) | def initialize(name)
    method find (line 15) | def self.find(name)
    method reset (line 27) | def reset
    method all (line 31) | def self.all
    method dismantled (line 35) | def self.dismantled
    method exist? (line 39) | def self.exist?(name)
    method with_deprecated_strategies_enabled (line 46) | def self.with_deprecated_strategies_enabled(value=true, &block)
    method strategy (line 56) | def strategy
    method valid? (line 60) | def valid?
    method launch_at? (line 64) | def launch_at?(user, request = nil)
    method aim (line 70) | def aim(strategy_name, options = nil)
    method adjust (line 85) | def adjust(strategy_name, options = nil)
    method augment (line 91) | def augment(strategy_name, new_options)
    method dismantle (line 112) | def dismantle
    method add_comment (line 117) | def add_comment(comment)
    method expiration_date (line 125) | def expiration_date
    method set_expiration_date (line 131) | def set_expiration_date(expiration_date)
    method history (line 136) | def history
    method feature_id (line 143) | def feature_id
    method as_json (line 151) | def as_json(options = {})
    method to_s (line 155) | def to_s
    method inspect (line 160) | def inspect
    method export (line 164) | def export
    method chained? (line 170) | def chained?

FILE: lib/trebuchet/feature/stubbing.rb
  class Trebuchet (line 1) | class Trebuchet
    class Feature (line 2) | class Feature
      type Stubbing (line 3) | module Stubbing
        function stub (line 5) | def stub(state)
        function stubbed? (line 9) | def stubbed?
        function included (line 13) | def self.included(base)
        type ClassMethods (line 17) | module ClassMethods
          function dismantle_stubs (line 19) | def dismantle_stubs
          function stubbed_features (line 23) | def stubbed_features

FILE: lib/trebuchet/strategy.rb
  type Trebuchet::Strategy (line 3) | module Trebuchet::Strategy
    function for_feature (line 5) | def self.for_feature(feature)
    function find (line 15) | def self.find(*args)
    function name_class_map (line 41) | def self.name_class_map
    function deprecated_strategy_names (line 63) | def self.deprecated_strategy_names
    function class_for_name (line 70) | def self.class_for_name(name)
    function name_for_class (line 75) | def self.name_for_class(klass)
    type PerDenominationable (line 80) | module PerDenominationable
      function initialize (line 83) | def initialize(options)
      function set_range_from_options (line 88) | def set_range_from_options(options)
      function value_in_range? (line 101) | def value_in_range?(value)
      function to_s (line 107) | def to_s
      function export (line 111) | def export
    type Percentable (line 118) | module Percentable
      function set_range_from_options (line 124) | def set_range_from_options(options)
      function to_s (line 135) | def to_s
      function export (line 141) | def export
    type PercentableDeprecated (line 151) | module PercentableDeprecated
      function initialize (line 153) | def initialize(options)
      function set_range_from_options (line 158) | def set_range_from_options(options)
      function offset (line 177) | def offset
      function percentage (line 185) | def percentage
      function value_in_range? (line 192) | def value_in_range?(value)
      function offset_from (line 201) | def offset_from
      function offset_to (line 205) | def offset_to
      function to_s (line 209) | def to_s
      function export (line 224) | def export
    type Experimentable (line 234) | module Experimentable
      function initialize_experiment (line 238) | def initialize_experiment(options)
      function value_in_bucket? (line 245) | def value_in_bucket?(value)
      function valid? (line 253) | def valid?
      function type (line 259) | def type
      function as_json (line 263) | def as_json(options = {})
      function to_s (line 272) | def to_s
      function export (line 277) | def export
      function inspect (line 281) | def inspect

FILE: lib/trebuchet/strategy/base.rb
  class Trebuchet::Strategy::Base (line 3) | class Trebuchet::Strategy::Base
    method name (line 7) | def name
    method feature_id (line 11) | def feature_id
    method needs_user? (line 15) | def needs_user?
    method strategy_name (line 19) | def self.strategy_name
    method as_json (line 23) | def as_json(options = {})
    method inspect (line 33) | def inspect
    method export (line 37) | def export(options = nil)

FILE: lib/trebuchet/strategy/custom.rb
  class Trebuchet::Strategy::Custom (line 1) | class Trebuchet::Strategy::Custom < Trebuchet::Strategy::Base
    method initialize (line 7) | def initialize(name, options = nil)
    method launch_at? (line 13) | def launch_at?(user, request = nil)
    method define (line 17) | def self.define(name, block)
    method exists? (line 21) | def self.exists?(name)
    method needs_user? (line 25) | def needs_user?
    method as_json (line 35) | def as_json(options = {})
    method to_s (line 39) | def to_s
    method export (line 43) | def export

FILE: lib/trebuchet/strategy/custom_request_aware.rb
  class Trebuchet::Strategy::CustomRequestAware (line 1) | class Trebuchet::Strategy::CustomRequestAware < Trebuchet::Strategy::Custom
    method initialize (line 4) | def initialize(name, options = nil)
    method define (line 10) | def self.define(name, block)
    method exists? (line 14) | def self.exists?(name)
    method launch_at? (line 18) | def launch_at?(user, request = nil)
    method to_s (line 23) | def to_s

FILE: lib/trebuchet/strategy/default.rb
  class Trebuchet::Strategy::Default (line 4) | class Trebuchet::Strategy::Default < Trebuchet::Strategy::Base
    method initialize (line 7) | def initialize(options = nil)
    method name (line 11) | def name
    method launch_at? (line 15) | def launch_at?(user, request = nil)
    method needs_user? (line 19) | def needs_user?
    method to_s (line 23) | def to_s

FILE: lib/trebuchet/strategy/everyone.rb
  class Trebuchet::Strategy::Everyone (line 3) | class Trebuchet::Strategy::Everyone < Trebuchet::Strategy::Base
    method initialize (line 6) | def initialize(options = nil)
    method name (line 10) | def name
    method launch_at? (line 14) | def launch_at?(user, request = nil)
    method needs_user? (line 18) | def needs_user?
    method to_s (line 22) | def to_s

FILE: lib/trebuchet/strategy/experiment.rb
  class Trebuchet::Strategy::Experiment (line 3) | class Trebuchet::Strategy::Experiment < Trebuchet::Strategy::Base
    method initialize (line 7) | def initialize(options = {})
    method launch_at? (line 11) | def launch_at?(user, request = nil)

FILE: lib/trebuchet/strategy/hostname.rb
  class Trebuchet::Strategy::Hostname (line 1) | class Trebuchet::Strategy::Hostname < Trebuchet::Strategy::Base
    method initialize (line 5) | def initialize(hostnames)
    method launch_at? (line 13) | def launch_at?(user, request = nil)
    method needs_user? (line 18) | def needs_user?
    method to_s (line 22) | def to_s
    method export (line 26) | def export

FILE: lib/trebuchet/strategy/invalid.rb
  class Trebuchet::Strategy::Invalid (line 2) | class Trebuchet::Strategy::Invalid < Trebuchet::Strategy::Base
    method initialize (line 6) | def initialize(name, options = nil)
    method name (line 11) | def name
    method launch_at? (line 15) | def launch_at?(user, request = nil)
    method needs_user? (line 19) | def needs_user?
    method to_s (line 23) | def to_s

FILE: lib/trebuchet/strategy/logic_and.rb
  class Trebuchet::Strategy::LogicAnd (line 3) | class Trebuchet::Strategy::LogicAnd < Trebuchet::Strategy::LogicBase
    method launch_at? (line 5) | def launch_at?(user, request = nil)

FILE: lib/trebuchet/strategy/logic_base.rb
  class Trebuchet::Strategy::LogicBase (line 1) | class Trebuchet::Strategy::LogicBase < Trebuchet::Strategy::Base
    method initialize (line 6) | def initialize(options = {})
    method feature= (line 15) | def feature=(f)
    method launch_at? (line 20) | def launch_at?(user, request = nil)
    method needs_user? (line 24) | def needs_user?

FILE: lib/trebuchet/strategy/logic_not.rb
  class Trebuchet::Strategy::LogicNot (line 3) | class Trebuchet::Strategy::LogicNot < Trebuchet::Strategy::LogicBase
    method launch_at? (line 5) | def launch_at?(user, request = nil)

FILE: lib/trebuchet/strategy/logic_or.rb
  class Trebuchet::Strategy::LogicOr (line 3) | class Trebuchet::Strategy::LogicOr < Trebuchet::Strategy::LogicBase
    method launch_at? (line 5) | def launch_at?(user, request = nil)

FILE: lib/trebuchet/strategy/multiple.rb
  class Trebuchet::Strategy::Multiple (line 1) | class Trebuchet::Strategy::Multiple < Trebuchet::Strategy::Base
    method initialize (line 5) | def initialize(args)
    method feature= (line 13) | def feature=(f)
    method launch_at? (line 18) | def launch_at?(user, request = nil)
    method as_json (line 23) | def as_json(options = {})
    method needs_user? (line 27) | def needs_user?
    method export (line 33) | def export

FILE: lib/trebuchet/strategy/nobody.rb
  class Trebuchet::Strategy::Nobody (line 3) | class Trebuchet::Strategy::Nobody < Trebuchet::Strategy::Base
    method initialize (line 6) | def initialize(options = nil)
    method name (line 10) | def name
    method launch_at? (line 14) | def launch_at?(user, request = nil)
    method needs_user? (line 18) | def needs_user?
    method to_s (line 22) | def to_s

FILE: lib/trebuchet/strategy/per_denomination.rb
  class Trebuchet::Strategy::PerDenomination (line 1) | class Trebuchet::Strategy::PerDenomination < Trebuchet::Strategy::Base
    method set_range_from_options (line 4) | def set_range_from_options(options = {})
    method launch_at? (line 11) | def launch_at?(user, request = nil)

FILE: lib/trebuchet/strategy/percent.rb
  class Trebuchet::Strategy::Percent (line 1) | class Trebuchet::Strategy::Percent < Trebuchet::Strategy::Base
    method initialize (line 5) | def initialize(options)
    method launch_at? (line 9) | def launch_at?(user, request = nil)

FILE: lib/trebuchet/strategy/percent_deprecated.rb
  class Trebuchet::Strategy::PercentDeprecated (line 1) | class Trebuchet::Strategy::PercentDeprecated < Trebuchet::Strategy::Base
    method initialize (line 5) | def initialize(options)
    method launch_at? (line 9) | def launch_at?(user, request = nil)

FILE: lib/trebuchet/strategy/stub.rb
  class Trebuchet (line 1) | class Trebuchet
    type Strategy (line 2) | module Strategy
      class Stub (line 4) | class Stub < Trebuchet::Strategy::Base
        method initialize (line 7) | def initialize(state)
        method launch_at? (line 11) | def launch_at?(user, request = nil)
        method needs_user? (line 15) | def needs_user?
        method to_s (line 19) | def to_s
        method export (line 23) | def export

FILE: lib/trebuchet/strategy/user_id.rb
  class Trebuchet::Strategy::UserId (line 1) | class Trebuchet::Strategy::UserId < Trebuchet::Strategy::Base
    method initialize (line 5) | def initialize(user_ids)
    method launch_at? (line 10) | def launch_at?(user, request = nil)
    method to_s (line 14) | def to_s
    method export (line 18) | def export

FILE: lib/trebuchet/strategy/visitor_experiment.rb
  class Trebuchet::Strategy::VisitorExperiment (line 1) | class Trebuchet::Strategy::VisitorExperiment < Trebuchet::Strategy::Base
    method initialize (line 5) | def initialize(options = {})
    method launch_at? (line 9) | def launch_at?(user, request = nil)
    method needs_user? (line 19) | def needs_user?

FILE: lib/trebuchet/strategy/visitor_percent.rb
  class Trebuchet::Strategy::VisitorPercent (line 1) | class Trebuchet::Strategy::VisitorPercent < Trebuchet::Strategy::Base
    method initialize (line 5) | def initialize(options)
    method launch_at? (line 9) | def launch_at?(user, request = nil)
    method needs_user? (line 20) | def needs_user?

FILE: lib/trebuchet/strategy/visitor_percent_deprecated.rb
  class Trebuchet::Strategy::VisitorPercentDeprecated (line 1) | class Trebuchet::Strategy::VisitorPercentDeprecated < Trebuchet::Strateg...
    method initialize (line 5) | def initialize(options)
    method launch_at? (line 9) | def launch_at?(user, request = nil)
    method needs_user? (line 20) | def needs_user?

FILE: lib/trebuchet/version.rb
  class Trebuchet (line 1) | class Trebuchet

FILE: lib/trebuchet_rails/engine.rb
  type TrebuchetRails (line 5) | module TrebuchetRails
    class Engine (line 6) | class Engine < Rails::Engine

FILE: spec/feature_spec.rb
  function feature (line 5) | def feature
  function feature_names (line 9) | def feature_names
  function archived_feature_names (line 13) | def archived_feature_names

FILE: spec/percent_deprecated_strategy_spec.rb
  function should_only_launch_to_a_percentage_of_users (line 19) | def should_only_launch_to_a_percentage_of_users(feature_name)
  function offset_ids (line 60) | def offset_ids(ids, offset)

FILE: spec/redis_hammerspaced_spec.rb
  function uid (line 58) | def hammerspace.uid
  function close (line 62) | def hammerspace.close

FILE: spec/spec_helper.rb
  class Redis (line 9) | class Redis < MockRedis ; end
  function should_launch (line 26) | def should_launch(feature, users)
  function should_not_launch (line 30) | def should_not_launch(feature, users)
  function should_or_should_not_launch (line 34) | def should_or_should_not_launch(feature, users, be_true_or_false)
  function mock_request (line 41) | def mock_request(cookie = nil)

FILE: spec/trebuchet_spec.rb
  class BoomError (line 96) | class BoomError < StandardError ; end

FILE: spec/user.rb
  class User (line 1) | class User < Struct.new(:id, :role)
    method has_role? (line 3) | def has_role?(role)

FILE: spec/visitor_percent_deprecated_strategy_spec.rb
  function should_launch_test (line 38) | def should_launch_test(feature_name)

FILE: spec/visitor_percent_strategy_spec.rb
  function should_launch_test (line 30) | def should_launch_test(feature_name)
Condensed preview — 81 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (125K chars).
[
  {
    "path": ".gitignore",
    "chars": 46,
    "preview": "*.gem\n.bundle\nGemfile.lock\npkg/*\n.idea\n.rspec\n"
  },
  {
    "path": ".travis.yml",
    "chars": 156,
    "preview": "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"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 1979,
    "preview": "## 0.12.0 (Nov 26, 2019)\n - Contain fiber-unsafe state of the main Trebuchet class into a state object and optionally st"
  },
  {
    "path": "Gemfile",
    "chars": 209,
    "preview": "source \"http://rubygems.org\"\n\n# Specify your gem's dependencies in trebuchet.gemspec\ngemspec\n\ncurrent_ruby = Gem::Versio"
  },
  {
    "path": "README.md",
    "chars": 3626,
    "preview": "Trebuchet\n=========\n\nTrebuchet launches features at people. Wisely choose a strategy, aim, and launch!\n\nInstallation\n---"
  },
  {
    "path": "Rakefile",
    "chars": 117,
    "preview": "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",
    "chars": 2046,
    "preview": "module TrebuchetRails\n\n  class FeaturesController < ApplicationController\n\n    if Rails::VERSION::MAJOR >= 5\n      befor"
  },
  {
    "path": "app/helpers/trebuchet_helper.rb",
    "chars": 589,
    "preview": "module TrebuchetHelper\n\n  def feature(feature)\n    \"<dd>#{feature.name}</dd>\n    <dt><ul>#{strategy feature.strategy}</u"
  },
  {
    "path": "app/views/layouts/trebuchet.html.erb",
    "chars": 179,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n  \t<title>Trebuchet</title>\n  \t<style type=\"text/css\">\n  \t\t<%= trebuchet_css %>\n  \t</sty"
  },
  {
    "path": "app/views/trebuchet_rails/features/index.html.erb",
    "chars": 2263,
    "preview": "<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"
  },
  {
    "path": "app/views/trebuchet_rails/features/timeline.html.erb",
    "chars": 474,
    "preview": "<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-w"
  },
  {
    "path": "app/views/trebuchet_rails/trebuchet.css",
    "chars": 1312,
    "preview": "#trebuchet {\n\tfont-family: HelveticaNeue, 'Helvetica Neue', HelveticaNeueRoman, HelveticaNeue-Roman, 'Helvetica Neue Rom"
  },
  {
    "path": "config/routes.rb",
    "chars": 437,
    "preview": "# TrebuchetRails::Engine.routes do\n\nroutes_block = lambda do\n  scope \"trebuchet\", :module => \"trebuchet_rails\" do\n    ge"
  },
  {
    "path": "init.rb",
    "chars": 609,
    "preview": "require 'trebuchet'\n\n# FIXME: happens too early\n# def set_trebuchet_namespace(app_name)\n#   if Trebuchet.backend.respond"
  },
  {
    "path": "lib/trebuchet/action_controller.rb",
    "chars": 283,
    "preview": "require 'trebuchet/action_controller_filter'\n\nmodule Trebuchet::ActionController\n\n  def self.included(base)\n    base.hel"
  },
  {
    "path": "lib/trebuchet/action_controller_filter.rb",
    "chars": 436,
    "preview": "class Trebuchet::ActionControllerFilter\n\n  def self.before(controller)\n    Trebuchet.initialize_logs\n\n    if Trebuchet.b"
  },
  {
    "path": "lib/trebuchet/backend/disabled.rb",
    "chars": 520,
    "preview": "# This backend stores nothing and returns empty/false data (launch? will always be false)\n# It can be used to disable al"
  },
  {
    "path": "lib/trebuchet/backend/memcached.rb",
    "chars": 660,
    "preview": "require 'memcache'\n\nclass Trebuchet::Backend::Memcached\n\n  attr_accessor :namespace\n\n  def initialize(*args)\n    @memcac"
  },
  {
    "path": "lib/trebuchet/backend/memory.rb",
    "chars": 959,
    "preview": "class Trebuchet::Backend::Memory\n\n  def initialize(*args)\n    @hash = {}\n    @archived = []\n  end\n\n  def get_strategy(fe"
  },
  {
    "path": "lib/trebuchet/backend/redis.rb",
    "chars": 4095,
    "preview": "require 'redis' unless defined?(Redis)\nrequire 'json'\n\nclass Trebuchet::Backend::Redis\n\n  attr_accessor :namespace\n\n  de"
  },
  {
    "path": "lib/trebuchet/backend/redis_cached.rb",
    "chars": 1290,
    "preview": "require 'trebuchet/backend/redis'\n\nclass Trebuchet::Backend::RedisCached < Trebuchet::Backend::Redis\n\n  # cache strategi"
  },
  {
    "path": "lib/trebuchet/backend/redis_hammerspaced.rb",
    "chars": 3683,
    "preview": "require 'trebuchet/backend/redis'\nrequire 'json'\n\nclass Trebuchet::Backend::RedisHammerspaced < Trebuchet::Backend::Redi"
  },
  {
    "path": "lib/trebuchet/backend.rb",
    "chars": 345,
    "preview": "module Trebuchet::Backend\n\n  def self.lookup(name)\n    # From ActiveSupport::Inflector.camelize\n    const_name = name.to"
  },
  {
    "path": "lib/trebuchet/error.rb",
    "chars": 168,
    "preview": "class Trebuchet::Error < StandardError ; end\nclass Trebuchet::BackendInitializationError < Trebuchet::Error ; end\nclass "
  },
  {
    "path": "lib/trebuchet/feature/stubbing.rb",
    "chars": 492,
    "preview": "class Trebuchet\n  class Feature\n    module Stubbing\n\n      def stub(state)\n        self.class.stubbed_features[name] = s"
  },
  {
    "path": "lib/trebuchet/feature.rb",
    "chars": 4488,
    "preview": "require 'trebuchet/feature/stubbing'\n\nclass Trebuchet::Feature\n  include Stubbing\n\n  @@deprecated_strategies_enabled = t"
  },
  {
    "path": "lib/trebuchet/state.rb",
    "chars": 202,
    "preview": "# Represents the internal, global and thread-unsafe state of Trebuchet\nTrebuchet::State = Struct.new(\n  :visitor_id, :cu"
  },
  {
    "path": "lib/trebuchet/strategy/base.rb",
    "chars": 674,
    "preview": "require 'digest/sha1'\n\nclass Trebuchet::Strategy::Base\n\n  attr_accessor :feature\n\n  def name\n    self.class.strategy_nam"
  },
  {
    "path": "lib/trebuchet/strategy/custom.rb",
    "chars": 932,
    "preview": "class Trebuchet::Strategy::Custom < Trebuchet::Strategy::Base\n\n  attr_reader :options, :custom_name\n\n  @@custom_strategi"
  },
  {
    "path": "lib/trebuchet/strategy/custom_request_aware.rb",
    "chars": 680,
    "preview": "class Trebuchet::Strategy::CustomRequestAware < Trebuchet::Strategy::Custom\n  @@custom_request_aware_strategies = {}\n\n  "
  },
  {
    "path": "lib/trebuchet/strategy/default.rb",
    "chars": 386,
    "preview": "require 'singleton'\n\n# Default is to not launch the feature to anyone\nclass Trebuchet::Strategy::Default < Trebuchet::St"
  },
  {
    "path": "lib/trebuchet/strategy/everyone.rb",
    "chars": 383,
    "preview": "require 'singleton'\n# Everyone is to launch the feature to everyone\nclass Trebuchet::Strategy::Everyone < Trebuchet::Str"
  },
  {
    "path": "lib/trebuchet/strategy/experiment.rb",
    "chars": 404,
    "preview": "# require 'digest/sha1'\n\nclass Trebuchet::Strategy::Experiment < Trebuchet::Strategy::Base\n  \n  include Trebuchet::Strat"
  },
  {
    "path": "lib/trebuchet/strategy/hostname.rb",
    "chars": 508,
    "preview": "class Trebuchet::Strategy::Hostname < Trebuchet::Strategy::Base\n\n  attr_reader :hostnames\n\n  def initialize(hostnames)\n "
  },
  {
    "path": "lib/trebuchet/strategy/invalid.rb",
    "chars": 453,
    "preview": "# Default is to not launch the feature to anyone\nclass Trebuchet::Strategy::Invalid < Trebuchet::Strategy::Base\n\n  attr_"
  },
  {
    "path": "lib/trebuchet/strategy/logic_and.rb",
    "chars": 257,
    "preview": "require 'trebuchet/strategy/logic_base'\n\nclass Trebuchet::Strategy::LogicAnd < Trebuchet::Strategy::LogicBase\n\n  def lau"
  },
  {
    "path": "lib/trebuchet/strategy/logic_base.rb",
    "chars": 646,
    "preview": "class Trebuchet::Strategy::LogicBase < Trebuchet::Strategy::Base\n\n  attr_reader :strategies\n  attr_reader :options\n\n  de"
  },
  {
    "path": "lib/trebuchet/strategy/logic_not.rb",
    "chars": 258,
    "preview": "require 'trebuchet/strategy/logic_base'\n\nclass Trebuchet::Strategy::LogicNot < Trebuchet::Strategy::LogicBase\n\n  def lau"
  },
  {
    "path": "lib/trebuchet/strategy/logic_or.rb",
    "chars": 256,
    "preview": "require 'trebuchet/strategy/logic_base'\n\nclass Trebuchet::Strategy::LogicOr < Trebuchet::Strategy::LogicBase\n\n  def laun"
  },
  {
    "path": "lib/trebuchet/strategy/multiple.rb",
    "chars": 951,
    "preview": "class Trebuchet::Strategy::Multiple < Trebuchet::Strategy::Base\n\n  attr_reader :strategies\n\n  def initialize(args)\n    @"
  },
  {
    "path": "lib/trebuchet/strategy/nobody.rb",
    "chars": 374,
    "preview": "require 'singleton'\n# Nobody is to launch the feature to nobody\nclass Trebuchet::Strategy::Nobody < Trebuchet::Strategy:"
  },
  {
    "path": "lib/trebuchet/strategy/per_denomination.rb",
    "chars": 570,
    "preview": "class Trebuchet::Strategy::PerDenomination < Trebuchet::Strategy::Base\n  include Trebuchet::Strategy::PerDenominationabl"
  },
  {
    "path": "lib/trebuchet/strategy/percent.rb",
    "chars": 364,
    "preview": "class Trebuchet::Strategy::Percent < Trebuchet::Strategy::Base\n\n  include Trebuchet::Strategy::Percentable\n\n  def initia"
  },
  {
    "path": "lib/trebuchet/strategy/percent_deprecated.rb",
    "chars": 385,
    "preview": "class Trebuchet::Strategy::PercentDeprecated < Trebuchet::Strategy::Base\n\n  include Trebuchet::Strategy::PercentableDepr"
  },
  {
    "path": "lib/trebuchet/strategy/stub.rb",
    "chars": 411,
    "preview": "class Trebuchet\n  module Strategy\n\n    class Stub < Trebuchet::Strategy::Base\n      attr_reader :state\n\n      def initia"
  },
  {
    "path": "lib/trebuchet/strategy/user_id.rb",
    "chars": 410,
    "preview": "class Trebuchet::Strategy::UserId < Trebuchet::Strategy::Base\n\n  attr_reader :user_ids\n\n  def initialize(user_ids)\n    u"
  },
  {
    "path": "lib/trebuchet/strategy/visitor_experiment.rb",
    "chars": 556,
    "preview": "class Trebuchet::Strategy::VisitorExperiment < Trebuchet::Strategy::Base\n\n  include Trebuchet::Strategy::Experimentable\n"
  },
  {
    "path": "lib/trebuchet/strategy/visitor_percent.rb",
    "chars": 546,
    "preview": "class Trebuchet::Strategy::VisitorPercent < Trebuchet::Strategy::Base\n\n  include Trebuchet::Strategy::Percentable\n\n  def"
  },
  {
    "path": "lib/trebuchet/strategy/visitor_percent_deprecated.rb",
    "chars": 567,
    "preview": "class Trebuchet::Strategy::VisitorPercentDeprecated < Trebuchet::Strategy::Base\n\n  include Trebuchet::Strategy::Percenta"
  },
  {
    "path": "lib/trebuchet/strategy.rb",
    "chars": 7777,
    "preview": "require 'digest/sha1'\n\nmodule Trebuchet::Strategy\n\n  def self.for_feature(feature)\n    stub_state = Trebuchet::Feature.s"
  },
  {
    "path": "lib/trebuchet/version.rb",
    "chars": 50,
    "preview": "class Trebuchet\n\n  VERSION = \"0.12.1\".freeze\n\nend\n"
  },
  {
    "path": "lib/trebuchet.rb",
    "chars": 5905,
    "preview": "require 'digest/sha1'\nrequire 'forwardable'\nclass Trebuchet\n  # initialize a single one to save object allocations\n  # T"
  },
  {
    "path": "lib/trebuchet_rails/engine.rb",
    "chars": 247,
    "preview": "require 'rails'\nrequire File.expand_path(File.dirname(__FILE__) + \"/../trebuchet\")\n\n\nmodule TrebuchetRails\n  class Engin"
  },
  {
    "path": "lib/trebuchet_rails.rb",
    "chars": 53,
    "preview": "require 'trebuchet_rails/engine'\n\nrequire 'trebuchet'"
  },
  {
    "path": "spec/custom_request_aware_strategy_spec.rb",
    "chars": 1446,
    "preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::CustomRequestAware do\n  it \"should launch according to the custom s"
  },
  {
    "path": "spec/custom_strategy_spec.rb",
    "chars": 1811,
    "preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::Custom do\n\n  it \"should launch according to the custom strategy\" do"
  },
  {
    "path": "spec/default_strategy_spec.rb",
    "chars": 746,
    "preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::Default do\n\n  it \"should not launch if no strategy was defined\" do\n"
  },
  {
    "path": "spec/disabled_backend_spec.rb",
    "chars": 653,
    "preview": "require 'spec_helper'\n\ndescribe Trebuchet::Backend::Disabled do\n  \n  before do\n    Trebuchet.backend = :disabled\n    Tre"
  },
  {
    "path": "spec/everyone_strategy_spec.rb",
    "chars": 660,
    "preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::Everyone do\n\n  it \"should be named everyone\" do\n    Trebuchet::Stra"
  },
  {
    "path": "spec/experiment_strategy_spec.rb",
    "chars": 4704,
    "preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::Experiment do\n  \n  before do\n    @feature_name = \"Photographic Memo"
  },
  {
    "path": "spec/feature_spec.rb",
    "chars": 4917,
    "preview": "require 'spec_helper'\n\ndescribe Trebuchet::Feature do\n  \n  def feature\n    Trebuchet.feature('some_feature')\n  end\n\n  de"
  },
  {
    "path": "spec/logic_and_strategy_spec.rb",
    "chars": 1304,
    "preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::LogicAnd do\n\n  it \"passes the check only when all conditions are me"
  },
  {
    "path": "spec/logic_base_strategy_spec.rb",
    "chars": 921,
    "preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::LogicBase do\n\n  it \"should set @feature on sub-strategies\" do\n    f"
  },
  {
    "path": "spec/logic_not_strategy_spec.rb",
    "chars": 383,
    "preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::LogicNot do\n\n  it \"works expectedly as a not operator\" do\n    Trebu"
  },
  {
    "path": "spec/logic_or_strategy_spec.rb",
    "chars": 1295,
    "preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::LogicOr do\n\n  it \"passes the check as long as one of the children s"
  },
  {
    "path": "spec/multiple_strategy_spec.rb",
    "chars": 1883,
    "preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::Multiple do\n\n  it \"should support chaining strategies\" do\n    Trebu"
  },
  {
    "path": "spec/nobody_strategy_spec.rb",
    "chars": 648,
    "preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::Nobody do\n\n  it \"should be named nobody\" do\n    Trebuchet::Strategy"
  },
  {
    "path": "spec/per_denomination_strategy_spec.rb",
    "chars": 4251,
    "preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::PerDenomination do\n  describe 'launch' do\n    before(:each) do\n    "
  },
  {
    "path": "spec/percent_deprecated_strategy_spec.rb",
    "chars": 4946,
    "preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::PercentDeprecated do\n\n  it \"should be deprecated\" do\n    Trebuchet:"
  },
  {
    "path": "spec/percent_strategy_spec.rb",
    "chars": 3001,
    "preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::Percent do\n\n  it \"should not launch to unsaved users, users with no"
  },
  {
    "path": "spec/redis_backend_spec.rb",
    "chars": 1370,
    "preview": "require 'spec_helper'\nrequire 'mock_redis'\nrequire 'trebuchet/backend/redis'\n\n\ndescribe Trebuchet::Backend::Redis do\n\n  "
  },
  {
    "path": "spec/redis_hammerspaced_spec.rb",
    "chars": 4074,
    "preview": "require 'spec_helper'\nrequire 'mock_redis'\nrequire 'trebuchet/backend/redis_hammerspaced'\n\n\ndescribe Trebuchet::Backend:"
  },
  {
    "path": "spec/spec_helper.rb",
    "chars": 1063,
    "preview": "$:.unshift File.dirname(__FILE__) + '/../lib'\n\nrequire 'bundler'\n\n# Bundler breaks things\nBundler.require :default, :tes"
  },
  {
    "path": "spec/stubbing_spec.rb",
    "chars": 2236,
    "preview": "require 'spec_helper'\n\ndescribe Trebuchet::Feature::Stubbing do\n\n  before do\n    Trebuchet.dismantle_stubs\n  end\n\n  desc"
  },
  {
    "path": "spec/trebuchet_spec.rb",
    "chars": 5779,
    "preview": "require 'spec_helper'\n\ndescribe Trebuchet do\n\n  describe \"launch?\" do\n\n    it \"should call launch_at? on feature\" do\n   "
  },
  {
    "path": "spec/user.rb",
    "chars": 92,
    "preview": "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",
    "chars": 1250,
    "preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::UserId do\n\n  it \"should only launch to designated users\" do\n    Tre"
  },
  {
    "path": "spec/visitor_experiment_strategy_spec.rb",
    "chars": 1457,
    "preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::VisitorExperiment do\n  \n  before do\n    @feature_name = \"Infrared V"
  },
  {
    "path": "spec/visitor_percent_deprecated_strategy_spec.rb",
    "chars": 4484,
    "preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::VisitorPercentDeprecated do\n\n  it \"should be deprecated\" do\n    Tre"
  },
  {
    "path": "spec/visitor_percent_strategy_spec.rb",
    "chars": 2497,
    "preview": "require 'spec_helper'\n\ndescribe Trebuchet::Strategy::VisitorPercent do\n\n  it \"should not break if no visitor id is set\" "
  },
  {
    "path": "trebuchet.gemspec",
    "chars": 957,
    "preview": "# -*- encoding: utf-8 -*-\n$:.push File.expand_path(\"../lib\", __FILE__)\nrequire 'trebuchet'\n\nGem::Specification.new do |s"
  }
]

About this extraction

This page contains the full source code of the airbnb/trebuchet GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 81 files (112.8 KB), approximately 33.7k tokens, and a symbol index with 309 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!