mikeg-vanity 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/CHANGELOG +153 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README.rdoc +83 -0
  4. data/bin/vanity +53 -0
  5. data/lib/vanity.rb +38 -0
  6. data/lib/vanity/backport.rb +43 -0
  7. data/lib/vanity/commands.rb +2 -0
  8. data/lib/vanity/commands/list.rb +21 -0
  9. data/lib/vanity/commands/report.rb +60 -0
  10. data/lib/vanity/experiment/ab_test.rb +477 -0
  11. data/lib/vanity/experiment/base.rb +212 -0
  12. data/lib/vanity/helpers.rb +59 -0
  13. data/lib/vanity/metric/active_record.rb +77 -0
  14. data/lib/vanity/metric/base.rb +221 -0
  15. data/lib/vanity/metric/google_analytics.rb +70 -0
  16. data/lib/vanity/mock_redis.rb +76 -0
  17. data/lib/vanity/playground.rb +197 -0
  18. data/lib/vanity/rails.rb +22 -0
  19. data/lib/vanity/rails/dashboard.rb +24 -0
  20. data/lib/vanity/rails/helpers.rb +158 -0
  21. data/lib/vanity/rails/testing.rb +11 -0
  22. data/lib/vanity/templates/_ab_test.erb +26 -0
  23. data/lib/vanity/templates/_experiment.erb +5 -0
  24. data/lib/vanity/templates/_experiments.erb +7 -0
  25. data/lib/vanity/templates/_metric.erb +14 -0
  26. data/lib/vanity/templates/_metrics.erb +13 -0
  27. data/lib/vanity/templates/_report.erb +27 -0
  28. data/lib/vanity/templates/flot.min.js +1 -0
  29. data/lib/vanity/templates/jquery.min.js +19 -0
  30. data/lib/vanity/templates/vanity.css +26 -0
  31. data/lib/vanity/templates/vanity.js +82 -0
  32. data/test/ab_test_test.rb +656 -0
  33. data/test/experiment_test.rb +136 -0
  34. data/test/experiments/age_and_zipcode.rb +19 -0
  35. data/test/experiments/metrics/cheers.rb +3 -0
  36. data/test/experiments/metrics/signups.rb +2 -0
  37. data/test/experiments/metrics/yawns.rb +3 -0
  38. data/test/experiments/null_abc.rb +5 -0
  39. data/test/metric_test.rb +518 -0
  40. data/test/playground_test.rb +10 -0
  41. data/test/rails_test.rb +104 -0
  42. data/test/test_helper.rb +135 -0
  43. data/vanity.gemspec +18 -0
  44. data/vendor/redis-rb/LICENSE +20 -0
  45. data/vendor/redis-rb/README.markdown +36 -0
  46. data/vendor/redis-rb/Rakefile +62 -0
  47. data/vendor/redis-rb/bench.rb +44 -0
  48. data/vendor/redis-rb/benchmarking/suite.rb +24 -0
  49. data/vendor/redis-rb/benchmarking/worker.rb +71 -0
  50. data/vendor/redis-rb/bin/distredis +33 -0
  51. data/vendor/redis-rb/examples/basic.rb +16 -0
  52. data/vendor/redis-rb/examples/incr-decr.rb +18 -0
  53. data/vendor/redis-rb/examples/list.rb +26 -0
  54. data/vendor/redis-rb/examples/sets.rb +36 -0
  55. data/vendor/redis-rb/lib/dist_redis.rb +124 -0
  56. data/vendor/redis-rb/lib/hash_ring.rb +128 -0
  57. data/vendor/redis-rb/lib/pipeline.rb +21 -0
  58. data/vendor/redis-rb/lib/redis.rb +370 -0
  59. data/vendor/redis-rb/lib/redis/raketasks.rb +1 -0
  60. data/vendor/redis-rb/profile.rb +22 -0
  61. data/vendor/redis-rb/redis-rb.gemspec +30 -0
  62. data/vendor/redis-rb/spec/redis_spec.rb +637 -0
  63. data/vendor/redis-rb/spec/spec_helper.rb +4 -0
  64. data/vendor/redis-rb/speed.rb +16 -0
  65. data/vendor/redis-rb/tasks/redis.tasks.rb +140 -0
  66. metadata +125 -0
