1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
|
require 'abstract_unit'
class MemoizableTest < ActiveSupport::TestCase
class Person
extend ActiveSupport::Memoizable
attr_reader :name_calls, :age_calls, :is_developer_calls
def initialize
@name_calls = 0
@age_calls = 0
@is_developer_calls = 0
end
def name
@name_calls += 1
"Josh"
end
def name?
true
end
memoize :name?
def update(name)
"Joshua"
end
memoize :update
def age
@age_calls += 1
nil
end
memoize :name, :age
private
def is_developer?
@is_developer_calls += 1
"Yes"
end
memoize :is_developer?
end
class Company
attr_reader :name_calls
def initialize
@name_calls = 0
end
def name
@name_calls += 1
"37signals"
end
end
module Rates
extend ActiveSupport::Memoizable
attr_reader :sales_tax_calls
def sales_tax(price)
@sales_tax_calls ||= 0
@sales_tax_calls += 1
price * 0.1025
end
memoize :sales_tax
end
class Calculator
extend ActiveSupport::Memoizable
include Rates
attr_reader :fib_calls
def initialize
@fib_calls = 0
end
def fib(n)
@fib_calls += 1
if n == 0 || n == 1
n
else
fib(n - 1) + fib(n - 2)
end
end
memoize :fib
def counter
@count ||= 0
@count += 1
end
memoize :counter
end
def setup
@person = Person.new
@calculator = Calculator.new
end
def test_memoization
assert_equal "Josh", @person.name
assert_equal 1, @person.name_calls
3.times { assert_equal "Josh", @person.name }
assert_equal 1, @person.name_calls
end
def test_memoization_with_punctuation
assert_equal true, @person.name?
assert_nothing_raised(NameError) do
@person.memoize_all
@person.unmemoize_all
end
end
def test_memoization_with_nil_value
assert_equal nil, @person.age
assert_equal 1, @person.age_calls
3.times { assert_equal nil, @person.age }
assert_equal 1, @person.age_calls
end
def test_memorized_results_are_immutable
# This is purely a performance enhancement that we can revisit once the rest of
# the code is in place. Ideally, we'd be able to do memoization in a freeze-friendly
# way without amc hacks
pending do
assert_equal "Josh", @person.name
assert_raise(ActiveSupport::FrozenObjectError) { @person.name.gsub!("Josh", "Gosh") }
end
end
def test_reloadable
counter = @calculator.counter
assert_equal 1, @calculator.counter
assert_equal 2, @calculator.counter(:reload)
assert_equal 2, @calculator.counter
assert_equal 3, @calculator.counter(true)
assert_equal 3, @calculator.counter
end
def test_flush_cache
assert_equal 1, @calculator.counter
assert @calculator.instance_variable_get(:@_memoized_counter).any?
@calculator.flush_cache(:counter)
assert @calculator.instance_variable_get(:@_memoized_counter).empty?
assert_equal 2, @calculator.counter
end
def test_unmemoize_all
assert_equal 1, @calculator.counter
assert @calculator.instance_variable_get(:@_memoized_counter).any?
@calculator.unmemoize_all
assert @calculator.instance_variable_get(:@_memoized_counter).empty?
assert_equal 2, @calculator.counter
end
def test_memoize_all
@calculator.memoize_all
assert @calculator.instance_variable_defined?(:@_memoized_counter)
end
def test_memoization_cache_is_different_for_each_instance
assert_equal 1, @calculator.counter
assert_equal 2, @calculator.counter(:reload)
assert_equal 1, Calculator.new.counter
end
def test_memoized_is_not_affected_by_freeze
@person.freeze
assert_equal "Josh", @person.name
assert_equal "Joshua", @person.update("Joshua")
end
def test_memoization_with_args
assert_equal 55, @calculator.fib(10)
assert_equal 11, @calculator.fib_calls
end
def test_reloadable_with_args
assert_equal 55, @calculator.fib(10)
assert_equal 11, @calculator.fib_calls
assert_equal 55, @calculator.fib(10, :reload)
assert_equal 12, @calculator.fib_calls
assert_equal 55, @calculator.fib(10, true)
assert_equal 13, @calculator.fib_calls
end
def test_object_memoization
[Company.new, Company.new, Company.new].each do |company|
company.extend ActiveSupport::Memoizable
company.memoize :name
assert_equal "37signals", company.name
assert_equal 1, company.name_calls
assert_equal "37signals", company.name
assert_equal 1, company.name_calls
end
end
def test_memoized_module_methods
assert_equal 1.025, @calculator.sales_tax(10)
assert_equal 1, @calculator.sales_tax_calls
assert_equal 1.025, @calculator.sales_tax(10)
assert_equal 1, @calculator.sales_tax_calls
assert_equal 2.5625, @calculator.sales_tax(25)
assert_equal 2, @calculator.sales_tax_calls
end
def test_object_memoized_module_methods
company = Company.new
company.extend(Rates)
assert_equal 1.025, company.sales_tax(10)
assert_equal 1, company.sales_tax_calls
assert_equal 1.025, company.sales_tax(10)
assert_equal 1, company.sales_tax_calls
assert_equal 2.5625, company.sales_tax(25)
assert_equal 2, company.sales_tax_calls
end
def test_double_memoization
assert_raise(RuntimeError) { Person.memoize :name }
person = Person.new
person.extend ActiveSupport::Memoizable
assert_raise(RuntimeError) { person.memoize :name }
company = Company.new
company.extend ActiveSupport::Memoizable
company.memoize :name
assert_raise(RuntimeError) { company.memoize :name }
end
def test_private_method_memoization
person = Person.new
assert_raise(NoMethodError) { person.is_developer? }
assert_equal "Yes", person.send(:is_developer?)
assert_equal 1, person.is_developer_calls
assert_equal "Yes", person.send(:is_developer?)
assert_equal 1, person.is_developer_calls
end
end
|