picky 0.12.3 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/deployment.rb +2 -2
- data/lib/picky/application.rb +172 -12
- data/lib/picky/cacher/generator.rb +1 -1
- data/lib/picky/calculations/location.rb +9 -1
- data/lib/picky/character_substituters/west_european.rb +1 -1
- data/lib/picky/configuration/index.rb +1 -1
- data/lib/picky/cores.rb +1 -1
- data/lib/picky/extensions/array.rb +1 -1
- data/lib/picky/extensions/hash.rb +1 -1
- data/lib/picky/extensions/module.rb +1 -1
- data/lib/picky/extensions/object.rb +1 -1
- data/lib/picky/extensions/symbol.rb +1 -1
- data/lib/picky/generator.rb +2 -2
- data/lib/picky/helpers/cache.rb +7 -5
- data/lib/picky/helpers/gc.rb +2 -0
- data/lib/picky/helpers/measuring.rb +2 -0
- data/lib/picky/index/bundle.rb +1 -1
- data/lib/picky/index_api.rb +33 -15
- data/lib/picky/indexed/bundle.rb +1 -1
- data/lib/picky/indexed/index.rb +1 -1
- data/lib/picky/indexed/wrappers/bundle/location.rb +1 -1
- data/lib/picky/indexers/no_source_specified_error.rb +1 -1
- data/lib/picky/indexes_api.rb +1 -1
- data/lib/picky/indexing/bundle.rb +1 -1
- data/lib/picky/indexing/index.rb +1 -1
- data/lib/picky/loader.rb +1 -1
- data/lib/picky/loggers/search.rb +1 -1
- data/lib/picky/performant.rb +3 -0
- data/lib/picky/query/allocation.rb +1 -1
- data/lib/picky/query/allocations.rb +1 -1
- data/lib/picky/query/base.rb +48 -16
- data/lib/picky/query/combination.rb +1 -1
- data/lib/picky/query/combinations.rb +1 -1
- data/lib/picky/query/full.rb +7 -2
- data/lib/picky/query/live.rb +9 -7
- data/lib/picky/query/qualifiers.rb +6 -2
- data/lib/picky/query/solr.rb +1 -1
- data/lib/picky/query/token.rb +2 -1
- data/lib/picky/query/tokens.rb +4 -1
- data/lib/picky/query/weigher.rb +1 -1
- data/lib/picky/query/weights.rb +1 -1
- data/lib/picky/rack/harakiri.rb +14 -5
- data/lib/picky/results/base.rb +1 -1
- data/lib/picky/routing.rb +1 -1
- data/lib/picky/solr/schema_generator.rb +2 -1
- data/lib/picky/sources/base.rb +39 -25
- data/lib/picky/sources/couch.rb +22 -8
- data/lib/picky/sources/csv.rb +29 -6
- data/lib/picky/sources/db.rb +46 -30
- data/lib/picky/sources/delicious.rb +12 -2
- data/lib/picky/sources/wrappers/base.rb +3 -1
- data/lib/picky/tokenizers/base.rb +1 -1
- data/project_prototype/Gemfile +1 -1
- data/project_prototype/app/README +0 -1
- data/spec/lib/calculations/location_spec.rb +28 -16
- data/spec/lib/index_api_spec.rb +64 -0
- data/spec/lib/indexed/index_spec.rb +2 -2
- data/spec/lib/indexed/wrappers/exact_first_spec.rb +2 -2
- data/spec/lib/indexing/index_spec.rb +2 -2
- data/spec/lib/rack/harakiri_spec.rb +22 -10
- metadata +7 -4
data/lib/picky/sources/db.rb
CHANGED
@@ -1,11 +1,32 @@
|
|
1
1
|
module Sources
|
2
2
|
|
3
|
-
# Describes a database source.
|
3
|
+
# Describes a database source. Needs a SELECT statement
|
4
4
|
# (with id in it), and a file option or the options from an AR config file.
|
5
5
|
#
|
6
|
+
# The select statement can be as complicated as you want,
|
7
|
+
# as long as it has an id in it and as long as it can be
|
8
|
+
# used in a CREATE TABLE AS statement.
|
9
|
+
# (working on that last one)
|
10
|
+
#
|
11
|
+
# Examples:
|
12
|
+
# Sources::DB.new('SELECT id, title, author, year FROM books') # Uses the config from app/db.yml by default.
|
13
|
+
# Sources::DB.new('SELECT id, title, author, year FROM books', file: 'app/some_db.yml')
|
14
|
+
# Sources::DB.new('SELECT b.id, b.title, b.author, b.publishing_year as year FROM books b INNER JOIN ON ...', file: 'app/some_db.yml')
|
15
|
+
# Sources::DB.new('SELECT id, title, author, year FROM books', adapter: 'mysql', host:'localhost', ...)
|
16
|
+
#
|
6
17
|
class DB < Base
|
7
18
|
|
8
|
-
|
19
|
+
# The select statement that was passed in.
|
20
|
+
#
|
21
|
+
attr_reader :select_statement
|
22
|
+
|
23
|
+
# The database adapter.
|
24
|
+
#
|
25
|
+
attr_reader :database
|
26
|
+
|
27
|
+
# The database connection options that were either passed in or loaded from the given file.
|
28
|
+
#
|
29
|
+
attr_reader :connection_options
|
9
30
|
|
10
31
|
def initialize select_statement, options = { file: 'app/db.yml' }
|
11
32
|
@select_statement = select_statement
|
@@ -13,17 +34,10 @@ module Sources
|
|
13
34
|
@options = options
|
14
35
|
end
|
15
36
|
|
16
|
-
#
|
17
|
-
#
|
18
|
-
|
19
|
-
|
20
|
-
# * file => 'some/filename.yml' # With an active record configuration.
|
21
|
-
# Or
|
22
|
-
# * The configuration as a hash.
|
23
|
-
#
|
24
|
-
# TODO Do not use ActiveRecord directly.
|
25
|
-
#
|
26
|
-
def create_database_adapter
|
37
|
+
# Creates a database adapter for use with this source.
|
38
|
+
def create_database_adapter # :nodoc:
|
39
|
+
# TODO Do not use ActiveRecord directly.
|
40
|
+
#
|
27
41
|
adapter_class = Class.new ActiveRecord::Base
|
28
42
|
adapter_class.abstract_class = true
|
29
43
|
adapter_class
|
@@ -37,7 +51,7 @@ module Sources
|
|
37
51
|
# Or
|
38
52
|
# * The configuration as a hash.
|
39
53
|
#
|
40
|
-
def configure options
|
54
|
+
def configure options # :nodoc:
|
41
55
|
@connection_options = if filename = options[:file]
|
42
56
|
File.open(File.join(PICKY_ROOT, filename)) { |file| YAML::load(file) }
|
43
57
|
else
|
@@ -48,13 +62,17 @@ module Sources
|
|
48
62
|
|
49
63
|
# Connect the backend.
|
50
64
|
#
|
65
|
+
# Will raise unless connection options have been given.
|
66
|
+
#
|
51
67
|
def connect_backend
|
52
68
|
configure @options
|
53
69
|
raise "Database backend not configured" unless connection_options
|
54
70
|
database.establish_connection connection_options
|
55
71
|
end
|
56
72
|
|
57
|
-
# Take the
|
73
|
+
# Take a snapshot of the data.
|
74
|
+
#
|
75
|
+
# Uses CREATE TABLE AS with the given SELECT statement to create a snapshot of the data.
|
58
76
|
#
|
59
77
|
def take_snapshot type
|
60
78
|
connect_backend
|
@@ -75,23 +93,19 @@ module Sources
|
|
75
93
|
|
76
94
|
# Counts all the entries that are used for the index.
|
77
95
|
#
|
78
|
-
def count type
|
96
|
+
def count type # :nodoc:
|
79
97
|
connect_backend
|
80
98
|
|
81
99
|
database.connection.select_value("SELECT COUNT(id) FROM #{snapshot_table_name(type)}").to_i
|
82
100
|
end
|
83
101
|
|
84
|
-
# Ok here?
|
85
102
|
#
|
86
|
-
|
103
|
+
#
|
104
|
+
def snapshot_table_name type # :nodoc:
|
87
105
|
"#{type.name}_type_index"
|
88
106
|
end
|
89
107
|
|
90
|
-
# Harvests the data to index
|
91
|
-
#
|
92
|
-
# Subclasses should override harvest_statement to define how their data is found.
|
93
|
-
# Example:
|
94
|
-
# "SELECT indexed_id, value FROM bla_table st WHERE kind = 'bla'"
|
108
|
+
# Harvests the data to index in chunks.
|
95
109
|
#
|
96
110
|
def harvest type, category
|
97
111
|
connect_backend
|
@@ -105,9 +119,9 @@ module Sources
|
|
105
119
|
end
|
106
120
|
end
|
107
121
|
|
108
|
-
# Gets
|
122
|
+
# Gets the data from the backend.
|
109
123
|
#
|
110
|
-
def get_data type, category, offset
|
124
|
+
def get_data type, category, offset # :nodoc:
|
111
125
|
database.connection.execute harvest_statement_with_offset(type, category, offset)
|
112
126
|
end
|
113
127
|
|
@@ -115,7 +129,7 @@ module Sources
|
|
115
129
|
#
|
116
130
|
# TODO Use the adapter for this.
|
117
131
|
#
|
118
|
-
def harvest_statement_with_offset type, category, offset
|
132
|
+
def harvest_statement_with_offset type, category, offset # :nodoc:
|
119
133
|
statement = harvest_statement type, category
|
120
134
|
|
121
135
|
statement += statement.include?('WHERE') ? ' AND' : ' WHERE'
|
@@ -123,15 +137,17 @@ module Sources
|
|
123
137
|
"#{statement} st.id > #{offset} LIMIT #{chunksize}"
|
124
138
|
end
|
125
139
|
|
126
|
-
#
|
140
|
+
# The harvest statement used to pull data from the snapshot table.
|
127
141
|
#
|
128
|
-
def harvest_statement type, category
|
142
|
+
def harvest_statement type, category # :nodoc:
|
129
143
|
"SELECT indexed_id, #{category.from} FROM #{snapshot_table_name(type)} st"
|
130
144
|
end
|
131
145
|
|
132
|
-
#
|
146
|
+
# The amount of records that are loaded each chunk.
|
133
147
|
#
|
134
|
-
def chunksize
|
148
|
+
def chunksize # :nodoc:
|
149
|
+
# TODO Make parametrizable.
|
150
|
+
#
|
135
151
|
25_000
|
136
152
|
end
|
137
153
|
|
@@ -1,5 +1,15 @@
|
|
1
1
|
module Sources
|
2
2
|
|
3
|
+
# Describes a Delicious (http://deli.cio.us) source.
|
4
|
+
#
|
5
|
+
# This source has a fixed set of categories:
|
6
|
+
# * title
|
7
|
+
# * tags
|
8
|
+
# * url
|
9
|
+
#
|
10
|
+
# Examples:
|
11
|
+
# Sources::CSV.new('usrnam', 'paswrd')
|
12
|
+
#
|
3
13
|
class Delicious < Base
|
4
14
|
|
5
15
|
def initialize username, password
|
@@ -7,7 +17,7 @@ module Sources
|
|
7
17
|
@username = username
|
8
18
|
@password = password
|
9
19
|
end
|
10
|
-
def check_gem
|
20
|
+
def check_gem # :nodoc:
|
11
21
|
require 'www/delicious'
|
12
22
|
rescue LoadError
|
13
23
|
puts "Delicious gem missing!\nTo use the delicious source, you need to:\n 1. Add the following line to Gemfile:\n gem 'www-delicious'\n 2. Then, run:\n bundle update\n"
|
@@ -28,7 +38,7 @@ module Sources
|
|
28
38
|
|
29
39
|
#
|
30
40
|
#
|
31
|
-
def get_data
|
41
|
+
def get_data # :nodoc:
|
32
42
|
@generated_id ||= 0
|
33
43
|
@posts ||= WWW::Delicious.new(@username, @password).posts_recent(count: 100)
|
34
44
|
@posts.each do |post|
|
data/project_prototype/Gemfile
CHANGED
@@ -2,32 +2,44 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe Calculations::Location do
|
4
4
|
|
5
|
-
context '
|
5
|
+
context 'with precision 1' do
|
6
6
|
before(:each) do
|
7
|
-
@calculation = Calculations::Location.new
|
8
|
-
@calculation.minimum =
|
7
|
+
@calculation = Calculations::Location.new 1.5, 1
|
8
|
+
@calculation.minimum = 42.7
|
9
9
|
end
|
10
|
-
describe '
|
11
|
-
it '
|
12
|
-
@calculation.recalculate(
|
10
|
+
describe 'recalculate' do
|
11
|
+
it 'sets the minimum close value to the minimum minus user grid' do
|
12
|
+
@calculation.recalculate(41.2).should == 1
|
13
13
|
end
|
14
|
-
it '
|
15
|
-
@calculation.recalculate(
|
14
|
+
it 'sets the minimum value to 1 plus precision' do
|
15
|
+
@calculation.recalculate(42.7).should == 2
|
16
|
+
end
|
17
|
+
it 'sets the minimum value plus 2/3 of the grid size to 2 plus 1 grid length' do
|
18
|
+
@calculation.recalculate(43.7).should == 3
|
19
|
+
end
|
20
|
+
it 'sets the minimum value plus 20/3 of the grid size to 2 plus 10 grid length' do
|
21
|
+
@calculation.recalculate(52.7).should == 12
|
16
22
|
end
|
17
23
|
end
|
18
24
|
end
|
19
25
|
|
20
|
-
context '
|
26
|
+
context 'with precision 3' do
|
21
27
|
before(:each) do
|
22
|
-
@calculation = Calculations::Location.new
|
23
|
-
@calculation.minimum =
|
28
|
+
@calculation = Calculations::Location.new 1.5, 3
|
29
|
+
@calculation.minimum = 42.7
|
24
30
|
end
|
25
|
-
describe '
|
26
|
-
it '
|
27
|
-
@calculation.recalculate(
|
31
|
+
describe 'recalculate' do
|
32
|
+
it 'sets the minimum close value to the minimum minus user grid' do
|
33
|
+
@calculation.recalculate(41.2).should == 1
|
34
|
+
end
|
35
|
+
it 'sets the minimum value to 1 plus precision' do
|
36
|
+
@calculation.recalculate(42.7).should == 4
|
37
|
+
end
|
38
|
+
it 'sets the minimum value plus 2/3 of the grid size plus 1 plus precision plus 1 grid length' do
|
39
|
+
@calculation.recalculate(43.7).should == 6
|
28
40
|
end
|
29
|
-
it '
|
30
|
-
@calculation.recalculate(
|
41
|
+
it 'sets the minimum value plus 20/3 of the grid size to 2 plus 10 grid length' do
|
42
|
+
@calculation.recalculate(52.7).should == 27
|
31
43
|
end
|
32
44
|
end
|
33
45
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe IndexAPI do
|
6
|
+
|
7
|
+
context 'initializer' do
|
8
|
+
it 'works' do
|
9
|
+
lambda { IndexAPI.new :some_index_name, :some_source }.should_not raise_error
|
10
|
+
end
|
11
|
+
it 'registers with the indexes' do
|
12
|
+
@api = IndexAPI.allocate
|
13
|
+
|
14
|
+
Indexes.should_receive(:register).once.with @api
|
15
|
+
|
16
|
+
@api.send :initialize, :some_index_name, :some_source
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'unit' do
|
21
|
+
before(:each) do
|
22
|
+
@api = IndexAPI.new :some_index_name, :some_source
|
23
|
+
end
|
24
|
+
|
25
|
+
describe 'define_category' do
|
26
|
+
context 'with block' do
|
27
|
+
it 'returns itself' do
|
28
|
+
@api.define_category(:some_name){ |indexing, indexed| }.should == @api
|
29
|
+
end
|
30
|
+
it 'takes a string' do
|
31
|
+
lambda { @api.define_category('some_name'){ |indexing, indexed| } }.should_not raise_error
|
32
|
+
end
|
33
|
+
it 'yields both the indexing category and the indexed category' do
|
34
|
+
@api.define_category(:some_name) do |indexing, indexed|
|
35
|
+
indexing.should be_kind_of(Indexing::Category)
|
36
|
+
indexed.should be_kind_of(Indexed::Category)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
it 'yields the indexing category which has the given name' do
|
40
|
+
@api.define_category(:some_name) do |indexing, indexed|
|
41
|
+
indexing.name.should == :some_name
|
42
|
+
end
|
43
|
+
end
|
44
|
+
it 'yields the indexed category which has the given name' do
|
45
|
+
@api.define_category(:some_name) do |indexing, indexed|
|
46
|
+
indexed.name.should == :some_name
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
context 'without block' do
|
51
|
+
it 'works' do
|
52
|
+
lambda { @api.define_category(:some_name) }.should_not raise_error
|
53
|
+
end
|
54
|
+
it 'takes a string' do
|
55
|
+
lambda { @api.define_category('some_name').should == @api }.should_not raise_error
|
56
|
+
end
|
57
|
+
it 'returns itself' do
|
58
|
+
@api.define_category(:some_name).should == @api
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -7,8 +7,8 @@ describe Indexed::Index do
|
|
7
7
|
@categories = stub :categories
|
8
8
|
|
9
9
|
@index = Indexed::Index.new :some_name
|
10
|
-
@index.
|
11
|
-
@index.
|
10
|
+
@index.define_category :some_category_name1
|
11
|
+
@index.define_category :some_category_name2
|
12
12
|
|
13
13
|
@index.stub! :categories => @categories
|
14
14
|
end
|
@@ -16,7 +16,7 @@ describe Indexed::Wrappers::ExactFirst do
|
|
16
16
|
#
|
17
17
|
# it "wraps each category" do
|
18
18
|
# type = Index::Type.new :type_name
|
19
|
-
# type.
|
19
|
+
# type.define_category :some_category
|
20
20
|
#
|
21
21
|
# Index::Wrappers::ExactFirst.wrap type
|
22
22
|
#
|
@@ -24,7 +24,7 @@ describe Indexed::Wrappers::ExactFirst do
|
|
24
24
|
# end
|
25
25
|
it "returns the type" do
|
26
26
|
type = Indexed::Index.new :type_name
|
27
|
-
type.
|
27
|
+
type.define_category :some_category
|
28
28
|
|
29
29
|
Indexed::Wrappers::ExactFirst.wrap(type).should == type
|
30
30
|
end
|
@@ -9,8 +9,8 @@ describe Indexing::Index do
|
|
9
9
|
@categories = stub :categories
|
10
10
|
|
11
11
|
@index = Indexing::Index.new :some_name, @source
|
12
|
-
@index.
|
13
|
-
@index.
|
12
|
+
@index.define_category :some_category_name1
|
13
|
+
@index.define_category :some_category_name2
|
14
14
|
|
15
15
|
@index.stub! :categories => @categories
|
16
16
|
end
|
@@ -7,21 +7,33 @@ describe Rack::Harakiri do
|
|
7
7
|
end
|
8
8
|
context "defaults" do
|
9
9
|
before(:each) do
|
10
|
-
@
|
10
|
+
@ronin = Rack::Harakiri.new @app
|
11
11
|
end
|
12
|
-
it "should quit after
|
13
|
-
@
|
12
|
+
it "should quit after a default amount of requests" do
|
13
|
+
@ronin.instance_variable_get(:@quit_after_requests).should == 50
|
14
|
+
end
|
15
|
+
describe 'harakiri?' do
|
16
|
+
it "should be true after 50 harakiri calls" do
|
17
|
+
50.times { @ronin.harakiri }
|
18
|
+
|
19
|
+
@ronin.harakiri?.should == true
|
20
|
+
end
|
21
|
+
it "should not be true after just 49 harakiri calls" do
|
22
|
+
49.times { @ronin.harakiri }
|
23
|
+
|
24
|
+
@ronin.harakiri?.should == false
|
25
|
+
end
|
14
26
|
end
|
15
27
|
describe "harakiri" do
|
16
28
|
it "should kill the process after 50 harakiri calls" do
|
17
29
|
Process.should_receive(:kill).once
|
18
30
|
|
19
|
-
50.times { @
|
31
|
+
50.times { @ronin.harakiri }
|
20
32
|
end
|
21
33
|
it "should not kill the process after 49 harakiri calls" do
|
22
34
|
Process.should_receive(:kill).never
|
23
35
|
|
24
|
-
49.times { @
|
36
|
+
49.times { @ronin.harakiri }
|
25
37
|
end
|
26
38
|
end
|
27
39
|
describe "call" do
|
@@ -30,27 +42,27 @@ describe Rack::Harakiri do
|
|
30
42
|
@app.stub! :harakiri
|
31
43
|
end
|
32
44
|
it "calls harakiri" do
|
33
|
-
@
|
45
|
+
@ronin.should_receive(:harakiri).once.with
|
34
46
|
|
35
|
-
@
|
47
|
+
@ronin.call :env
|
36
48
|
end
|
37
49
|
it "calls the app" do
|
38
50
|
@app.should_receive(:call).once.with :env
|
39
51
|
|
40
|
-
@
|
52
|
+
@ronin.call :env
|
41
53
|
end
|
42
54
|
end
|
43
55
|
end
|
44
56
|
context "with harakiri set" do
|
45
57
|
before(:each) do
|
46
58
|
Rack::Harakiri.after = 100
|
47
|
-
@
|
59
|
+
@ronin = Rack::Harakiri.new @app
|
48
60
|
end
|
49
61
|
after(:each) do
|
50
62
|
Rack::Harakiri.after = nil
|
51
63
|
end
|
52
64
|
it "should quit after an amount of requests" do
|
53
|
-
@
|
65
|
+
@ronin.instance_variable_get(:@quit_after_requests).should == 100
|
54
66
|
end
|
55
67
|
end
|
56
68
|
|
metadata
CHANGED
@@ -3,10 +3,10 @@ name: picky
|
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
|
+
- 1
|
6
7
|
- 0
|
7
|
-
-
|
8
|
-
|
9
|
-
version: 0.12.3
|
8
|
+
- 0
|
9
|
+
version: 1.0.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Florian Hanke
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-
|
17
|
+
date: 2010-12-09 00:00:00 +01:00
|
18
18
|
default_executable: picky
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -103,6 +103,7 @@ files:
|
|
103
103
|
- lib/picky/indexing/indexes.rb
|
104
104
|
- lib/picky/loader.rb
|
105
105
|
- lib/picky/loggers/search.rb
|
106
|
+
- lib/picky/performant.rb
|
106
107
|
- lib/picky/query/allocation.rb
|
107
108
|
- lib/picky/query/allocations.rb
|
108
109
|
- lib/picky/query/base.rb
|
@@ -189,6 +190,7 @@ files:
|
|
189
190
|
- spec/lib/index/file/marshal_spec.rb
|
190
191
|
- spec/lib/index/file/text_spec.rb
|
191
192
|
- spec/lib/index/files_spec.rb
|
193
|
+
- spec/lib/index_api_spec.rb
|
192
194
|
- spec/lib/indexed/bundle_spec.rb
|
193
195
|
- spec/lib/indexed/categories_spec.rb
|
194
196
|
- spec/lib/indexed/category_spec.rb
|
@@ -294,6 +296,7 @@ test_files:
|
|
294
296
|
- spec/lib/index/file/marshal_spec.rb
|
295
297
|
- spec/lib/index/file/text_spec.rb
|
296
298
|
- spec/lib/index/files_spec.rb
|
299
|
+
- spec/lib/index_api_spec.rb
|
297
300
|
- spec/lib/indexed/bundle_spec.rb
|
298
301
|
- spec/lib/indexed/categories_spec.rb
|
299
302
|
- spec/lib/indexed/category_spec.rb
|