smugmug 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.
Files changed (49) hide show
  1. data/HISTORY +2 -0
  2. data/LICENSE +19 -0
  3. data/MANIFEST +48 -0
  4. data/README +30 -0
  5. data/Rakefile +100 -0
  6. data/bin/smcli +225 -0
  7. data/bin/smugmug2sql +158 -0
  8. data/doc/API +310 -0
  9. data/doc/TODO +32 -0
  10. data/lib/net/httpz.rb +31 -0
  11. data/lib/smugmug.rb +179 -0
  12. data/lib/smugmug/album/info.rb +131 -0
  13. data/lib/smugmug/album/stats.rb +31 -0
  14. data/lib/smugmug/albums.rb +39 -0
  15. data/lib/smugmug/base.rb +104 -0
  16. data/lib/smugmug/cache.rb +33 -0
  17. data/lib/smugmug/config.rb +48 -0
  18. data/lib/smugmug/image/exif.rb +72 -0
  19. data/lib/smugmug/image/info.rb +88 -0
  20. data/lib/smugmug/image/stats.rb +32 -0
  21. data/lib/smugmug/images.rb +52 -0
  22. data/lib/smugmug/table.rb +133 -0
  23. data/lib/smugmug/util.rb +12 -0
  24. data/test/album.rb +359 -0
  25. data/test/config.rb +39 -0
  26. data/test/httpz.rb +120 -0
  27. data/test/image.rb +540 -0
  28. data/test/login.rb +24 -0
  29. data/test/runner.rb +83 -0
  30. data/test/servlet.rb +257 -0
  31. data/test/table.rb +113 -0
  32. data/xml/canned +212 -0
  33. data/xml/fail/empty.set.xml +4 -0
  34. data/xml/fail/invalid.apikey.xml +4 -0
  35. data/xml/fail/invalid.login.xml +4 -0
  36. data/xml/fail/invalid.method.xml +4 -0
  37. data/xml/fail/invalid.user.xml +4 -0
  38. data/xml/fail/system.error.xml +4 -0
  39. data/xml/standard/albums.get.xml +24 -0
  40. data/xml/standard/albums.getInfo.xml +38 -0
  41. data/xml/standard/albums.getStats.xml +43 -0
  42. data/xml/standard/categories.get.xml +213 -0
  43. data/xml/standard/images.get.xml +9 -0
  44. data/xml/standard/images.getEXIF.xml +34 -0
  45. data/xml/standard/images.getInfo.xml +29 -0
  46. data/xml/standard/images.getStats.xml +15 -0
  47. data/xml/standard/login.withHash.xml +7 -0
  48. data/xml/standard/login.withPassword.xml +10 -0
  49. metadata +103 -0
