Perf-o-matic should memcache dashboard images
[WebKit-https.git] / Websites / webkit-perf.appspot.com / models_unittest.py
1 #!/usr/bin/env python
2 # Copyright (C) 2012 Google Inc. All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
6 # met:
7 #
8 #     * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 #     * Redistributions in binary form must reproduce the above
11 # copyright notice, this list of conditions and the following disclaimer
12 # in the documentation and/or other materials provided with the
13 # distribution.
14 #     * Neither the name of Google Inc. nor the names of its
15 # contributors may be used to endorse or promote products derived from
16 # this software without specific prior written permission.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30 import models
31 import unittest
32
33 from datetime import datetime
34 from datetime import timedelta
35 from google.appengine.api import memcache
36 from google.appengine.ext import testbed
37 from time import mktime
38
39 from models import NumericIdHolder
40 from models import Branch
41 from models import Platform
42 from models import Builder
43 from models import Build
44 from models import Test
45 from models import TestResult
46 from models import ReportLog
47 from models import PersistentCache
48 from models import DashboardImage
49 from models import create_in_transaction_with_numeric_id_holder
50 from models import delete_model_with_numeric_id_holder
51 from models import model_from_numeric_id
52
53
54 class DataStoreTestsBase(unittest.TestCase):
55     def setUp(self):
56         self.testbed = testbed.Testbed()
57         self.testbed.activate()
58         self.testbed.init_datastore_v3_stub()
59
60     def tearDown(self):
61         self.testbed.deactivate()
62
63     def assertThereIsNoInstanceOf(self, model):
64         self.assertEqual(len(model.all().fetch(5)), 0)
65
66     def assertOnlyInstance(self, only_instasnce):
67         self.assertOnlyInstances([only_instasnce])
68
69     def assertOnlyInstances(self, expected_instances):
70         actual_instances = expected_instances[0].__class__.all().fetch(len(expected_instances) + 1)
71         self.assertEqualUnorderedModelList(actual_instances, expected_instances)
72
73     def assertEqualUnorderedModelList(self, list1, list2):
74         self.assertEqualUnorderedList([item.key() for item in list1], [item.key() for item in list1])
75
76     def assertEqualUnorderedList(self, list1, list2):
77         self.assertEqual(set(list1), set(list2))
78         self.assertEqual(len(list1), len(list2))
79
80
81 class HelperTests(DataStoreTestsBase):
82     def _assert_there_is_exactly_one_id_holder_and_matches(self, id):
83         id_holders = NumericIdHolder.all().fetch(5)
84         self.assertEqual(len(id_holders), 1)
85         self.assertTrue(id_holders[0])
86         self.assertEqual(id_holders[0].key().id(), id)
87
88     def test_create_in_transaction_with_numeric_id_holder(self):
89
90         def execute(id):
91             return Branch(id=id, name='some branch', key_name='some-branch').put()
92
93         self.assertThereIsNoInstanceOf(Branch)
94         self.assertThereIsNoInstanceOf(NumericIdHolder)
95
96         self.assertTrue(create_in_transaction_with_numeric_id_holder(execute))
97
98         branches = Branch.all().fetch(5)
99         self.assertEqual(len(branches), 1)
100         self.assertEqual(branches[0].name, 'some branch')
101         self.assertEqual(branches[0].key().name(), 'some-branch')
102
103         self._assert_there_is_exactly_one_id_holder_and_matches(branches[0].id)
104
105     def test_failing_in_create_in_transaction_with_numeric_id_holder(self):
106
107         def execute(id):
108             return None
109
110         self.assertThereIsNoInstanceOf(Branch)
111         self.assertThereIsNoInstanceOf(NumericIdHolder)
112
113         self.assertFalse(create_in_transaction_with_numeric_id_holder(execute))
114
115         self.assertThereIsNoInstanceOf(Branch)
116         self.assertThereIsNoInstanceOf(NumericIdHolder)
117
118     def test_raising_in_create_in_transaction_with_numeric_id_holder(self):
119
120         def execute(id):
121             raise TypeError
122             return None
123
124         self.assertThereIsNoInstanceOf(Branch)
125         self.assertThereIsNoInstanceOf(NumericIdHolder)
126
127         self.assertRaises(TypeError, create_in_transaction_with_numeric_id_holder, (execute))
128
129         self.assertThereIsNoInstanceOf(Branch)
130         self.assertThereIsNoInstanceOf(NumericIdHolder)
131
132     def test_delete_model_with_numeric_id_holder(self):
133
134         def execute(id):
135             return Branch(id=id, name='some branch', key_name='some-branch').put()
136
137         branch = Branch.get(create_in_transaction_with_numeric_id_holder(execute))
138         self.assertOnlyInstance(branch)
139
140         delete_model_with_numeric_id_holder(branch)
141
142         self.assertThereIsNoInstanceOf(Branch)
143         self.assertThereIsNoInstanceOf(NumericIdHolder)
144
145     def test_model_from_numeric_id(self):
146
147         def execute(id):
148             return Branch(id=id, name='some branch', key_name='some-branch').put()
149
150         branch = Branch.get(create_in_transaction_with_numeric_id_holder(execute))
151
152         self.assertEqual(model_from_numeric_id(branch.id, Branch).key(), branch.key())
153         self.assertEqual(model_from_numeric_id(branch.id + 1, Branch), None)
154         delete_model_with_numeric_id_holder(branch)
155         self.assertEqual(model_from_numeric_id(branch.id, Branch), None)
156
157
158 class BranchTests(DataStoreTestsBase):
159     def test_create_if_possible(self):
160         self.assertThereIsNoInstanceOf(Branch)
161
162         branch = Branch.create_if_possible('some-branch', 'some branch')
163         self.assertTrue(branch)
164         self.assertTrue(branch.key().name(), 'some-branch')
165         self.assertTrue(branch.name, 'some branch')
166         self.assertOnlyInstance(branch)
167
168         self.assertFalse(Branch.create_if_possible('some-branch', 'some other branch'))
169         self.assertTrue(branch.name, 'some branch')
170         self.assertOnlyInstance(branch)
171
172
173 class PlatformTests(DataStoreTestsBase):
174     def test_create_if_possible(self):
175         self.assertThereIsNoInstanceOf(Platform)
176
177         platform = Platform.create_if_possible('some-platform', 'some platform')
178         self.assertTrue(platform)
179         self.assertTrue(platform.key().name(), 'some-platform')
180         self.assertTrue(platform.name, 'some platform')
181         self.assertOnlyInstance(platform)
182
183         self.assertFalse(Platform.create_if_possible('some-platform', 'some other platform'))
184         self.assertTrue(platform.name, 'some platform')
185         self.assertOnlyInstance(platform)
186
187
188 class BuilderTests(DataStoreTestsBase):
189     def test_create(self):
190         builder_key = Builder.create('some builder', 'some password')
191         self.assertTrue(builder_key)
192         builder = Builder.get(builder_key)
193         self.assertEqual(builder.key().name(), 'some builder')
194         self.assertEqual(builder.name, 'some builder')
195         self.assertEqual(builder.password, Builder._hashed_password('some password'))
196
197     def test_update_password(self):
198         builder = Builder.get(Builder.create('some builder', 'some password'))
199         self.assertEqual(builder.password, Builder._hashed_password('some password'))
200         builder.update_password('other password')
201         self.assertEqual(builder.password, Builder._hashed_password('other password'))
202
203         # Make sure it's saved
204         builder = Builder.get(builder.key())
205         self.assertEqual(builder.password, Builder._hashed_password('other password'))
206
207     def test_hashed_password(self):
208         self.assertNotEqual(Builder._hashed_password('some password'), 'some password')
209         self.assertFalse('some password' in Builder._hashed_password('some password'))
210         self.assertEqual(len(Builder._hashed_password('some password')), 64)
211
212     def test_authenticate(self):
213         builder = Builder.get(Builder.create('some builder', 'some password'))
214         self.assertTrue(builder.authenticate('some password'))
215         self.assertFalse(builder.authenticate('bad password'))
216
217
218 def _create_some_builder():
219     branch = Branch.create_if_possible('some-branch', 'Some Branch')
220     platform = Platform.create_if_possible('some-platform', 'Some Platform')
221     builder_key = Builder.create('some-builder', 'Some Builder')
222     return branch, platform, Builder.get(builder_key)
223
224
225 def _create_build(branch, platform, builder, key_name='some-build'):
226     build_key = Build(key_name=key_name, branch=branch, platform=platform, builder=builder,
227         buildNumber=1, revision=100, timestamp=datetime.now()).put()
228     return Build.get(build_key)
229
230
231 class BuildTests(DataStoreTestsBase):
232     def test_get_or_insert_from_log(self):
233         branch, platform, builder = _create_some_builder()
234
235         timestamp = datetime.now().replace(microsecond=0)
236         log = ReportLog(timestamp=timestamp, headers='some headers',
237             payload='{"branch": "some-branch", "platform": "some-platform", "builder-name": "some-builder",' +
238                 '"build-number": 123, "webkit-revision": 456, "timestamp": %d}' % int(mktime(timestamp.timetuple())))
239
240         self.assertThereIsNoInstanceOf(Build)
241
242         build = Build.get_or_insert_from_log(log)
243         self.assertTrue(build)
244         self.assertEqual(build.branch.key(), branch.key())
245         self.assertEqual(build.platform.key(), platform.key())
246         self.assertEqual(build.builder.key(), builder.key())
247         self.assertEqual(build.buildNumber, 123)
248         self.assertEqual(build.revision, 456)
249         self.assertEqual(build.chromiumRevision, None)
250         self.assertEqual(build.timestamp, timestamp)
251
252         self.assertOnlyInstance(build)
253
254
255 class TestModelTests(DataStoreTestsBase):
256     def test_update_or_insert(self):
257         branch = Branch.create_if_possible('some-branch', 'Some Branch')
258         platform = Platform.create_if_possible('some-platform', 'Some Platform')
259
260         self.assertThereIsNoInstanceOf(Test)
261
262         test = Test.update_or_insert('some-test', branch, platform)
263         self.assertTrue(test)
264         self.assertEqual(test.branches, [branch.key()])
265         self.assertEqual(test.platforms, [platform.key()])
266         self.assertOnlyInstance(test)
267
268     def test_update_or_insert_to_update(self):
269         branch = Branch.create_if_possible('some-branch', 'Some Branch')
270         platform = Platform.create_if_possible('some-platform', 'Some Platform')
271         test = Test.update_or_insert('some-test', branch, platform)
272         self.assertOnlyInstance(test)
273
274         other_branch = Branch.create_if_possible('other-branch', 'Other Branch')
275         other_platform = Platform.create_if_possible('other-platform', 'Other Platform')
276         test = Test.update_or_insert('some-test', other_branch, other_platform)
277         self.assertOnlyInstance(test)
278         self.assertEqualUnorderedList(test.branches, [branch.key(), other_branch.key()])
279         self.assertEqualUnorderedList(test.platforms, [platform.key(), other_platform.key()])
280
281         test = Test.get(test.key())
282         self.assertEqualUnorderedList(test.branches, [branch.key(), other_branch.key()])
283         self.assertEqualUnorderedList(test.platforms, [platform.key(), other_platform.key()])
284
285     def test_merge(self):
286         branch, platform, builder = _create_some_builder()
287         some_build = _create_build(branch, platform, builder)
288         some_result = TestResult.get_or_insert_from_parsed_json('some-test', some_build, 50)
289         some_test = Test.update_or_insert('some-test', branch, platform)
290
291         other_build = _create_build(branch, platform, builder, 'other-build')
292         other_result = TestResult.get_or_insert_from_parsed_json('other-test', other_build, 30)
293         other_test = Test.update_or_insert('other-test', branch, platform)
294
295         self.assertOnlyInstances([some_result, other_result])
296         self.assertNotEqual(some_result.key(), other_result.key())
297         self.assertOnlyInstances([some_test, other_test])
298
299         self.assertRaises(AssertionError, some_test.merge, (some_test))
300         self.assertOnlyInstances([some_test, other_test])
301
302         some_test.merge(other_test)
303         results_for_some_test = TestResult.all()
304         results_for_some_test.filter('name =', 'some-test')
305         results_for_some_test = results_for_some_test.fetch(5)
306         self.assertEqual(len(results_for_some_test), 2)
307
308         self.assertEqual(results_for_some_test[0].name, 'some-test')
309         self.assertEqual(results_for_some_test[1].name, 'some-test')
310
311         if results_for_some_test[0].value == 50:
312             self.assertEqual(results_for_some_test[1].value, 30)
313         else:
314             self.assertEqual(results_for_some_test[1].value, 50)
315
316
317 class TestResultTests(DataStoreTestsBase):
318     def test_get_or_insert_value(self):
319         branch, platform, builder = _create_some_builder()
320         build = _create_build(branch, platform, builder)
321         self.assertThereIsNoInstanceOf(TestResult)
322         result = TestResult.get_or_insert_from_parsed_json('some-test', build, 50)
323         self.assertOnlyInstance(result)
324         self.assertEqual(result.name, 'some-test')
325         self.assertEqual(result.build.key(), build.key())
326         self.assertEqual(result.value, 50.0)
327         self.assertEqual(result.valueMedian, None)
328         self.assertEqual(result.valueStdev, None)
329         self.assertEqual(result.valueMin, None)
330         self.assertEqual(result.valueMax, None)
331
332     def test_get_or_insert_stat_value(self):
333         branch, platform, builder = _create_some_builder()
334         build = _create_build(branch, platform, builder)
335         self.assertThereIsNoInstanceOf(TestResult)
336         result = TestResult.get_or_insert_from_parsed_json('some-test', build,
337             {"avg": 40, "median": "40.1", "stdev": 3.25, "min": 30.5, "max": 45})
338         self.assertOnlyInstance(result)
339         self.assertEqual(result.name, 'some-test')
340         self.assertEqual(result.build.key(), build.key())
341         self.assertEqual(result.value, 40.0)
342         self.assertEqual(result.valueMedian, 40.1)
343         self.assertEqual(result.valueStdev, 3.25)
344         self.assertEqual(result.valueMin, 30.5)
345         self.assertEqual(result.valueMax, 45)
346
347     def test_replace_to_change_test_name(self):
348         branch, platform, builder = _create_some_builder()
349         build = _create_build(branch, platform, builder)
350         self.assertThereIsNoInstanceOf(TestResult)
351         result = TestResult.get_or_insert_from_parsed_json('some-test', build, 50)
352         self.assertOnlyInstance(result)
353         self.assertEqual(result.name, 'some-test')
354
355         new_result = result.replace_to_change_test_name('other-test')
356         self.assertNotEqual(result, new_result)
357         self.assertOnlyInstance(new_result)
358
359         self.assertEqual(new_result.name, 'other-test')
360         self.assertEqual(new_result.build.key(), result.build.key())
361         self.assertEqual(new_result.value, result.value)
362         self.assertEqual(new_result.valueMedian, None)
363         self.assertEqual(new_result.valueStdev, None)
364         self.assertEqual(new_result.valueMin, None)
365         self.assertEqual(new_result.valueMax, None)
366
367     def test_replace_to_change_test_name_with_stat_value(self):
368         branch, platform, builder = _create_some_builder()
369         build = _create_build(branch, platform, builder)
370         self.assertThereIsNoInstanceOf(TestResult)
371         result = TestResult.get_or_insert_from_parsed_json('some-test', build,
372             {"avg": 40, "median": "40.1", "stdev": 3.25, "min": 30.5, "max": 45})
373         self.assertOnlyInstance(result)
374         self.assertEqual(result.name, 'some-test')
375
376         new_result = result.replace_to_change_test_name('other-test')
377         self.assertNotEqual(result, new_result)
378         self.assertOnlyInstance(new_result)
379
380         self.assertEqual(new_result.name, 'other-test')
381         self.assertEqual(new_result.build.key(), result.build.key())
382         self.assertEqual(new_result.value, result.value)
383         self.assertEqual(result.value, 40.0)
384         self.assertEqual(result.valueMedian, 40.1)
385         self.assertEqual(result.valueStdev, 3.25)
386         self.assertEqual(result.valueMin, 30.5)
387         self.assertEqual(result.valueMax, 45)
388
389     def test_replace_to_change_test_name_overrides_conflicting_result(self):
390         branch, platform, builder = _create_some_builder()
391         build = _create_build(branch, platform, builder)
392         self.assertThereIsNoInstanceOf(TestResult)
393         result = TestResult.get_or_insert_from_parsed_json('some-test', build, 20)
394         self.assertOnlyInstance(result)
395
396         conflicting_result = TestResult.get_or_insert_from_parsed_json('other-test', build, 10)
397
398         new_result = result.replace_to_change_test_name('other-test')
399         self.assertNotEqual(result, new_result)
400         self.assertOnlyInstance(new_result)
401
402         self.assertEqual(new_result.name, 'other-test')
403         self.assertEqual(TestResult.get(conflicting_result.key()).value, 20)
404
405
406 class ReportLogTests(DataStoreTestsBase):
407     def _create_log_with_payload(self, payload):
408         return ReportLog(timestamp=datetime.now(), headers='some headers', payload=payload)
409
410     def test_parsed_payload(self):
411         log = self._create_log_with_payload('')
412         self.assertFalse('_parsed' in log.__dict__)
413         self.assertEqual(log._parsed_payload(), False)
414         self.assertEqual(log._parsed, False)
415
416         log = self._create_log_with_payload('{"key": "value", "another key": 1}')
417         self.assertEqual(log._parsed_payload(), {"key": "value", "another key": 1})
418         self.assertEqual(log._parsed, {"key": "value", "another key": 1})
419
420     def test_get_value(self):
421         log = self._create_log_with_payload('{"string": "value", "integer": 1, "float": 1.1}')
422         self.assertEqual(log.get_value('string'), 'value')
423         self.assertEqual(log.get_value('integer'), 1)
424         self.assertEqual(log.get_value('float'), 1.1)
425         self.assertEqual(log.get_value('bad'), None)
426
427     def test_results(self):
428         log = self._create_log_with_payload('{"results": 123}')
429         self.assertEqual(log.results(), 123)
430
431         log = self._create_log_with_payload('{"key": "value"}')
432         self.assertEqual(log.results(), None)
433
434     def test_builder(self):
435         log = self._create_log_with_payload('{"key": "value"}')
436         self.assertEqual(log.builder(), None)
437
438         builder_name = "Chromium Mac Release (Perf)"
439         log = self._create_log_with_payload('{"builder-name": "%s"}' % builder_name)
440         self.assertEqual(log.builder(), None)
441
442         builder_key = Builder.create(builder_name, 'some password')
443         log = self._create_log_with_payload('{"builder-name": "%s"}' % builder_name)
444         self.assertEqual(log.builder().key(), builder_key)
445
446     def test_branch(self):
447         log = self._create_log_with_payload('{"key": "value"}')
448         self.assertEqual(log.branch(), None)
449
450         log = self._create_log_with_payload('{"branch": "some-branch"}')
451         self.assertEqual(log.branch(), None)
452
453         branch = Branch.create_if_possible("some-branch", "Some Branch")
454         log = self._create_log_with_payload('{"branch": "some-branch"}')
455         self.assertEqual(log.branch().key(), branch.key())
456
457     def test_platform(self):
458         log = self._create_log_with_payload('{"key": "value"}')
459         self.assertEqual(log.platform(), None)
460
461         log = self._create_log_with_payload('{"platform": "some-platform"}')
462         self.assertEqual(log.platform(), None)
463
464         platform = Platform.create_if_possible("some-platform", "Some Platform")
465         log = self._create_log_with_payload('{"platform": "some-platform"}')
466         self.assertEqual(log.platform().key(), platform.key())
467
468     def test_build_number(self):
469         log = self._create_log_with_payload('{"build-number": 123}')
470         self.assertEqual(log.build_number(), 123)
471
472         log = self._create_log_with_payload('{"key": "value"}')
473         self.assertEqual(log.build_number(), None)
474
475     def test_webkit_revision(self):
476         log = self._create_log_with_payload('{"key": "value"}')
477         self.assertEqual(log.webkit_revision(), None)
478
479         log = self._create_log_with_payload('{"webkit-revision": 123}')
480         self.assertEqual(log.webkit_revision(), 123)
481
482     def chromium_revision(self):
483         log = self._create_log_with_payload('{"chromium-revision": 123}')
484         self.assertEqual(log.webkit_revision(), 123)
485
486         log = self._create_log_with_payload('{"key": "value"}')
487         self.assertEqual(log.webkit_revision(), None)
488
489
490 class PersistentCacheTests(DataStoreTestsBase):
491     def setUp(self):
492         super(PersistentCacheTests, self).setUp()
493         self.testbed.init_memcache_stub()
494
495     def _assert_persistent_cache(self, name, value):
496         self.assertEqual(PersistentCache.get_by_key_name(name).value, value)
497         self.assertEqual(memcache.get(name), value)
498
499     def test_set_cache(self):
500         self.assertThereIsNoInstanceOf(PersistentCache)
501
502         PersistentCache.set_cache('some-cache', 'some data')
503         self._assert_persistent_cache('some-cache', 'some data')
504
505         PersistentCache.set_cache('some-cache', 'some other data')
506
507         self._assert_persistent_cache('some-cache', 'some other data')
508
509     def test_get_cache(self):
510         self.assertEqual(memcache.get('some-cache'), None)
511         self.assertEqual(PersistentCache.get_cache('some-cache'), None)
512
513         PersistentCache.set_cache('some-cache', 'some data')
514
515         self.assertEqual(memcache.get('some-cache'), 'some data')
516         self.assertEqual(PersistentCache.get_cache('some-cache'), 'some data')
517
518         memcache.delete('some-cache')
519         self.assertEqual(memcache.get('some-cache'), None)
520         self.assertEqual(PersistentCache.get_cache('some-cache'), 'some data')
521
522
523 class DashboardImageTests(DataStoreTestsBase):
524     def setUp(self):
525         super(DashboardImageTests, self).setUp()
526         self.testbed.init_memcache_stub()
527
528     def test_create(self):
529         self.assertEqual(memcache.get('dashboard-image:1:2:3:7'), None)
530         self.assertThereIsNoInstanceOf(DashboardImage)
531         image = DashboardImage.create(1, 2, 3, 7, 'blah')
532         self.assertOnlyInstance(image)
533         self.assertEqual(memcache.get('dashboard-image:1:2:3:7'), 'blah')
534
535     def test_get(self):
536         image = DashboardImage.create(1, 2, 3, 7, 'blah')
537         self.assertEqual(memcache.get('dashboard-image:1:2:3:7'), 'blah')
538         memcache.set('dashboard-image:1:2:3:7', 'new value')
539
540         # Check twice to make sure the first call doesn't clear memcache
541         self.assertEqual(DashboardImage.get_image(1, 2, 3, 7), 'new value')
542         self.assertEqual(DashboardImage.get_image(1, 2, 3, 7), 'new value')
543
544         memcache.delete('dashboard-image:1:2:3:7')
545         self.assertEqual(memcache.get('dashboard-image:1:2:3:7'), None)
546         self.assertEqual(DashboardImage.get_image(1, 2, 3, 7), 'blah')
547         self.assertEqual(memcache.get('dashboard-image:1:2:3:7'), 'blah')
548
549     def test_needs_update(self):
550         self.assertTrue(DashboardImage.needs_update(1, 2, 3, 7))
551         self.assertTrue(DashboardImage.needs_update(1, 2, 3, 30))
552         self.assertTrue(DashboardImage.needs_update(1, 2, 3, 60))
553         self.assertTrue(DashboardImage.needs_update(1, 2, 3, 365))
554
555         image = DashboardImage(key_name=DashboardImage.key_name(1, 2, 3, 7), image='blah')
556         image.put()
557         self.assertOnlyInstance(image)
558         self.assertTrue(DashboardImage.needs_update(1, 2, 3, 7))
559
560         DashboardImage(key_name=DashboardImage.key_name(1, 2, 3, 30), image='blah').put()
561         self.assertFalse(DashboardImage.needs_update(1, 2, 3, 30, datetime.now() + timedelta(0, 10)))
562
563         DashboardImage(key_name=DashboardImage.key_name(1, 2, 4, 30), image='blah').put()
564         self.assertTrue(DashboardImage.needs_update(1, 2, 4, 30, datetime.now() + timedelta(1)))
565
566         DashboardImage(key_name=DashboardImage.key_name(1, 2, 3, 90), image='blah').put()
567         self.assertFalse(DashboardImage.needs_update(1, 2, 3, 90, datetime.now() + timedelta(0, 20)))
568
569         DashboardImage(key_name=DashboardImage.key_name(1, 2, 4, 90), image='blah').put()
570         self.assertTrue(DashboardImage.needs_update(1, 2, 4, 90, datetime.now() + timedelta(1)))
571
572         DashboardImage(key_name=DashboardImage.key_name(1, 2, 3, 365), image='blah').put()
573         self.assertFalse(DashboardImage.needs_update(1, 2, 3, 365, datetime.now() + timedelta(1)))
574
575         DashboardImage(key_name=DashboardImage.key_name(1, 2, 4, 365), image='blah').put()
576         self.assertTrue(DashboardImage.needs_update(1, 2, 4, 365, datetime.now() + timedelta(10)))
577
578
579 if __name__ == '__main__':
580     unittest.main()