@@ -0,0 +1,10 @@
1
+ require "test/test_helper"
2
+
3
+ class PlaygroundTest < Test::Unit::TestCase
4
+
5
+ def test_has_one_global_instance
6
+ assert instance = Vanity.playground
7
+ assert_equal instance, Vanity.playground
8
+ end
9
+
10
+ end
@@ -0,0 +1,104 @@
1
+ require "test/test_helper"
2
+
3
+ class UseVanityController < ActionController::Base
4
+ attr_accessor :current_user
5
+
6
+ def index
7
+ render :text=>ab_test(:pie_or_cake)
8
+ end
9
+ end
10
+
11
+ # Pages accessible to everyone, e.g. sign in, community search.
12
+ class UseVanityTest < ActionController::TestCase
13
+ tests UseVanityController
14
+
15
+ def setup
16
+ super
17
+ metric :sugar_high
18
+ new_ab_test :pie_or_cake do
19
+ metrics :sugar_high
20
+ end
21
+ UseVanityController.class_eval do
22
+ use_vanity :current_user
23
+ end
24
+ end
25
+
26
+ def test_vanity_cookie_is_persistent
27
+ get :index
28
+ assert cookie = @response["Set-Cookie"].find { |c| c[/^vanity_id=/] }
29
+ assert expires = cookie[/vanity_id=[a-f0-9]{32}; path=\/; expires=(.*)(;|$)/, 1]
30
+ assert_in_delta Time.parse(expires), Time.now + 1.month, 1.minute
31
+ end
32
+
33
+ def test_vanity_cookie_default_id
34
+ get :index
35
+ assert cookies['vanity_id'] =~ /^[a-f0-9]{32}$/
36
+ end
37
+
38
+ def test_vanity_cookie_retains_id
39
+ @request.cookies['vanity_id'] = "from_last_time"
40
+ get :index
41
+ assert_equal "from_last_time", cookies['vanity_id']
42
+ end
43
+
44
+ def test_vanity_identity_set_from_cookie
45
+ @request.cookies['vanity_id'] = "from_last_time"
46
+ get :index
47
+ assert_equal "from_last_time", @controller.send(:vanity_identity)
48
+ end
49
+
50
+ def test_vanity_identity_set_from_user
51
+ @controller.current_user = mock("user", :id=>"user_id")
52
+ get :index
53
+ assert_equal "user_id", @controller.send(:vanity_identity)
54
+ end
55
+
56
+ def test_vanity_identity_with_no_user_model
57
+ UseVanityController.class_eval do
58
+ use_vanity nil
59
+ end
60
+ @controller.current_user = Object.new
61
+ get :index
62
+ assert cookies['vanity_id'] =~ /^[a-f0-9]{32}$/
63
+ end
64
+
65
+ def test_vanity_identity_set_with_block
66
+ UseVanityController.class_eval do
67
+ attr_accessor :project_id
68
+ use_vanity { |controller| controller.project_id }
69
+ end
70
+ @controller.project_id = "576"
71
+ get :index
72
+ assert_equal "576", @controller.send(:vanity_identity)
73
+ end
74
+
75
+ # query parameter filter
76
+
77
+ def test_redirects_and_loses_vanity_query_parameter
78
+ get :index, :foo=>"bar", :_vanity=>"567"
79
+ assert_redirected_to "/use_vanity?foo=bar"
80
+ end
81
+
82
+ def test_sets_choices_from_vanity_query_parameter
83
+ first = experiment(:pie_or_cake).alternatives.first
84
+ # experiment(:pie_or_cake).fingerprint(first)
85
+ 10.times do
86
+ @controller = nil ; setup_controller_request_and_response
87
+ get :index, :_vanity=>"aae9ff8081"
88
+ assert !experiment(:pie_or_cake).choose
89
+ assert experiment(:pie_or_cake).showing?(first)
90
+ end
91
+ end
92
+
93
+ def test_does_nothing_with_vanity_query_parameter_for_posts
94
+ first = experiment(:pie_or_cake).alternatives.first
95
+ post :index, :foo=>"bar", :_vanity=>"567"
96
+ assert_response :success
97
+ assert !experiment(:pie_or_cake).showing?(first)
98
+ end
99
+
100
+ def teardown
101
+ super
102
+ UseVanityController.send(:filter_chain).clear
103
+ end
104
+ end
@@ -0,0 +1,135 @@
1
+ $LOAD_PATH.delete_if { |path| path[/gems\/vanity-\d/] }
2
+ $LOAD_PATH.unshift File.expand_path("../lib", File.dirname(__FILE__))
3
+ RAILS_ROOT = File.expand_path("..")
4
+ require "test/unit"
5
+ require "mocha"
6
+ require "action_controller"
7
+ require "action_controller/test_case"
8
+ require "active_record"
9
+ require "initializer"
10
+ Rails.configuration = Rails::Configuration.new
11
+ require "lib/vanity"
12
+ require "timecop"
13
+
14
+
15
+ if $VERBOSE
16
+ $logger = Logger.new(STDOUT)
17
+ $logger.level = Logger::DEBUG
18
+ else
19
+ $logger = Logger.new("/dev/null")
20
+ end
21
+
22
+ class Test::Unit::TestCase
23
+
24
+ def setup
25
+ FileUtils.mkpath "tmp/experiments/metrics"
26
+ new_playground
27
+ end
28
+
29
+ # Call this on teardown. It wipes put the playground and any state held in it
30
+ # (mostly experiments), resets vanity ID, and clears Redis of all experiments.
31
+ def nuke_playground
32
+ new_playground
33
+ Vanity.playground.redis.flushdb
34
+ end
35
+
36
+ # Call this if you need a new playground, e.g. to re-define the same experiment,
37
+ # or reload an experiment (saved by the previous playground).
38
+ def new_playground
39
+ Vanity.playground = Vanity::Playground.new(:logger=>$logger, :load_path=>"tmp/experiments", :db=>15)
40
+ Vanity.playground.mock! unless ENV["REDIS"]
41
+ end
42
+
43
+ # Defines the specified metrics (one or more names). Returns metric, or array
44
+ # of metric (if more than one argument).
45
+ def metric(*names)
46
+ metrics = names.map do |name|
47
+ id = name.to_s.downcase.gsub(/\W+/, '_').to_sym
48
+ Vanity.playground.metrics[id] ||= Vanity::Metric.new(Vanity.playground, name)
49
+ end
50
+ names.size == 1 ? metrics.first : metrics
51
+ end
52
+
53
+ # Defines an A/B experiment.
54
+ def new_ab_test(name, &block)
55
+ id = name.to_s.downcase.gsub(/\W/, "_").to_sym
56
+ experiment = Vanity::Experiment::AbTest.new(Vanity.playground, id, name)
57
+ experiment.instance_eval &block
58
+ experiment.save
59
+ Vanity.playground.experiments[id] = experiment
60
+ end
61
+
62
+ # Returns named experiment.
63
+ def experiment(name)
64
+ Vanity.playground.experiment(name)
65
+ end
66
+
67
+ def teardown
68
+ Vanity.context = nil
69
+ FileUtils.rm_rf "tmp"
70
+ Vanity.playground.redis.flushdb
71
+ end
72
+
73
+ end
74
+
75
+ ActionController::Routing::Routes.draw do |map|
76
+ map.connect ':controller/:action/:id'
77
+ end
78
+
79
+
80
+ ActiveRecord::Base.logger = $logger
81
+ ActiveRecord::Base.establish_connection :adapter=>"sqlite3", :database=>File.expand_path("database.sqlite")
82
+ # Call this to define aggregate functions not available in SQlite.
83
+ class ActiveRecord::Base
84
+ def self.aggregates
85
+ connection.raw_connection.create_aggregate("minimum", 1) do
86
+ step do |func, value|
87
+ func[:minimum] = value.to_i unless func[:minimum] && func[:minimum].to_i < value.to_i
88
+ end
89
+ finalize { |func| func.result = func[:minimum] }
90
+ end
91
+
92
+ connection.raw_connection.create_aggregate("maximum", 1) do
93
+ step do |func, value|
94
+ func[:maximum] = value.to_i unless func[:maximum] && func[:maximum].to_i > value.to_i
95
+ end
96
+ finalize { |func| func.result = func[:maximum] }
97
+ end
98
+
99
+ connection.raw_connection.create_aggregate("average", 1) do
100
+ step do |func, value|
101
+ func[:total] = func[:total].to_i + value.to_i
102
+ func[:count] = func[:count].to_i + 1
103
+ end
104
+ finalize { |func| func.result = func[:total].to_i / func[:count].to_i }
105
+ end
106
+ end
107
+ end
108
+
109
+
110
+ class Array
111
+ # Not in Ruby 1.8.6.
112
+ unless method_defined?(:shuffle)
113
+ def shuffle
114
+ copy = clone
115
+ Array.new(size) { copy.delete_at(Kernel.rand(copy.size)) }
116
+ end
117
+ end
118
+ end
119
+
120
+
121
+ # Source: http://gist.github.com/25455
122
+ def context(*args, &block)
123
+ return super unless (name = args.first) && block
124
+ parent = Class === self ? self : (defined?(ActiveSupport::TestCase) ? ActiveSupport::TestCase : Test::Unit::TestCase)
125
+ klass = Class.new(parent) do
126
+ def self.test(name, &block)
127
+ define_method("test_#{name.gsub(/\W/,'_')}", &block) if block
128
+ end
129
+ def self.xtest(*args) end
130
+ def self.setup(&block) define_method(:setup) { super() ; block.call } end
131
+ def self.teardown(&block) define_method(:teardown) { super() ; block.call } end
132
+ end
133
+ parent.const_set name.split(/\W+/).map(&:capitalize).join, klass
134
+ klass.class_eval &block
135
+ end
@@ -0,0 +1,18 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "mikeg-vanity"
3
+ spec.version = "1.3.0"
4
+ spec.author = "Assaf Arkin"
5
+ spec.email = "assaf@labnotes.org"
6
+ spec.homepage = "http://vanity.labnotes.org"
7
+ spec.summary = "Experience Driven Development framework for Rails"
8
+ spec.description = "Mirror, mirror on the wall ..."
9
+ spec.post_install_message = "To get started run vanity --help"
10
+
11
+ spec.files = Dir["{bin,lib,vendor,test}/**/*", "CHANGELOG", "MIT-LICENSE", "README.rdoc", "vanity.gemspec"]
12
+ spec.executable = "vanity"
13
+
14
+ spec.has_rdoc = true
15
+ spec.extra_rdoc_files = "README.rdoc", "CHANGELOG"
16
+ spec.rdoc_options = "--title", "Vanity #{spec.version}", "--main", "README.rdoc",
17
+ "--webcvs", "http://github.com/assaf/#{spec.name}"
18
+ end
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Ezra Zygmuntowicz
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,36 @@
1
+ # redis-rb
2
+
3
+ A ruby client library for the redis key value storage system.
4
+
5
+ ## Information about redis
6
+
7
+ Redis is a key value store with some interesting features:
8
+ 1. It's fast.
9
+ 2. Keys are strings but values can have types of "NONE", "STRING", "LIST", or "SET". List's can be atomically push'd, pop'd, lpush'd, lpop'd and indexed. This allows you to store things like lists of comments under one key while retaining the ability to append comments without reading and putting back the whole list.
10
+
11
+ See [redis on code.google.com](http://code.google.com/p/redis/wiki/README) for more information.
12
+
13
+ See the build on [RunCodeRun](http://runcoderun.com/rsanheim/redis-rb)
14
+
15
+ ## Dependencies
16
+
17
+ 1. rspec -
18
+ sudo gem install rspec
19
+
20
+ 2. redis -
21
+
22
+ rake redis:install
23
+
24
+ 2. dtach -
25
+
26
+ rake dtach:install
27
+
28
+ 3. git - git is the new black.
29
+
30
+ ## Setup
31
+
32
+ Use the tasks mentioned above (in Dependencies) to get your machine setup.
33
+
34
+ ## Examples
35
+
36
+ Check the examples/ directory. *Note* you need to have redis-server running first.
@@ -0,0 +1,62 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+ require 'rubygems/specification'
4
+ require 'date'
5
+ require 'spec/rake/spectask'
6
+ require 'tasks/redis.tasks'
7
+
8
+
9
+ GEM = 'redis'
10
+ GEM_NAME = 'redis'
11
+ GEM_VERSION = '0.1'
12
+ AUTHORS = ['Ezra Zygmuntowicz', 'Taylor Weibley', 'Matthew Clark', 'Brian McKinney', 'Salvatore Sanfilippo', 'Luca Guidi']
13
+ EMAIL = "ez@engineyard.com"
14
+ HOMEPAGE = "http://github.com/ezmobius/redis-rb"
15
+ SUMMARY = "Ruby client library for redis key value storage server"
16
+
17
+ spec = Gem::Specification.new do |s|
18
+ s.name = GEM
19
+ s.version = GEM_VERSION
20
+ s.platform = Gem::Platform::RUBY
21
+ s.has_rdoc = true
22
+ s.extra_rdoc_files = ["LICENSE"]
23
+ s.summary = SUMMARY
24
+ s.description = s.summary
25
+ s.authors = AUTHORS
26
+ s.email = EMAIL
27
+ s.homepage = HOMEPAGE
28
+ s.add_dependency "rspec"
29
+ s.require_path = 'lib'
30
+ s.autorequire = GEM
31
+ s.files = %w(LICENSE README.markdown Rakefile) + Dir.glob("{lib,tasks,spec}/**/*")
32
+ end
33
+
34
+ task :default => :spec
35
+
36
+ desc "Run specs"
37
+ Spec::Rake::SpecTask.new do |t|
38
+ t.spec_files = FileList['spec/**/*_spec.rb']
39
+ t.spec_opts = %w(-fs --color)
40
+ end
41
+
42
+ Rake::GemPackageTask.new(spec) do |pkg|
43
+ pkg.gem_spec = spec
44
+ end
45
+
46
+ desc "install the gem locally"
47
+ task :install => [:package] do
48
+ sh %{sudo gem install pkg/#{GEM}-#{GEM_VERSION}}
49
+ end
50
+
51
+ desc "create a gemspec file"
52
+ task :make_spec do
53
+ File.open("#{GEM}.gemspec", "w") do |file|
54
+ file.puts spec.to_ruby
55
+ end
56
+ end
57
+
58
+ desc "Run all examples with RCov"
59
+ Spec::Rake::SpecTask.new(:rcov) do |t|
60
+ t.spec_files = FileList['spec/**/*_spec.rb']
61
+ t.rcov = true
62
+ end
@@ -0,0 +1,44 @@
1
+ require 'benchmark'
2
+ $:.push File.join(File.dirname(__FILE__), 'lib')
3
+ require 'redis'
4
+
5
+ times = 20000
6
+
7
+ @r = Redis.new#(:debug => true)
8
+ @r['foo'] = "The first line we sent to the server is some text"
9
+
10
+ Benchmark.bmbm do |x|
11
+ x.report("set") do
12
+ 20000.times do |i|
13
+ @r["set#{i}"] = "The first line we sent to the server is some text"; @r["foo#{i}"]
14
+ end
15
+ end
16
+
17
+ x.report("set (pipelined)") do
18
+ @r.pipelined do |pipeline|
19
+ 20000.times do |i|
20
+ pipeline["set_pipelined#{i}"] = "The first line we sent to the server is some text"; @r["foo#{i}"]
21
+ end
22
+ end
23
+ end
24
+
25
+ x.report("push+trim") do
26
+ 20000.times do |i|
27
+ @r.push_head "push_trim#{i}", i
28
+ @r.list_trim "push_trim#{i}", 0, 30
29
+ end
30
+ end
31
+
32
+ x.report("push+trim (pipelined)") do
33
+ @r.pipelined do |pipeline|
34
+ 20000.times do |i|
35
+ pipeline.push_head "push_trim_pipelined#{i}", i
36
+ pipeline.list_trim "push_trim_pipelined#{i}", 0, 30
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ @r.keys('*').each do |k|
43
+ @r.delete k
44
+ end
@@ -0,0 +1,24 @@
1
+ require 'fileutils'
2
+
3
+ def run_in_background(command)
4
+ fork { system command }
5
+ end
6
+
7
+ def with_all_segments(&block)
8
+ 0.upto(9) do |segment_number|
9
+ block_size = 100000
10
+ start_index = segment_number * block_size
11
+ end_index = start_index + block_size - 1
12
+ block.call(start_index, end_index)
13
+ end
14
+ end
15
+
16
+ #with_all_segments do |start_index, end_index|
17
+ # puts "Initializing keys from #{start_index} to #{end_index}"
18
+ # system "ruby worker.rb initialize #{start_index} #{end_index} 0"
19
+ #end
20
+
21
+ with_all_segments do |start_index, end_index|
22
+ run_in_background "ruby worker.rb write #{start_index} #{end_index} 10"
23
+ run_in_background "ruby worker.rb read #{start_index} #{end_index} 1"
24
+ end