@@ -0,0 +1,88 @@
1
+ # -*- Mode: ruby; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2
+ # $Hg: info.rb,v ecd3a25582a8 2007/08/11 07:17:53 boumenot $
3
+
4
+ require 'smugmug/base'
5
+ require 'smugmug/table'
6
+ require 'smugmug/image/exif'
7
+
8
+ module SmugMug
9
+ module Image
10
+ class Info < Base
11
+ attr_reader :album, :doc
12
+
13
+ def initialize(smugmug, album, doc)
14
+ super(smugmug)
15
+ @album, @doc = album, doc
16
+ self.class.table = MethodTable.new(Info.data(), smugmug.account_type, self)
17
+ end
18
+
19
+ ## overrides
20
+
21
+ def method
22
+ return 'smugmug.images.getInfo'
23
+ end
24
+
25
+ def fetch
26
+ params = {}
27
+ # params[:Password] = album.password
28
+ params[:ImageID] = image_id
29
+
30
+ return super(params)
31
+ end
32
+
33
+ ## methods
34
+
35
+ def <=>(rhs)
36
+ return self.file_name <=> rhs.file_name
37
+ end
38
+
39
+ def delete
40
+ raise RuntimeError, "method is not yet implemented"
41
+ end
42
+
43
+ def stats
44
+ @stats = Stats.new(@smugmug, self) if @stats.nil?
45
+ return @stats
46
+ end
47
+
48
+ def exif
49
+ @exif = Exif.new(@smugmug, self) if @exif.nil?
50
+ return @exif
51
+ end
52
+
53
+ ## accessors
54
+
55
+ # FIXME: needed to prevent infinite recursion
56
+ def image_id() return self.class.table.get(@doc, :image_id) ; end
57
+
58
+ def Info.data
59
+ return %q{
60
+ <smugmug>
61
+ <AlbumID type="int" xpath="//Album/attribute::id"/>
62
+ <ImageID type="int" xpath="//Image/attribute::id"/>
63
+ <Position type="int"/>
64
+ <Serial type="int"/>
65
+ <Size type="int"/>
66
+ <Width type="int"/>
67
+ <Height type="int"/>
68
+ <LastUpdated/>
69
+ <Caption/>
70
+ <FileName/>
71
+ <MD5Sum/>
72
+ <Watermark type="int"/>
73
+ <Format/>
74
+ <Keywords/>
75
+ <Date/>
76
+ <OriginalURL/>
77
+ <LargeURL/>
78
+ <MediumURL/>
79
+ <SmallURL/>
80
+ <ThumbURL/>
81
+ <TinyURL/>
82
+ <AlbumURL/>
83
+ </smugmug>
84
+ }
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,32 @@
1
+ # -*- Mode: ruby; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2
+ # $Hg$
3
+
4
+ require 'smugmug/base'
5
+
6
+ module SmugMug
7
+ module Image
8
+ class Stats < BaseStats
9
+ attr_reader :doc, :image
10
+ def initialize(smugmug, image)
11
+ super(smugmug)
12
+ @image = image
13
+ end
14
+
15
+ ## overrides
16
+
17
+ def method
18
+ return 'smugmug.images.getStats'
19
+ end
20
+
21
+ def fetch
22
+ params = {}
23
+ params[:ImageID] = image.image_id
24
+
25
+ super(params)
26
+ end
27
+
28
+ ## methods
29
+ ## accessors
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,52 @@
1
+ # -*- ruby -*-
2
+ # $Hg: images.rb,v a116fa71e3fb 2007/08/11 07:43:48 boumenot $
3
+
4
+ require 'forwardable'
5
+ require 'smugmug/base'
6
+ require 'smugmug/image/info'
7
+
8
+ module SmugMug
9
+ class Images < Base
10
+ include Enumerable
11
+
12
+ extend Forwardable
13
+ def_delegator :@images, :each, :each
14
+ def_delegator :@images, :any?, :any?
15
+ def_delegator :@images, :size, :size
16
+
17
+ attr_reader :album, :doc
18
+
19
+ def initialize(smugmug, album)
20
+ super(smugmug)
21
+
22
+ @album = album
23
+ @images = []
24
+
25
+ @doc = fetch()
26
+
27
+ @doc.elements.each('//Images/Image') do |x|
28
+ @images << Image::Info.new(smugmug, album, REXML::Document.new(x.to_s))
29
+ $log.info('Images#initialize') { "Added image ID #{@images[-1].image_id}" }
30
+ end
31
+ end
32
+
33
+ ## overrides
34
+
35
+ def method
36
+ return 'smugmug.images.get'
37
+ end
38
+
39
+ def method_missing(sym, *args)
40
+ raise NoMethodError.new("undefined method `#{sym}' for " +
41
+ "#{self.inspect}:#{self.class.name}")
42
+ end
43
+
44
+ def fetch
45
+ params = {}
46
+ # params[:Password] = XXX
47
+ params[:AlbumID] = album.album_id
48
+
49
+ return super(params)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,133 @@
1
+ # -*- Mode: ruby; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2
+ # $Hg: table.rb,v f2af7d25fd90 2007/08/11 07:20:47 boumenot $
3
+
4
+ require 'rexml/document'
5
+
6
+ module SmugMug
7
+ class MethodTable
8
+ include Enumerable
9
+
10
+ attr_reader :account_type, :klass, :table
11
+ def initialize(xml, account_type, klass)
12
+ parse(xml)
13
+ @account_type = account_type
14
+ @klass = klass
15
+ end
16
+
17
+ def get(doc, sym)
18
+ unless element?(sym)
19
+ raise NoMethodError.new("undefined method '#{sym}' for " +
20
+ "#{self.inspect}:#{self.class.name}")
21
+ end
22
+
23
+ method = alias?(sym) ? alias_to_method(sym) : sym
24
+
25
+ # XXX: add an exception to handle this case!
26
+ unless permission?(method)
27
+ raise "an account type of #{account_type} cannot access this method, " +
28
+ "which requires at least #{account(method)}"
29
+ end
30
+
31
+ val = REXML::XPath.first(doc, table[method][:xpath])
32
+ return nil if val.nil?
33
+ return convert_to_type(method, val.text.to_s) if val.respond_to?(:text)
34
+ return convert_to_type(method, val.to_s)
35
+ end
36
+
37
+ def each
38
+ @table.sort {|a,b| a.to_s <=> b.to_s }.each do |k,v|
39
+ next if @table[k].has_key?(:alias)
40
+ next unless permission?(k)
41
+
42
+ yield k, v
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def standard?
49
+ return account_type.downcase == 'standard'
50
+ end
51
+
52
+ def power?
53
+ return account_type.downcase == 'power'
54
+ end
55
+
56
+ def pro?
57
+ return account_type.downcase == 'pro'
58
+ end
59
+
60
+ def parse(xml)
61
+ @table = {}
62
+
63
+ doc = REXML::Document.new(xml)
64
+ doc.root.elements.each do |element|
65
+ sym = element.name.to_sym
66
+ @table[sym] ||= {}
67
+
68
+ element.attributes.each do |k,v|
69
+ @table[sym][k.to_sym] = v.to_s
70
+ end
71
+
72
+ @table[sym][:xpath] ||= "//#{sym.to_s}"
73
+ @table[sym][:type] ||= "string"
74
+ @table[sym][:account] ||= "standard"
75
+
76
+ _alias(sym)
77
+ end
78
+ end
79
+
80
+ def _alias(method)
81
+ sym = method.to_s.gsub(/([a-z0-9])([A-Z])/) { "#{$1}_#{$2}" }.downcase
82
+ sym += '?' if @table[method][:type] == 'boolean'
83
+ sym = sym.to_sym
84
+
85
+ @table[sym] = {}
86
+ @table[sym][:alias] = method
87
+ end
88
+
89
+ def alias?(method)
90
+ return @table[method].has_key?(:alias)
91
+ end
92
+
93
+ def alias_to_method(method)
94
+ return @table[method][:alias]
95
+ end
96
+
97
+ def convert_to_type(sym, val)
98
+ puts "sym=#{sym.to_s} val=#{val}" if sym.to_s == 'test_id'
99
+ case xtype(sym)
100
+ when 'boolean': return (val.to_i == 0) ? false : true
101
+ when 'float': return val.to_f
102
+ when 'int': return val.to_i
103
+ else return val.to_s
104
+ end
105
+ end
106
+
107
+ def permission?(sym)
108
+ case account(sym)
109
+ when 'standard': return (standard? or power? or pro?)
110
+ when 'power': return (power? or pro?)
111
+ when 'pro': return pro?
112
+ else
113
+ raise "Do not understand an account type of '#{account(sym)}' for method #{sym.to_s}!"
114
+ end
115
+ end
116
+
117
+ def element?(sym)
118
+ return table.has_key?(sym)
119
+ end
120
+
121
+ def xpath(sym)
122
+ return @table[sym][:xpath]
123
+ end
124
+
125
+ def xtype(sym)
126
+ return @table[sym][:type]
127
+ end
128
+
129
+ def account(sym)
130
+ return @table[sym][:account]
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,12 @@
1
+ # -*- ruby -*-
2
+ # $Hg$
3
+
4
+ class File
5
+ def File.write(filename, data, offset=0)
6
+
7
+ File.open(filename, 'w') do |fh|
8
+ fh.seek(offset)
9
+ fh.write(data)
10
+ end
11
+ end
12
+ end
data/test/album.rb ADDED
@@ -0,0 +1,359 @@
1
+ # -*- Mode: ruby; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2
+ # $Hg$
3
+
4
+ require 'test/unit'
5
+ require 'smugmug'
6
+ require 'smugmug/album/info'
7
+ require 'smugmug/album/stats'
8
+
9
+
10
+ class TC_AlbumInfo < Test::Unit::TestCase
11
+ def setup
12
+ $smugmug.each do |album|
13
+ @album = album
14
+ break
15
+ end
16
+ end
17
+
18
+ def teardown
19
+ end
20
+
21
+ def test_raise_no_method
22
+ assert_raises(NoMethodError) { @album.No_SmugMug_Method }
23
+ end
24
+
25
+ ## ----------
26
+ ## accessors
27
+
28
+ def test_method
29
+ assert_equal('smugmug.albums.getInfo', @album.method)
30
+ end
31
+
32
+ def test_each_album
33
+ arr = %w{One Two Three}
34
+ $smugmug.each do |album|
35
+ assert_equal("Gallery #{arr.shift}", album.title)
36
+ end
37
+ end
38
+
39
+ def test_delegation
40
+ assert_equal(3, $smugmug.size)
41
+ assert($smugmug.any?)
42
+ assert_equal(3, @album.size)
43
+ assert(@album.any?)
44
+ end
45
+
46
+ ## ----------
47
+ ## dynamic accessors (method_missing)
48
+
49
+ ## Boolean ###############################################
50
+
51
+ def test_Public
52
+ val = @album.Public
53
+ assert_equal(true, val)
54
+ assert_kind_of(TrueClass, val)
55
+ assert_equal(val, @album.public?)
56
+ end
57
+
58
+ def test_Printable
59
+ val = @album.Printable
60
+ assert_equal(true, val)
61
+ assert_kind_of(TrueClass, val)
62
+ assert_kind_of(TrueClass, @album.printable?)
63
+ assert_equal(val, @album.printable?)
64
+ end
65
+
66
+ def test_Filenames
67
+ val = @album.Filenames
68
+ assert_equal(false, val)
69
+ assert_kind_of(FalseClass, val)
70
+ assert_kind_of(FalseClass, @album.filenames?)
71
+ assert_equal(val, @album.filenames?)
72
+ end
73
+
74
+ def test_Comments
75
+ val = @album.Comments
76
+ assert_equal(true, val)
77
+ assert_kind_of(TrueClass, val)
78
+ assert_kind_of(TrueClass, @album.comments?)
79
+ assert_equal(val, @album.comments?)
80
+ end
81
+
82
+ def test_External
83
+ val = @album.External
84
+ assert_equal(false, val)
85
+ assert_kind_of(FalseClass, val)
86
+ assert_kind_of(FalseClass, @album.external?)
87
+ assert_equal(val, @album.external?)
88
+ end
89
+
90
+ def test_Originals
91
+ val = @album.Originals
92
+ assert_equal(true, val)
93
+ assert_kind_of(TrueClass, val)
94
+ assert_kind_of(TrueClass, @album.originals?)
95
+ assert_equal(val, @album.originals?)
96
+ end
97
+
98
+ def test_EXIF
99
+ val = @album.EXIF
100
+ assert_equal(true, val)
101
+ assert_kind_of(TrueClass, val)
102
+ assert_kind_of(TrueClass, @album.exif?)
103
+ assert_equal(val, @album.exif?)
104
+ end
105
+
106
+ def test_Share
107
+ val = @album.Share
108
+ assert_equal(true, val)
109
+ assert_kind_of(TrueClass, val)
110
+ assert_kind_of(TrueClass, @album.share?)
111
+ assert_equal(val, @album.share?)
112
+ end
113
+
114
+ def test_SortDirection
115
+ val = @album.SortDirection
116
+ assert_equal(false, val)
117
+ assert_kind_of(FalseClass, val)
118
+ assert_kind_of(FalseClass, @album.sort_direction?)
119
+ assert_equal(val, @album.sort_direction?)
120
+ end
121
+
122
+ def test_FamilyEdit
123
+ val = @album.FamilyEdit
124
+ assert_equal(false, val)
125
+ assert_kind_of(FalseClass, val)
126
+ assert_kind_of(FalseClass, @album.family_edit?)
127
+ assert_equal(val, @album.family_edit?)
128
+ end
129
+
130
+ def test_FriendEdit
131
+ val = @album.FriendEdit
132
+ assert_equal(false, val)
133
+ assert_kind_of(FalseClass, val)
134
+ assert_kind_of(FalseClass, @album.friend_edit?)
135
+ assert_equal(val, @album.friend_edit?)
136
+ end
137
+
138
+ def test_HideOwner
139
+ val = @album.HideOwner
140
+ assert_equal(false, val)
141
+ assert_kind_of(FalseClass, val)
142
+ assert_kind_of(FalseClass, @album.hide_owner?)
143
+ assert_equal(val, @album.hide_owner?)
144
+ end
145
+
146
+ def test_CanRank
147
+ val = @album.CanRank
148
+ assert_equal(true, val)
149
+ assert_kind_of(TrueClass, val)
150
+ assert_kind_of(TrueClass, @album.can_rank?)
151
+ assert_equal(val, @album.can_rank?)
152
+ end
153
+
154
+ def test_Clean
155
+ assert_raises(RuntimeError) { @album.Clean }
156
+ end
157
+
158
+ def test_SmugSearchable
159
+ val = @album.SmugSearchable
160
+ assert_equal(false, val)
161
+ assert_kind_of(FalseClass, val)
162
+ assert_kind_of(FalseClass, @album.smug_searchable?)
163
+ assert_equal(val, @album.smug_searchable?)
164
+ end
165
+
166
+ def test_WorldSearchable
167
+ val = @album.WorldSearchable
168
+ assert_equal(false, val)
169
+ assert_kind_of(FalseClass, val)
170
+ assert_kind_of(FalseClass, @album.world_searchable?)
171
+ assert_equal(val, @album.world_searchable?)
172
+ end
173
+
174
+
175
+ ## Fixnum ################################################
176
+
177
+ def test_AlbumID
178
+ val = @album.AlbumID
179
+ assert_equal(1001, val)
180
+ assert_kind_of(Fixnum, val)
181
+ assert_kind_of(Fixnum, @album.album_id)
182
+ assert_equal(val, @album.album_id)
183
+ end
184
+
185
+ def test_HighlightID
186
+ val = @album.HighlightID
187
+ assert_equal(0, val)
188
+ assert_kind_of(Fixnum, val)
189
+ assert_kind_of(Fixnum, @album.highlight_id)
190
+ assert_equal(val, @album.highlight_id)
191
+ end
192
+
193
+ def test_CommunityID
194
+ val = @album.CommunityID
195
+ assert_equal(0, val)
196
+ assert_kind_of(Fixnum, val)
197
+ assert_kind_of(Fixnum, @album.community_id)
198
+ assert_equal(val, @album.community_id)
199
+ end
200
+
201
+ def test_CategoryID
202
+ val = @album.CategoryID
203
+ assert_equal(47, val)
204
+ assert_kind_of(Fixnum, val)
205
+ assert_kind_of(Fixnum, @album.category_id)
206
+ assert_equal(val, @album.category_id)
207
+ end
208
+
209
+ def test_ImageCount
210
+ val = @album.ImageCount
211
+ assert_equal(3, val)
212
+ assert_kind_of(Fixnum, val)
213
+ assert_kind_of(Fixnum, @album.image_count)
214
+ assert_equal(val, @album.image_count)
215
+ end
216
+
217
+ def test_Position
218
+ val = @album.Position
219
+ assert_equal(1, val)
220
+ assert_kind_of(Fixnum, val)
221
+ assert_kind_of(Fixnum, @album.position)
222
+ assert_equal(val, @album.position)
223
+ end
224
+
225
+
226
+ ## String ################################################
227
+
228
+ # def test_CategoryTitle
229
+ # val = @album.CategoryTitle
230
+ # assert_equal('Children', val)
231
+ # assert_kind_of(String, val)
232
+ # assert_kind_of(String, @album.category_title)
233
+ # assert_equal(val, @album.category_title)
234
+ # end
235
+
236
+ def test_Description
237
+ val = @album.Description
238
+ assert_equal('description', val)
239
+ assert_kind_of(String, val)
240
+ assert_kind_of(String, @album.description)
241
+ assert_equal(val, @album.description)
242
+ end
243
+
244
+ def test_Keywords
245
+ val = @album.Keywords
246
+ assert_equal('keywords', val)
247
+ assert_kind_of(String, val)
248
+ assert_kind_of(String, @album.keywords)
249
+ assert_equal(val, @album.keywords)
250
+ end
251
+
252
+ def test_LastUpdated
253
+ val = @album.LastUpdated
254
+ assert_equal('1970-01-01 00:00:00', val)
255
+ assert_kind_of(String, val)
256
+ assert_kind_of(String, @album.last_updated)
257
+ assert_equal(val, @album.last_updated)
258
+ end
259
+
260
+
261
+ def test_Password
262
+ val = @album.Password
263
+ assert_equal('', val)
264
+ assert_kind_of(String, val)
265
+ assert_kind_of(String, @album.password)
266
+ assert_equal(val, @album.password)
267
+ end
268
+
269
+ def test_PasswordHint
270
+ val = @album.PasswordHint
271
+ assert_equal('', val)
272
+ assert_kind_of(String, val)
273
+ assert_kind_of(String, @album.password_hint)
274
+ assert_equal(val, @album.password_hint)
275
+ end
276
+
277
+ def test_SortMethod
278
+ val = @album.SortMethod
279
+ assert_equal('Position', val)
280
+ assert_kind_of(String, val)
281
+ assert_kind_of(String, @album.sort_method)
282
+ assert_equal(val, @album.sort_method)
283
+ end
284
+
285
+ def test_Title
286
+ val = @album.Title
287
+ assert_equal('Gallery One', val)
288
+ assert_kind_of(String, val)
289
+ assert_kind_of(String, @album.title)
290
+ assert_equal(val, @album.title)
291
+ end
292
+ end
293
+
294
+ class TC_AlbumStats < Test::Unit::TestCase
295
+ def setup
296
+ $smugmug.each do |album|
297
+ @stats = album.stats
298
+ break
299
+ end
300
+
301
+ end
302
+
303
+ def teardown
304
+ end
305
+
306
+ def test_method
307
+ assert('smugmug.albums.getStats', @stats.method)
308
+ end
309
+
310
+ def test_bytes
311
+ assert_kind_of(Fixnum, @stats.bytes)
312
+ assert_equal(234659, @stats.bytes)
313
+ assert_kind_of(Fixnum, @stats.Bytes)
314
+ assert_equal(234659, @stats.Bytes)
315
+ end
316
+
317
+ def test_medium
318
+ assert_kind_of(Fixnum, @stats.medium)
319
+ assert_equal(2, @stats.medium)
320
+ assert_kind_of(Fixnum, @stats.Medium)
321
+ assert_equal(2, @stats.Medium)
322
+ end
323
+
324
+ def test_original
325
+ assert_kind_of(Float, @stats.original)
326
+ assert_equal(0, @stats.original)
327
+ assert_kind_of(Float, @stats.original)
328
+ assert_equal(0, @stats.original)
329
+ end
330
+
331
+ def test_small
332
+ assert_kind_of(Fixnum, @stats.small)
333
+ assert_equal(0, @stats.small)
334
+ assert_kind_of(Fixnum, @stats.Small)
335
+ assert_equal(0, @stats.Small)
336
+ end
337
+
338
+ ## XXX: should I support this?
339
+ # def test_status
340
+ # assert_kind_of(String, @stats.status)
341
+ # assert_equal('Open', @stats.status)
342
+ # assert_kind_of(String, @stats.Status)
343
+ # assert_equal('Open', @stats.Status)
344
+ # end
345
+
346
+ def test_thumb
347
+ assert_kind_of(Fixnum, @stats.thumb)
348
+ assert_equal(2, @stats.thumb)
349
+ assert_kind_of(Fixnum, @stats.Thumb)
350
+ assert_equal(2, @stats.Thumb)
351
+ end
352
+
353
+ def test_tiny
354
+ assert_kind_of(Fixnum, @stats.tiny)
355
+ assert_equal(20, @stats.tiny)
356
+ assert_kind_of(Fixnum, @stats.Tiny)
357
+ assert_equal(20, @stats.Tiny)
358
+ end
359
+ end