sapling 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/README.md +12 -0
- data/Rakefile +10 -0
- data/db/create.sql +6 -0
- data/lib/sapling.rb +10 -0
- data/lib/sapling/active_record.rb +53 -0
- data/lib/sapling/active_record_model.rb +8 -0
- data/lib/sapling/api.rb +12 -0
- data/lib/sapling/base.rb +4 -0
- data/lib/sapling/memory.rb +82 -0
- data/lib/sapling/util.rb +22 -0
- data/lib/sapling/version.rb +3 -0
- data/sapling.gemspec +26 -0
- data/spec/active_record_spec.rb +42 -0
- data/spec/memory_feature_spec.rb +13 -0
- data/spec/memory_spec.rb +35 -0
- data/spec/sapling_examples.rb +86 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +19 -0
- metadata +155 -0
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
Sapling Gem
|
2
|
+
===========
|
3
|
+
|
4
|
+
Sapling lets you seed your new features to just a few users at a time. You can change which and how many users are
|
5
|
+
seeded for a feature dynamically by updating the database via the Sapling API. Core features are the ability to
|
6
|
+
seed a feature for specific users and/or a percentage of users.
|
7
|
+
|
8
|
+
Heritage
|
9
|
+
--------
|
10
|
+
|
11
|
+
This is a port of the [rollout gem](https://github.com/jamesgolick/rollout) for
|
12
|
+
use with ActiveRecord instead of Redis. We dropped the groups functionality, but otherwise we mirrored the API.
|
data/Rakefile
ADDED
data/db/create.sql
ADDED
data/lib/sapling.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
module Sapling
|
2
|
+
class ActiveRecord < Base
|
3
|
+
|
4
|
+
def table_name
|
5
|
+
Model.table_name
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClientAPI
|
9
|
+
# see Sapling::API::Client
|
10
|
+
def active?(feature, options={})
|
11
|
+
options = Util.normalized_options options
|
12
|
+
v =Model.count(:conditions => [
|
13
|
+
"feature = ? AND ((user_id IS NOT NULL AND user_id = ?) OR (percentage IS NOT NULL AND ? < percentage)) ",
|
14
|
+
feature,
|
15
|
+
(u=options[:user]) && u.id,
|
16
|
+
((c=options[:context_id]) && c%100) || 100
|
17
|
+
])
|
18
|
+
v > 0
|
19
|
+
end
|
20
|
+
end
|
21
|
+
include ClientAPI
|
22
|
+
|
23
|
+
module AdminAPI
|
24
|
+
|
25
|
+
def activate_user(feature, user)
|
26
|
+
Model.transaction do
|
27
|
+
deactivate_user(feature, user)
|
28
|
+
Model.create(:feature => feature, :user_id => user.id)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def deactivate_user(feature, user)
|
33
|
+
Model.delete_all ["feature = ? AND percentage IS NULL and user_id = ?",feature,user.id]
|
34
|
+
end
|
35
|
+
|
36
|
+
def activate_percentage(feature, percentage)
|
37
|
+
raise "invalid percentage #{percentage.inspect}" unless percentage.kind_of?(Integer) && percentage>=0 && percentage<=100
|
38
|
+
Model.transaction do
|
39
|
+
deactivate_percentage(feature)
|
40
|
+
Model.create(:feature => feature, :percentage => percentage)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def deactivate_percentage(feature)
|
45
|
+
Model.delete_all ["feature = ? AND percentage IS NOT NULL AND user_id IS NULL",
|
46
|
+
feature
|
47
|
+
]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
include AdminAPI
|
52
|
+
end
|
53
|
+
end
|
data/lib/sapling/api.rb
ADDED
data/lib/sapling/base.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
module Sapling
|
2
|
+
class Memory < Base
|
3
|
+
|
4
|
+
class Feature
|
5
|
+
attr_accessor :users,:percentage
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@users={}
|
9
|
+
self.percentage = 0
|
10
|
+
end
|
11
|
+
|
12
|
+
# see Sapling::API::Client
|
13
|
+
def active?(options={})
|
14
|
+
options = Util::normalized_options(options)
|
15
|
+
individually_active?(options[:user]) || percentage_active?(options)
|
16
|
+
end
|
17
|
+
|
18
|
+
def percentage_active?(options={})
|
19
|
+
(Util.context_id(options) % 100) < percentage
|
20
|
+
end
|
21
|
+
|
22
|
+
def individually_active?(user)
|
23
|
+
user && users[user.id]
|
24
|
+
end
|
25
|
+
|
26
|
+
def activate_user(user)
|
27
|
+
users[user.id]=true
|
28
|
+
end
|
29
|
+
|
30
|
+
def deactivate_user(user)
|
31
|
+
users.delete(user.id)
|
32
|
+
end
|
33
|
+
|
34
|
+
def activate_percentage(percentage)
|
35
|
+
@percentage=percentage
|
36
|
+
end
|
37
|
+
|
38
|
+
def deactivate_percentage
|
39
|
+
@percentage=0
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_accessor :features
|
44
|
+
|
45
|
+
def initialize
|
46
|
+
@features={}
|
47
|
+
end
|
48
|
+
|
49
|
+
module ClientAPI
|
50
|
+
# see Sapling::API::Client
|
51
|
+
def active?(feature, options={})
|
52
|
+
options = Util::normalized_options(options)
|
53
|
+
(f = @features[feature]) && f.active?(options)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
module AdminAPI
|
58
|
+
def activate_feature(feature)
|
59
|
+
features[feature]||=Feature.new
|
60
|
+
end
|
61
|
+
|
62
|
+
def activate_user(feature, user)
|
63
|
+
activate_feature(feature).activate_user(user)
|
64
|
+
end
|
65
|
+
|
66
|
+
def deactivate_user(feature, user)
|
67
|
+
(f=features[feature]) && f.deactivate_user(user)
|
68
|
+
end
|
69
|
+
|
70
|
+
def activate_percentage(feature, percentage)
|
71
|
+
activate_feature(feature).activate_percentage(percentage)
|
72
|
+
end
|
73
|
+
|
74
|
+
def deactivate_percentage(feature)
|
75
|
+
activate_feature(feature).deactivate_percentage
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
include ClientAPI
|
80
|
+
include AdminAPI
|
81
|
+
end
|
82
|
+
end
|
data/lib/sapling/util.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
module Sapling
|
2
|
+
class Util
|
3
|
+
class << self
|
4
|
+
def context_id(options)
|
5
|
+
options[:context_id] || options[:user].id
|
6
|
+
end
|
7
|
+
|
8
|
+
def normalized_options(options)
|
9
|
+
case options
|
10
|
+
when Hash then
|
11
|
+
options[:context_id] ||= options[:user].id
|
12
|
+
options
|
13
|
+
when Integer then
|
14
|
+
{:context_id => options}
|
15
|
+
else
|
16
|
+
user=options
|
17
|
+
{:user => user, :context_id => user.id}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/sapling.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "sapling/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "sapling"
|
7
|
+
s.version = Sapling::VERSION
|
8
|
+
s.authors = ["Shane Brinkman-Davis", "Jason Strutz"]
|
9
|
+
s.email = ["shanebdavis@imikimi.com", "jason@cumuluscode.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{A gem expressing if a feature is seeded for a user}
|
12
|
+
s.description = %q{}
|
13
|
+
|
14
|
+
s.rubyforge_project = "sapling"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
s.add_development_dependency "rspec", "~>2.7.0"
|
23
|
+
s.add_development_dependency "mocha"
|
24
|
+
s.add_development_dependency "activerecord", "~>3.1.1"
|
25
|
+
s.add_development_dependency "sqlite3", "~>1.3.4"
|
26
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
require 'active_record'
|
3
|
+
|
4
|
+
describe "Sapling::ActiveRecord" do
|
5
|
+
it_behaves_like Sapling do
|
6
|
+
before do
|
7
|
+
ActiveRecord::Base.establish_connection(
|
8
|
+
:adapter => 'sqlite3',
|
9
|
+
:database => ':memory:'
|
10
|
+
)
|
11
|
+
sql = File.read(File.expand_path(File.dirname(__FILE__) + '/../db/create.sql'))
|
12
|
+
ActiveRecord::Base.connection.execute sql
|
13
|
+
@sapling = Sapling::ActiveRecord.new
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
#
|
18
|
+
# it "should init" do
|
19
|
+
# Sapling::Memory.new
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# it "should support activating users" do
|
23
|
+
# mem = Sapling::Memory.new
|
24
|
+
# user = UserMock.new
|
25
|
+
#
|
26
|
+
# mem.active?(:my_feature, user).should be_false
|
27
|
+
#
|
28
|
+
# mem.activate_user(:my_feature, user)
|
29
|
+
# mem.active?(:my_feature, user).should be_true
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# it "should support deactivating users" do
|
33
|
+
# mem = Sapling::Memory.new
|
34
|
+
# user = UserMock.new
|
35
|
+
#
|
36
|
+
# mem.activate_user(:my_feature, user)
|
37
|
+
# mem.active?(:my_feature, user).should be_true
|
38
|
+
#
|
39
|
+
# mem.deactivate_user(:my_feature, user)
|
40
|
+
# mem.active?(:my_feature, user).should be_false
|
41
|
+
# end
|
42
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe "Sapling::Memory::Feature" do
|
4
|
+
it "should support activating users" do
|
5
|
+
f=Sapling::Memory::Feature.new
|
6
|
+
u=UserMock.new
|
7
|
+
|
8
|
+
f.active?(u).should be_false
|
9
|
+
f.activate_user(u)
|
10
|
+
|
11
|
+
f.active?(u).should be_true
|
12
|
+
end
|
13
|
+
end
|
data/spec/memory_spec.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe "Sapling::Memory" do
|
4
|
+
it_behaves_like Sapling do
|
5
|
+
before do
|
6
|
+
@sapling = Sapling::Memory.new
|
7
|
+
end
|
8
|
+
|
9
|
+
end
|
10
|
+
#
|
11
|
+
# it "should init" do
|
12
|
+
# Sapling::Memory.new
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# it "should support activating users" do
|
16
|
+
# mem = Sapling::Memory.new
|
17
|
+
# user = UserMock.new
|
18
|
+
#
|
19
|
+
# mem.active?(:my_feature, user).should be_false
|
20
|
+
#
|
21
|
+
# mem.activate_user(:my_feature, user)
|
22
|
+
# mem.active?(:my_feature, user).should be_true
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# it "should support deactivating users" do
|
26
|
+
# mem = Sapling::Memory.new
|
27
|
+
# user = UserMock.new
|
28
|
+
#
|
29
|
+
# mem.activate_user(:my_feature, user)
|
30
|
+
# mem.active?(:my_feature, user).should be_true
|
31
|
+
#
|
32
|
+
# mem.deactivate_user(:my_feature, user)
|
33
|
+
# mem.active?(:my_feature, user).should be_false
|
34
|
+
# end
|
35
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
shared_examples_for Sapling do
|
4
|
+
|
5
|
+
describe "features default to being disabled" do
|
6
|
+
it "is not active for a specific user by default" do
|
7
|
+
@sapling.should_not be_active(:chat, stub(:id => 5))
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "activating a percentage" do
|
12
|
+
before do
|
13
|
+
@sapling.activate_percentage(:chat, 20)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "activates the feature for that percentage of users" do
|
17
|
+
(1..1000).select { |id| @sapling.active?(:chat, UserMock.new(id)) }.length.should == 200
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "deactivating a percentage" do
|
22
|
+
before do
|
23
|
+
@sapling.activate_percentage(:chat, 100)
|
24
|
+
@sapling.deactivate_percentage(:chat)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "becomes inactive for all users" do
|
28
|
+
@sapling.should_not be_active(:chat, stub(:id => 24))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "activating a specific user" do
|
33
|
+
before do
|
34
|
+
@sapling.activate_user(:chat, stub(:id => 2))
|
35
|
+
end
|
36
|
+
|
37
|
+
it "activates the feature for that user" do
|
38
|
+
@sapling.should be_active(:chat, stub(:id => 2))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "deactivating a specific user" do
|
43
|
+
before do
|
44
|
+
@sapling.activate_user(:chat, stub(:id => 2))
|
45
|
+
@sapling.deactivate_user(:chat, stub(:id => 2))
|
46
|
+
end
|
47
|
+
|
48
|
+
it "deactivates the feature for that user" do
|
49
|
+
@sapling.should_not be_active(:chat, stub(:id => 2))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "activate percentage consider a generic integer" do
|
54
|
+
before do
|
55
|
+
@sapling.activate_percentage(:chat, 20)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "activates a context_id" do
|
59
|
+
@sapling.should_not be_active(:chat, 60)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
#
|
65
|
+
#
|
66
|
+
# feature :string
|
67
|
+
# user_id :integer
|
68
|
+
# group :string
|
69
|
+
# percentage :integer
|
70
|
+
#
|
71
|
+
#
|
72
|
+
# select count(*) from saplingawfwefwef where feature = 'aabce'
|
73
|
+
# and percentage < #{user_id % 100}
|
74
|
+
#
|
75
|
+
# select count(*) from saplingawfwefwef where feature = 'aabce'
|
76
|
+
# and group = 'admins'
|
77
|
+
#
|
78
|
+
# select count(*) from saplingawfwefwef where feature = 'aabce'
|
79
|
+
# and user_id = 123
|
80
|
+
#
|
81
|
+
# select feature from sapling where user_id = id or precentage < userid% 100 or group in (#{groups.join(',')})
|
82
|
+
#
|
83
|
+
# def current_features
|
84
|
+
# @features ||=
|
85
|
+
# end
|
86
|
+
#
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
|
4
|
+
require 'sapling'
|
5
|
+
require 'rspec'
|
6
|
+
require 'rspec/autorun'
|
7
|
+
require 'sapling_examples'
|
8
|
+
|
9
|
+
class UserMock
|
10
|
+
def initialize(i=1)
|
11
|
+
@id=i
|
12
|
+
end
|
13
|
+
def id;@id;end
|
14
|
+
end
|
15
|
+
|
16
|
+
RSpec.configure do |config|
|
17
|
+
# config.mock_with :mocha
|
18
|
+
config.before { }
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sapling
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Shane Brinkman-Davis
|
14
|
+
- Jason Strutz
|
15
|
+
autorequire:
|
16
|
+
bindir: bin
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2011-12-02 00:00:00 -08:00
|
20
|
+
default_executable:
|
21
|
+
dependencies:
|
22
|
+
- !ruby/object:Gem::Dependency
|
23
|
+
name: rspec
|
24
|
+
prerelease: false
|
25
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ~>
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
hash: 19
|
31
|
+
segments:
|
32
|
+
- 2
|
33
|
+
- 7
|
34
|
+
- 0
|
35
|
+
version: 2.7.0
|
36
|
+
type: :development
|
37
|
+
version_requirements: *id001
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: mocha
|
40
|
+
prerelease: false
|
41
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
hash: 3
|
47
|
+
segments:
|
48
|
+
- 0
|
49
|
+
version: "0"
|
50
|
+
type: :development
|
51
|
+
version_requirements: *id002
|
52
|
+
- !ruby/object:Gem::Dependency
|
53
|
+
name: activerecord
|
54
|
+
prerelease: false
|
55
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ~>
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
hash: 1
|
61
|
+
segments:
|
62
|
+
- 3
|
63
|
+
- 1
|
64
|
+
- 1
|
65
|
+
version: 3.1.1
|
66
|
+
type: :development
|
67
|
+
version_requirements: *id003
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: sqlite3
|
70
|
+
prerelease: false
|
71
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ~>
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
hash: 19
|
77
|
+
segments:
|
78
|
+
- 1
|
79
|
+
- 3
|
80
|
+
- 4
|
81
|
+
version: 1.3.4
|
82
|
+
type: :development
|
83
|
+
version_requirements: *id004
|
84
|
+
description: ""
|
85
|
+
email:
|
86
|
+
- shanebdavis@imikimi.com
|
87
|
+
- jason@cumuluscode.com
|
88
|
+
executables: []
|
89
|
+
|
90
|
+
extensions: []
|
91
|
+
|
92
|
+
extra_rdoc_files: []
|
93
|
+
|
94
|
+
files:
|
95
|
+
- .gitignore
|
96
|
+
- Gemfile
|
97
|
+
- README.md
|
98
|
+
- Rakefile
|
99
|
+
- db/create.sql
|
100
|
+
- lib/sapling.rb
|
101
|
+
- lib/sapling/active_record.rb
|
102
|
+
- lib/sapling/active_record_model.rb
|
103
|
+
- lib/sapling/api.rb
|
104
|
+
- lib/sapling/base.rb
|
105
|
+
- lib/sapling/memory.rb
|
106
|
+
- lib/sapling/util.rb
|
107
|
+
- lib/sapling/version.rb
|
108
|
+
- sapling.gemspec
|
109
|
+
- spec/active_record_spec.rb
|
110
|
+
- spec/memory_feature_spec.rb
|
111
|
+
- spec/memory_spec.rb
|
112
|
+
- spec/sapling_examples.rb
|
113
|
+
- spec/spec.opts
|
114
|
+
- spec/spec_helper.rb
|
115
|
+
has_rdoc: true
|
116
|
+
homepage: ""
|
117
|
+
licenses: []
|
118
|
+
|
119
|
+
post_install_message:
|
120
|
+
rdoc_options: []
|
121
|
+
|
122
|
+
require_paths:
|
123
|
+
- lib
|
124
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
125
|
+
none: false
|
126
|
+
requirements:
|
127
|
+
- - ">="
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
hash: 3
|
130
|
+
segments:
|
131
|
+
- 0
|
132
|
+
version: "0"
|
133
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
134
|
+
none: false
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
hash: 3
|
139
|
+
segments:
|
140
|
+
- 0
|
141
|
+
version: "0"
|
142
|
+
requirements: []
|
143
|
+
|
144
|
+
rubyforge_project: sapling
|
145
|
+
rubygems_version: 1.5.3
|
146
|
+
signing_key:
|
147
|
+
specification_version: 3
|
148
|
+
summary: A gem expressing if a feature is seeded for a user
|
149
|
+
test_files:
|
150
|
+
- spec/active_record_spec.rb
|
151
|
+
- spec/memory_feature_spec.rb
|
152
|
+
- spec/memory_spec.rb
|
153
|
+
- spec/sapling_examples.rb
|
154
|
+
- spec/spec.opts
|
155
|
+
- spec/spec_helper.rb
|