boffin 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +8 -0
- data/LICENSE +18 -0
- data/README.md +302 -0
- data/Rakefile +14 -0
- data/boffin.gemspec +29 -0
- data/lib/boffin.rb +87 -0
- data/lib/boffin/config.rb +91 -0
- data/lib/boffin/hit.rb +83 -0
- data/lib/boffin/keyspace.rb +147 -0
- data/lib/boffin/trackable.rb +66 -0
- data/lib/boffin/tracker.rb +229 -0
- data/lib/boffin/utils.rb +171 -0
- data/lib/boffin/version.rb +4 -0
- data/spec/boffin/config_spec.rb +65 -0
- data/spec/boffin/hit_spec.rb +42 -0
- data/spec/boffin/keyspace_spec.rb +85 -0
- data/spec/boffin/trackable_spec.rb +38 -0
- data/spec/boffin/tracker_spec.rb +162 -0
- data/spec/boffin/utils_spec.rb +158 -0
- data/spec/boffin_spec.rb +44 -0
- data/spec/spec_helper.rb +50 -0
- metadata +128 -0
data/lib/boffin/utils.rb
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
module Boffin
|
2
|
+
# A collection of utility methods that are used throughout the library
|
3
|
+
module Utils
|
4
|
+
|
5
|
+
# The number of seconds in an hour
|
6
|
+
SECONDS_IN_HOUR = 3600
|
7
|
+
|
8
|
+
# Number of seconds in a day
|
9
|
+
SECONDS_IN_DAY = 24 * SECONDS_IN_HOUR
|
10
|
+
|
11
|
+
# Number of seconds in a month
|
12
|
+
SECONDS_IN_MONTH = 30 * SECONDS_IN_DAY
|
13
|
+
|
14
|
+
# Number of seconds for a single value of each unit
|
15
|
+
SECONDS_IN_UNIT = {
|
16
|
+
hours: SECONDS_IN_HOUR,
|
17
|
+
days: SECONDS_IN_DAY,
|
18
|
+
months: SECONDS_IN_MONTH
|
19
|
+
}
|
20
|
+
|
21
|
+
module_function
|
22
|
+
|
23
|
+
# @param [#to_s] thing
|
24
|
+
# A Module, Class, String, or anything in which the underscored value of
|
25
|
+
# `#to_s` is desirable.
|
26
|
+
# @return [String]
|
27
|
+
# The underscored version of `#to_s` on thing
|
28
|
+
# @note
|
29
|
+
# Originally pulled from ActiveSupport::Inflector
|
30
|
+
def underscore(thing)
|
31
|
+
thing.to_s.dup.tap do |word|
|
32
|
+
word.gsub!(/::/, '_')
|
33
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
|
34
|
+
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
35
|
+
word.tr!('-', '_')
|
36
|
+
word.downcase!
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param [Object] obj any Ruby object
|
41
|
+
# @return [true, false]
|
42
|
+
# `true` if the provided object is blank, examples of blank objects are:
|
43
|
+
# `[]`, `{}`, `nil`, `false`, `''`.
|
44
|
+
def blank?(obj)
|
45
|
+
obj.respond_to?(:empty?) ? obj.empty? : !obj
|
46
|
+
end
|
47
|
+
|
48
|
+
# Pulls time interval information from a hash of options.
|
49
|
+
# @example
|
50
|
+
# extract_time_unit(this: 'is ignored', days: 6, so_is: 'this')
|
51
|
+
# #=> [:days, 6]
|
52
|
+
# @param [Hash] hsh
|
53
|
+
# Any Hash that contains amoungst its keys one of `:hours`, `:days`, or
|
54
|
+
# `:months`.
|
55
|
+
# @return [Array]
|
56
|
+
# A two-element array containing the unit-type (`:hours`, `:days`, or
|
57
|
+
# `:months`) and the value.
|
58
|
+
def extract_time_unit(hsh)
|
59
|
+
case
|
60
|
+
when hsh.key?(:hours) then [:hours, hsh[:hours]]
|
61
|
+
when hsh.key?(:days) then [:days, hsh[:days]]
|
62
|
+
when hsh.key?(:months) then [:months, hsh[:months]]
|
63
|
+
else
|
64
|
+
raise ArgumentError, 'no time unit exists in the hash provided'
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# @example
|
69
|
+
# time_ago(Time.local(2011, 1, 3), days: 2)
|
70
|
+
# # => 2011-01-01 00:00:00
|
71
|
+
# @param [Time] time
|
72
|
+
# The initial time that the offset will be calculated from
|
73
|
+
# @param [Hash] unit
|
74
|
+
# (see {#extract_time_unit})
|
75
|
+
# @return [Time]
|
76
|
+
# The time in the past offset by the specified amount
|
77
|
+
def time_ago(time, unit)
|
78
|
+
unit, unit_value = *extract_time_unit(unit)
|
79
|
+
time - (unit_value * SECONDS_IN_UNIT[unit])
|
80
|
+
end
|
81
|
+
|
82
|
+
# @param [Time] upto
|
83
|
+
# The base time of which to calculate the range from
|
84
|
+
# @param [Hash] unit
|
85
|
+
# (see {#extract_time_unit})
|
86
|
+
# @return [Array<Time>]
|
87
|
+
# An array of times in the calculated range
|
88
|
+
# @example
|
89
|
+
# time_ago_range(Time.local(2011, 1, 5), days: 3)
|
90
|
+
# # => [2011-01-03 00:00:00, 2011-01-04 00:00:00, 2011-01-05 00:00:00]
|
91
|
+
def time_ago_range(upto, unit)
|
92
|
+
unit, size = *extract_time_unit(unit)
|
93
|
+
ago = time_ago(upto, unit => (size - 1))
|
94
|
+
max, count, times = upto.to_i, ago.to_i, []
|
95
|
+
begin
|
96
|
+
times << Time.at(count)
|
97
|
+
end while (count += SECONDS_IN_UNIT[unit]) <= max
|
98
|
+
times
|
99
|
+
end
|
100
|
+
|
101
|
+
# Generates a set member based off the first object in the provided array
|
102
|
+
# that is not `nil`. If the array is empty or only contains `nil` elements
|
103
|
+
# then {Boffin::NIL_SESSION_MEMBER} is returned.
|
104
|
+
# @param [Array] aspects
|
105
|
+
# An array of which the first non-nil element is passed to
|
106
|
+
# {#object_as_session_identifier}
|
107
|
+
# @return [String]
|
108
|
+
def uniquenesses_as_session_identifier(aspects)
|
109
|
+
if (obj = aspects.flatten.reject { |u| blank?(u) }.first)
|
110
|
+
object_as_session_identifier(obj)
|
111
|
+
else
|
112
|
+
NIL_SESSION_MEMBER
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# @param [String, Symbol, Object] obj
|
117
|
+
# @return [String]
|
118
|
+
# Returns a string that can be used as a namespace in Redis keys
|
119
|
+
def object_as_namespace(obj)
|
120
|
+
case obj
|
121
|
+
when String, Symbol
|
122
|
+
obj.to_s
|
123
|
+
else
|
124
|
+
underscore(obj)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# @param [#as_member, #id, #to_s] obj
|
129
|
+
# @param [Hash] opts
|
130
|
+
# @option opts [true, false] :namespace
|
131
|
+
# If `true` the generated value will be prefixed with a namespace
|
132
|
+
# @option opts [true, false] :encode
|
133
|
+
# If `true` and object fails to respond to `#as_member` or `#id`, the
|
134
|
+
# generated value will be Base64 encoded.
|
135
|
+
# @return [String]
|
136
|
+
def object_as_identifier(obj, opts = {})
|
137
|
+
if obj.respond_to?(:as_member) || obj.respond_to?(:id)
|
138
|
+
''.tap do |s|
|
139
|
+
s << "#{underscore(obj.class)}:" if opts[:namespace]
|
140
|
+
s << (obj.respond_to?(:as_member) ? obj.as_member : obj.id).to_s
|
141
|
+
end
|
142
|
+
else
|
143
|
+
opts[:encode] ? Base64.strict_encode64(obj.to_s) : obj.to_s
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# @return [String]
|
148
|
+
# @param [#as_member, #id, #to_s] obj
|
149
|
+
# @return [String] A string that can be used as a member in
|
150
|
+
# {Keyspace#hits_time_window}.
|
151
|
+
# @see #object_as_identifier
|
152
|
+
def object_as_member(obj)
|
153
|
+
object_as_identifier(obj)
|
154
|
+
end
|
155
|
+
|
156
|
+
# @param [#as_member, #id, #to_s] obj
|
157
|
+
# @return [String] A string that can be used as a member in {Keyspace#hits}.
|
158
|
+
# @see #object_as_identifier
|
159
|
+
def object_as_session_identifier(obj)
|
160
|
+
object_as_identifier(obj, namespace: true)
|
161
|
+
end
|
162
|
+
|
163
|
+
# @param [#as_member, #id, #to_s] obj
|
164
|
+
# @return [String] A string that can be used as part of a Redis key
|
165
|
+
# @see #object_as_identifier
|
166
|
+
def object_as_key(obj)
|
167
|
+
object_as_identifier(obj, encode: true)
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Boffin::Config do
|
4
|
+
describe '#namespace' do
|
5
|
+
specify { subject.namespace.should == 'boffin' }
|
6
|
+
|
7
|
+
it 'includes the Rails environment when available' do
|
8
|
+
ENV['RAILS_ENV'] = 'production'
|
9
|
+
Boffin::Config.new.namespace.should == 'boffin:production'
|
10
|
+
ENV['RAILS_ENV'] = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'includes the Rack environment when available' do
|
14
|
+
ENV['RACK_ENV'] = 'staging'
|
15
|
+
Boffin::Config.new.namespace.should == 'boffin:staging'
|
16
|
+
ENV['RACK_ENV'] = nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#new' do
|
21
|
+
it 'returns a default config instance with no arguments' do
|
22
|
+
Boffin::Config.new.should be_a Boffin::Config
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'can be sent a block' do
|
26
|
+
conf = Boffin::Config.new { |c| c.namespace = 'hihi' }
|
27
|
+
conf.namespace.should == 'hihi'
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'can be sent a hash' do
|
31
|
+
conf = Boffin::Config.new(namespace: 'hello')
|
32
|
+
conf.namespace.should == 'hello'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#merge' do
|
37
|
+
it 'copies the existing instance' do
|
38
|
+
newconf = subject.merge(namespace: 'carsten')
|
39
|
+
newconf.namespace.should == 'carsten'
|
40
|
+
subject.namespace.should == 'boffin'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '#redis' do
|
45
|
+
it 'calls Redis.connect by default' do
|
46
|
+
Boffin::Config.new.redis.should be_a Redis
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '#hours_window_secs' do
|
51
|
+
specify { subject.hours_window_secs.should == 3 * 24 * 3600 } # 3 days
|
52
|
+
end
|
53
|
+
|
54
|
+
describe '#days_window_secs' do
|
55
|
+
specify { subject.days_window_secs.should == 3 * 30 * 24 * 3600 } # 3 months
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '#months_window_secs' do
|
59
|
+
specify { subject.months_window_secs.should == 3 * 12 * 30 * 24 * 3600 } # 3 years
|
60
|
+
end
|
61
|
+
|
62
|
+
describe '#cache_expire_secs' do
|
63
|
+
specify { subject.cache_expire_secs.should == 900 } # 15 minutes
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Boffin::Hit, '::new' do
|
4
|
+
before do
|
5
|
+
SpecHelper.flush_keyspace!
|
6
|
+
@tracker = Boffin::Tracker.new(MockDitty)
|
7
|
+
@ditty = MockDitty.new
|
8
|
+
@user = MockUser.new
|
9
|
+
Timecop.travel(Time.local(2011, 1, 1))
|
10
|
+
end
|
11
|
+
|
12
|
+
after do
|
13
|
+
Timecop.return
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'stores hit data under the appropriate keys' do
|
17
|
+
Boffin::Hit.new(@tracker, :tests, @ditty, [nil, @user])
|
18
|
+
[:hours, :days, :months].each do |interval|
|
19
|
+
@tracker.top(:tests, interval => 1, counts: true).
|
20
|
+
should == [['1', 1]]
|
21
|
+
@tracker.top(:tests, interval => 1, counts: true, unique: true).
|
22
|
+
should == [['1', 1]]
|
23
|
+
end
|
24
|
+
@tracker.hit_count(:tests, @ditty).should == 1
|
25
|
+
@tracker.uhit_count(:tests, @ditty).should == 1
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'does not store data under unique keys if the hit is not unique' do
|
29
|
+
Boffin::Hit.new(@tracker, :tests, @ditty, [nil, @user])
|
30
|
+
Boffin::Hit.new(@tracker, :tests, @ditty, [nil, @user])
|
31
|
+
[:hours, :days, :months].each do |interval|
|
32
|
+
@tracker.top(:tests, interval => 1, counts: true).
|
33
|
+
should == [['1', 2]]
|
34
|
+
@tracker.top(:tests, interval => 1, counts: true, unique: true).
|
35
|
+
should == [['1', 1]]
|
36
|
+
end
|
37
|
+
@tracker.hit_count_for_session_id(:tests, @ditty, @user).should == 2
|
38
|
+
@tracker.hit_count(:tests, @ditty).should == 2
|
39
|
+
@tracker.uhit_count(:tests, @ditty).should == 1
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Boffin::Keyspace do
|
4
|
+
before :all do
|
5
|
+
@tracker = Boffin::Tracker.new(MockDitty)
|
6
|
+
@tracker.config.namespace = 'b'
|
7
|
+
@ks = @tracker.keyspace
|
8
|
+
@uks = @tracker.keyspace(true)
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '#root' do
|
12
|
+
specify { @ks.root.should == 'b:mock_ditty' }
|
13
|
+
specify { @uks.root.should == 'b:mock_ditty:uniq' }
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '#hits' do
|
17
|
+
specify do
|
18
|
+
@ks.hits([:views, :likes]).should == 'b:mock_ditty:views_likes:hits'
|
19
|
+
end
|
20
|
+
|
21
|
+
specify do
|
22
|
+
@ks.hits(:views).should == 'b:mock_ditty:views:hits'
|
23
|
+
end
|
24
|
+
|
25
|
+
specify do
|
26
|
+
@ks.hits(:views, MockDitty.new).should == 'b:mock_ditty.1:views:hits'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#hit_count' do
|
31
|
+
specify do
|
32
|
+
@ks.hit_count(:views, MockDitty.new).should == 'b:mock_ditty.1:views:hit_count'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#hits_union' do
|
37
|
+
specify do
|
38
|
+
@ks.hits_union(:views, :days, 5).
|
39
|
+
should == 'b:mock_ditty:views:hits:current.days_5'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '#hits_union_multi' do
|
44
|
+
specify do
|
45
|
+
@ks.hits_union_multi({ views: 1, likes: 3 }, :days, 5).
|
46
|
+
should == 'b:mock_ditty:views_1_likes_3:hits:current.days_5'
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '#hits_window' do
|
51
|
+
specify do
|
52
|
+
@ks.hits_window(:views, '*').should == 'b:mock_ditty:views:hits.*'
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe '#hits_time_window' do
|
57
|
+
before do
|
58
|
+
@time = Time.local(2011, 1, 1, 23)
|
59
|
+
end
|
60
|
+
|
61
|
+
Boffin::INTERVAL_TYPES.each do |format|
|
62
|
+
describe "given #{format} based window" do
|
63
|
+
specify do
|
64
|
+
strf = Boffin::INTERVAL_FORMATS[format]
|
65
|
+
@ks.hits_time_window(:views, format, @time).
|
66
|
+
should == "b:mock_ditty:views:hits.#{@time.strftime(strf)}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe '#hit_time_windows' do
|
73
|
+
before do
|
74
|
+
@time = Time.local(2011, 1, 1, 23)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'generates keys each interval in the range' do
|
78
|
+
@ks.hit_time_windows(:views, :days, 3, @time).should == [
|
79
|
+
'b:mock_ditty:views:hits.2010-12-30',
|
80
|
+
'b:mock_ditty:views:hits.2010-12-31',
|
81
|
+
'b:mock_ditty:views:hits.2011-01-01'
|
82
|
+
]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Boffin::Trackable do
|
4
|
+
before :all do
|
5
|
+
SpecHelper.flush_keyspace!
|
6
|
+
@mock = MockTrackableInjected.new(1)
|
7
|
+
@mock.hit(:views, ['sess.1'])
|
8
|
+
@mock.hit(:views, ['sess.1'])
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'can be included' do
|
12
|
+
MockTrackableIncluded.boffin.hit_types.should == [:views, :likes]
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'can be injected' do
|
16
|
+
MockTrackableInjected.boffin.hit_types.should == [:views, :likes]
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'provides ::boffin as an accessor to the Tracker instance' do
|
20
|
+
MockTrackableInjected.boffin.should be_a Boffin::Tracker
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'delegates ::top_ids to the Tracker instance' do
|
24
|
+
MockTrackableInjected.top_ids(:views, days: 1).should == ['1']
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'delegates #hit_count to the Tracker instance' do
|
28
|
+
@mock.hit_count(:views).should == 2
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'delegates #uhit_count to the Tracker instance' do
|
32
|
+
@mock.uhit_count(:views).should == 1
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'delegates #hit_count_for_session_id to the Tracker instance' do
|
36
|
+
@mock.hit_count_for_session_id(:views, 'sess.1').should == 2
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Boffin::Tracker do
|
4
|
+
before :all do
|
5
|
+
SpecHelper.flush_keyspace!
|
6
|
+
@tracker = Boffin::Tracker.new(MockDitty, [:views, :likes, :shares])
|
7
|
+
@instance1 = MockDitty.new(100)
|
8
|
+
@instance2 = MockDitty.new(200)
|
9
|
+
@instance3 = MockDitty.new(300)
|
10
|
+
@instance4 = MockDitty.new(400)
|
11
|
+
@user1 = MockUser.new(1)
|
12
|
+
@user2 = MockUser.new(2)
|
13
|
+
@date = Date.today
|
14
|
+
|
15
|
+
Timecop.freeze(@date - 2) do
|
16
|
+
@tracker.hit(:views, @instance3)
|
17
|
+
@tracker.hit(:likes, @instance3, [@user1])
|
18
|
+
@tracker.hit(:views, @instance3, ['sess.1'])
|
19
|
+
@tracker.hit(:views, @instance3, ['sess.2'])
|
20
|
+
@tracker.hit(:views, @instance3, [@user2])
|
21
|
+
@tracker.hit(:likes, @instance1, [nil, nil])
|
22
|
+
@tracker.hit(:views, @instance3, ['sess.4'])
|
23
|
+
@tracker.hit(:views, @instance3, [@user1])
|
24
|
+
@tracker.hit(:views, @instance2, [@user2])
|
25
|
+
@tracker.hit(:views, @instance3, [@user2])
|
26
|
+
@tracker.hit(:likes, @instance3)
|
27
|
+
@tracker.hit(:views, @instance3, ['sess.1'])
|
28
|
+
@tracker.hit(:views, @instance3)
|
29
|
+
@tracker.hit(:likes, @instance3, [@user1])
|
30
|
+
@tracker.hit(:views, @instance3, ['sess.1'])
|
31
|
+
end
|
32
|
+
|
33
|
+
Timecop.freeze(@date - 1) do
|
34
|
+
@tracker.hit(:views, @instance1)
|
35
|
+
@tracker.hit(:likes, @instance2, [@user1])
|
36
|
+
@tracker.hit(:views, @instance2, ['sess.4'])
|
37
|
+
@tracker.hit(:views, @instance2, [nil, @user1])
|
38
|
+
@tracker.hit(:views, @instance2, ['sess.3'])
|
39
|
+
@tracker.hit(:views, @instance1, ['sess.3'])
|
40
|
+
@tracker.hit(:views, @instance1, [@user1])
|
41
|
+
@tracker.hit(:views, @instance2, ['sess.2'])
|
42
|
+
@tracker.hit(:views, @instance1, [@user1])
|
43
|
+
@tracker.hit(:views, @instance1, [@user2])
|
44
|
+
end
|
45
|
+
|
46
|
+
@tracker.hit(:views, @instance3, ['sess.2'])
|
47
|
+
@tracker.hit(:views, @instance2, [@user2])
|
48
|
+
@tracker.hit(:likes, @instance2)
|
49
|
+
@tracker.hit(:views, @instance2, [@user1])
|
50
|
+
@tracker.hit(:views, @instance1, ['sess.4'])
|
51
|
+
@tracker.hit(:views, @instance3, ['sess.3'])
|
52
|
+
@tracker.hit(:views, @instance1, [@user1])
|
53
|
+
@tracker.hit(:views, @instance1, [@user2])
|
54
|
+
end
|
55
|
+
|
56
|
+
describe '#hit' do
|
57
|
+
it 'throws an error if the hit type is not in the list' do
|
58
|
+
-> { @tracker.hit(:view, @instance1) }.
|
59
|
+
should raise_error Boffin::UndefinedHitTypeError
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe '#hit_count' do
|
64
|
+
it 'throws an error if the hit type is not in the list' do
|
65
|
+
-> { @tracker.hit_count(:view, @instance1) }.
|
66
|
+
should raise_error Boffin::UndefinedHitTypeError
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'returns the raw hit count for the instance' do
|
70
|
+
@tracker.hit_count(:views, @instance1).should == 8
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'returns 0 for an instance that was never hit' do
|
74
|
+
@tracker.hit_count(:views, 'neverhit').should == 0
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe '#uhit_count' do
|
79
|
+
it 'throws an error if the hit type is not in the list' do
|
80
|
+
-> { @tracker.uhit_count(:view, @instance1) }.
|
81
|
+
should raise_error Boffin::UndefinedHitTypeError
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'returns the unique hit count for the instance' do
|
85
|
+
@tracker.uhit_count(:views, @instance1).should == 5
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'returns 0 for an instance that was never hit' do
|
89
|
+
@tracker.hit_count(:likes, @instance4).should == 0
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe '#hit_count_for_session_id' do
|
94
|
+
it 'throws an error if the hit type is not in the list' do
|
95
|
+
-> { @tracker.hit_count_for_session_id(:view, @instance1, 'sess.1') }.
|
96
|
+
should raise_error Boffin::UndefinedHitTypeError
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'returns the number of times the instance was hit by the session id' do
|
100
|
+
@tracker.hit_count_for_session_id(:views, @instance3, 'sess.1').should == 3
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'returns a count of 0 if the session id never hit the instance' do
|
104
|
+
@tracker.hit_count_for_session_id(:views, @instance1, 'nohit').should == 0
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe '#top' do
|
109
|
+
it 'throws an error if passed hit type is invalid' do
|
110
|
+
-> { @tracker.top(:view, days: 3) }.
|
111
|
+
should raise_error Boffin::UndefinedHitTypeError
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'throws an error if passed weights with hit type that is invalid' do
|
115
|
+
-> { @tracker.top({ view: 1 }, days: 3) }.
|
116
|
+
should raise_error Boffin::UndefinedHitTypeError
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'returns ids ordered by hit counts of weighted totals' do
|
120
|
+
ids = @tracker.top({ views: 1, likes: 2 }, days: 3)
|
121
|
+
ids.should == ['300', '200', '100']
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'returns ids ordered by total counts of a specific hit type' do
|
125
|
+
ids = @tracker.top(:views, days: 3)
|
126
|
+
ids.should == ['300', '100', '200']
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'returns ids in ascending order when passed { order: "asc" } as an option' do
|
130
|
+
ids = @tracker.top(:views, days: 3, order: 'asc')
|
131
|
+
ids.should == ['200', '100', '300']
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'returns ids and counts when passed { counts: true } as an option' do
|
135
|
+
ids = @tracker.top(:views, days: 3, counts: true)
|
136
|
+
ids.should == [
|
137
|
+
['300', 12],
|
138
|
+
['100', 8],
|
139
|
+
['200', 7]
|
140
|
+
]
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'returns ids based on unique hit data when passed { unique: true } as an option' do
|
144
|
+
ids = @tracker.top(:views, days: 3, counts: true, unique: true)
|
145
|
+
ids.should == [
|
146
|
+
['300', 7],
|
147
|
+
['200', 5],
|
148
|
+
['100', 5]
|
149
|
+
]
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
describe '#keyspace' do
|
154
|
+
it 'returns a keyspace' do
|
155
|
+
@tracker.keyspace.unique_namespace?.should be_false
|
156
|
+
end
|
157
|
+
|
158
|
+
it 'returns a unique keyspace when passed true' do
|
159
|
+
@tracker.keyspace(true).unique_namespace?.should be_true
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|