boffin 0.1.0
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 +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
|