blob: f3a073db9395c16f7c0acb24942e39137029b62d [file] [log] [blame]
Richard Barnette56844c02016-10-04 14:24:05 -07001# Copyright 2016 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Richard Barnettebda07ea2016-10-26 18:55:24 -07005"""
6Unit tests for functions in `assign_stable_images`.
7"""
8
9
Ningning Xia7459c5d2018-02-09 09:17:26 -080010import mock
Richard Barnette56844c02016-10-04 14:24:05 -070011import unittest
12
13import common
Richard Barnette7412fa82016-10-21 15:29:51 -070014from autotest_lib.site_utils.stable_images import assign_stable_images
Richard Barnette8c1c1b82018-04-03 08:51:32 -070015from autotest_lib.site_utils.stable_images import build_data
Richard Barnette56844c02016-10-04 14:24:05 -070016
17
Richard Barnette56844c02016-10-04 14:24:05 -070018_DEFAULT_BOARD = assign_stable_images._DEFAULT_BOARD
19
20
Ningning Xia7459c5d2018-02-09 09:17:26 -080021class GetFirmwareUpgradesTests(unittest.TestCase):
22 """Tests for _get_firmware_upgrades."""
23
Richard Barnette8c1c1b82018-04-03 08:51:32 -070024 @mock.patch.object(build_data, 'get_firmware_versions')
Ningning Xia7459c5d2018-02-09 09:17:26 -080025 def test_get_firmware_upgrades(self, mock_get_firmware_versions):
26 """Test _get_firmware_upgrades."""
27 mock_get_firmware_versions.side_effect = [
Richard Barnette08e487d2018-03-30 18:39:31 -070028 {'auron_paine': 'fw_version'},
29 {'blue': 'fw_version',
30 'robo360': 'fw_version',
31 'porbeagle': 'fw_version'}
Ningning Xia7459c5d2018-02-09 09:17:26 -080032 ]
33 cros_versions = {
Richard Barnette08e487d2018-03-30 18:39:31 -070034 'coral': 'R64-10176.65.0',
35 'auron_paine': 'R64-10176.65.0'
Ningning Xia7459c5d2018-02-09 09:17:26 -080036 }
37 boards = ['auron_paine', 'coral']
38
39 firmware_upgrades = assign_stable_images._get_firmware_upgrades(
Richard Barnette08e487d2018-03-30 18:39:31 -070040 cros_versions)
Ningning Xia7459c5d2018-02-09 09:17:26 -080041 expected_firmware_upgrades = {
Richard Barnette08e487d2018-03-30 18:39:31 -070042 'auron_paine': 'fw_version',
43 'blue': 'fw_version',
44 'robo360': 'fw_version',
45 'porbeagle': 'fw_version'
Ningning Xia7459c5d2018-02-09 09:17:26 -080046 }
47 self.assertEqual(firmware_upgrades, expected_firmware_upgrades)
48
49
Richard Barnettebda07ea2016-10-26 18:55:24 -070050class GetUpgradeTests(unittest.TestCase):
51 """Tests for the `_get_upgrade_versions()` function."""
Richard Barnette56844c02016-10-04 14:24:05 -070052
53 # _VERSIONS - a list of sample version strings such as may be used
54 # for Chrome OS, sorted from oldest to newest. These are used to
55 # construct test data in multiple test cases, below.
56 _VERSIONS = ['R1-1.0.0', 'R1-1.1.0', 'R2-4.0.0']
57
58 def test_board_conversions(self):
59 """
60 Test proper mapping of names from the AFE to Omaha.
61
62 Board names in Omaha don't have '_' characters; when an AFE
63 board contains '_' characters, they must be converted to '-'.
64
65 Assert that for various forms of name in the AFE mapping, the
66 converted name is the one looked up in the Omaha mapping.
67 """
68 board_equivalents = [
69 ('a-b', 'a-b'), ('c_d', 'c-d'),
70 ('e_f-g', 'e-f-g'), ('hi', 'hi')]
71 afe_versions = {
72 _DEFAULT_BOARD: self._VERSIONS[0]
73 }
74 omaha_versions = {}
75 expected = {}
76 boards = set()
77 for afe_board, omaha_board in board_equivalents:
78 boards.add(afe_board)
79 afe_versions[afe_board] = self._VERSIONS[1]
80 omaha_versions[omaha_board] = self._VERSIONS[2]
81 expected[afe_board] = self._VERSIONS[2]
82 upgrades, _ = assign_stable_images._get_upgrade_versions(
83 afe_versions, omaha_versions, boards)
84 self.assertEqual(upgrades, expected)
85
86 def test_afe_default(self):
87 """
88 Test that the AFE default board mapping is honored.
89
90 If a board isn't present in the AFE dictionary, the mapping
91 for `_DEFAULT_BOARD` should be used.
92
93 Primary assertions:
94 * When a board is present in the AFE mapping, its version
95 mapping is used.
96 * When a board is not present in the AFE mapping, the default
97 version mapping is used.
98
99 Secondarily, assert that when a mapping is absent from Omaha,
100 the AFE mapping is left unchanged.
101 """
102 afe_versions = {
103 _DEFAULT_BOARD: self._VERSIONS[0],
104 'a': self._VERSIONS[1]
105 }
106 boards = set(['a', 'b'])
107 expected = {
108 'a': self._VERSIONS[1],
109 'b': self._VERSIONS[0]
110 }
111 upgrades, _ = assign_stable_images._get_upgrade_versions(
112 afe_versions, {}, boards)
113 self.assertEqual(upgrades, expected)
114
115 def test_omaha_upgrade(self):
116 """
117 Test that upgrades from Omaha are detected.
118
119 Primary assertion:
120 * If a board is found in Omaha, and the version in Omaha is
121 newer than the AFE version, the Omaha version is the one
122 used.
123
124 Secondarily, asserts that version comparisons between various
125 specific version strings are all correct.
126 """
127 boards = set(['a'])
128 for i in range(0, len(self._VERSIONS) - 1):
129 afe_versions = {_DEFAULT_BOARD: self._VERSIONS[i]}
130 for j in range(i+1, len(self._VERSIONS)):
131 omaha_versions = {b: self._VERSIONS[j] for b in boards}
132 upgrades, _ = assign_stable_images._get_upgrade_versions(
133 afe_versions, omaha_versions, boards)
134 self.assertEqual(upgrades, omaha_versions)
135
136 def test_no_upgrade(self):
137 """
138 Test that if Omaha is behind the AFE, it is ignored.
139
140 Primary assertion:
141 * If a board is found in Omaha, and the version in Omaha is
142 older than the AFE version, the AFE version is the one used.
143
144 Secondarily, asserts that version comparisons between various
145 specific version strings are all correct.
146 """
147 boards = set(['a'])
148 for i in range(1, len(self._VERSIONS)):
149 afe_versions = {_DEFAULT_BOARD: self._VERSIONS[i]}
150 expected = {b: self._VERSIONS[i] for b in boards}
151 for j in range(0, i):
152 omaha_versions = {b: self._VERSIONS[j] for b in boards}
153 upgrades, _ = assign_stable_images._get_upgrade_versions(
154 afe_versions, omaha_versions, boards)
155 self.assertEqual(upgrades, expected)
156
157 def test_ignore_unused_boards(self):
158 """
159 Test that unlisted boards are ignored.
160
161 Assert that boards present in the AFE or Omaha mappings aren't
162 included in the return mappings when they aren't in the passed
163 in set of boards.
164 """
165 unused_boards = set(['a', 'b'])
166 used_boards = set(['c', 'd'])
167 afe_versions = {b: self._VERSIONS[0] for b in unused_boards}
168 afe_versions[_DEFAULT_BOARD] = self._VERSIONS[1]
169 expected = {b: self._VERSIONS[1] for b in used_boards}
170 omaha_versions = expected.copy()
171 omaha_versions.update(
172 {b: self._VERSIONS[0] for b in unused_boards})
173 upgrades, _ = assign_stable_images._get_upgrade_versions(
174 afe_versions, omaha_versions, used_boards)
175 self.assertEqual(upgrades, expected)
176
177 def test_default_unchanged(self):
178 """
179 Test correct handling when the default build is unchanged.
180
181 Assert that if in Omaha, one board in a set of three upgrades
182 from the AFE default, that the returned default board mapping is
183 the original default in the AFE.
184 """
185 boards = set(['a', 'b', 'c'])
186 afe_versions = {_DEFAULT_BOARD: self._VERSIONS[0]}
187 omaha_versions = {b: self._VERSIONS[0] for b in boards}
188 omaha_versions['c'] = self._VERSIONS[1]
189 _, new_default = assign_stable_images._get_upgrade_versions(
190 afe_versions, omaha_versions, boards)
191 self.assertEqual(new_default, self._VERSIONS[0])
192
193 def test_default_upgrade(self):
194 """
195 Test correct handling when the default build must change.
196
197 Assert that if in Omaha, two boards in a set of three upgrade
198 from the AFE default, that the returned default board mapping is
199 the new build in Omaha.
200 """
201 boards = set(['a', 'b', 'c'])
202 afe_versions = {_DEFAULT_BOARD: self._VERSIONS[0]}
203 omaha_versions = {b: self._VERSIONS[1] for b in boards}
204 omaha_versions['c'] = self._VERSIONS[0]
205 _, new_default = assign_stable_images._get_upgrade_versions(
206 afe_versions, omaha_versions, boards)
207 self.assertEqual(new_default, self._VERSIONS[1])
208
209
Richard Barnettebda07ea2016-10-26 18:55:24 -0700210# Sample version string values to be used when testing
211# `_apply_upgrades()`.
212#
213# _OLD_DEFAULT - Test value representing the default version mapping
214# in the `old_versions` dictionary in a call to `_apply_upgrades()`.
215# _NEW_DEFAULT - Test value representing the default version mapping
216# in the `new_versions` dictionary when a version update is being
217# tested.
218# _OLD_VERSION - Test value representing an arbitrary version for a
219# board that is mapped in the `old_versions` dictionary in a call to
220# `_apply_upgrades()`.
221# _NEW_VERSION - Test value representing an arbitrary version for a
222# board that is mapped in the `new_versions` dictionary in a call to
223# `_apply_upgrades()`.
224#
225_OLD_DEFAULT = 'old-default-version'
226_NEW_DEFAULT = 'new-default-version'
227_OLD_VERSION = 'old-board-version'
228_NEW_VERSION = 'new-board-version'
229
230
Richard Barnette728e36f2016-11-03 16:04:29 -0700231class _StubAFE(object):
232 """Stubbed out version of `server.frontend.AFE`."""
233
234 CROS_IMAGE_TYPE = 'cros-image-type'
235 FIRMWARE_IMAGE_TYPE = 'firmware-image-type'
236
237 def get_stable_version_map(self, image_type):
238 return image_type
239
240
Richard Barnettebda07ea2016-10-26 18:55:24 -0700241class _TestUpdater(assign_stable_images._VersionUpdater):
242 """
243 Subclass of `_VersionUpdater` for testing.
244
245 This class extends `_VersionUpdater` to provide support for testing
246 various assertions about the behavior of the base class and its
247 interactions with `_apply_cros_upgrades()` and
248 `_apply_firmware_upgrades()`.
249
250 The class tests assertions along the following lines:
251 * When applied to the original mappings, the calls to
252 `_do_set_mapping()` and `_do_delete_mapping()` create the
253 expected final mapping state.
254 * Calls to report state changes are made with the expected
255 values.
256 * There's a one-to-one match between reported and actually
257 executed changes.
258
259 """
260
261 def __init__(self, testcase):
Prathmesh Prabhua80e3c42018-09-25 14:25:22 -0700262 super(_TestUpdater, self).__init__(_StubAFE(), dry_run=True)
Richard Barnettebda07ea2016-10-26 18:55:24 -0700263 self._testcase = testcase
264 self._default_changed = None
265 self._reported_mappings = None
266 self._updated_mappings = None
267 self._reported_deletions = None
268 self._actual_deletions = None
269 self._original_mappings = None
270 self._mappings = None
271 self._expected_mappings = None
272 self._unchanged_boards = None
273
274 def pretest_init(self, initial_versions, expected_versions):
275 """
276 Initialize for testing.
277
278 @param initial_versions Mappings to be used as the starting
279 point for testing.
280 @param expected_versions The expected final value of the
281 mappings after the test.
282 """
283 self._default_changed = False
284 self._reported_mappings = {}
285 self._updated_mappings = {}
286 self._reported_deletions = set()
287 self._actual_deletions = set()
288 self._original_mappings = initial_versions.copy()
289 self._mappings = initial_versions.copy()
290 self._expected_mappings = expected_versions
291 self._unchanged_boards = set()
292
293 def check_results(self, change_default):
294 """
295 Assert that observed changes match expectations.
296
297 Asserts the following:
298 * The `report_default_changed()` method was called (or not)
299 based on whether `change_default` is true (or not).
300 * The changes reported via `_report_board_changed()` match
301 the changes actually applied.
302 * The final mappings after applying requested changes match
303 the actually expected mappings.
304
305 @param old_versions Parameter to be passed to
306 `_apply_cros_upgrades()`.
307 @param new_versions Parameter to be passed to
308 `_apply_cros_upgrades()`.
309 @param change_default Whether the test should include a change
310 to the default version mapping.
311 """
312 self._testcase.assertEqual(change_default,
313 self._default_changed)
314 self._testcase.assertEqual(self._reported_mappings,
315 self._updated_mappings)
316 self._testcase.assertEqual(self._reported_deletions,
317 self._actual_deletions)
318 self._testcase.assertEqual(self._mappings,
319 self._expected_mappings)
320
Richard Barnettebda07ea2016-10-26 18:55:24 -0700321 def report_default_changed(self, old_default, new_default):
322 """
323 Override of our parent class' method for test purposes.
324
325 Saves a record of the report for testing the final result in
326 `apply_upgrades()`, above.
327
328 Assert the following:
329 * The old and new default values match the values that
330 were passed in the original call's arguments.
331 * This function is not being called for a second time.
332
333 @param old_default The original default version.
334 @param new_default The new default version to be applied.
335 """
336 self._testcase.assertNotEqual(old_default, new_default)
337 self._testcase.assertEqual(old_default,
338 self._original_mappings[_DEFAULT_BOARD])
339 self._testcase.assertEqual(new_default,
340 self._expected_mappings[_DEFAULT_BOARD])
341 self._testcase.assertFalse(self._default_changed)
342 self._default_changed = True
343 self._reported_mappings[_DEFAULT_BOARD] = new_default
344
345 def _report_board_changed(self, board, old_version, new_version):
346 """
347 Override of our parent class' method for test purposes.
348
349 Saves a record of the report for testing the final result in
350 `apply_upgrades()`, above.
351
352 Assert the following:
353 * The change being reported actually reports two different
354 versions.
355 * If the board isn't mapped to the default version, then the
356 reported old version is the actually mapped old version.
357 * If the board isn't changing to the default version, then the
358 reported new version is the expected new version.
359 * This is not a second report for this board.
360
361 The implementation implicitly requires that the specified board
362 have a valid mapping.
363
364 @param board The board with the changing version.
365 @param old_version The original version mapped to the board.
366 @param new_version The new version to be applied to the board.
367 """
368 self._testcase.assertNotEqual(old_version, new_version)
369 if board in self._original_mappings:
370 self._testcase.assertEqual(old_version,
371 self._original_mappings[board])
372 if board in self._expected_mappings:
373 self._testcase.assertEqual(new_version,
374 self._expected_mappings[board])
375 self._testcase.assertNotIn(board, self._reported_mappings)
376 self._reported_mappings[board] = new_version
377 else:
378 self._testcase.assertNotIn(board, self._reported_deletions)
379 self._reported_deletions.add(board)
380
381 def report_board_unchanged(self, board, old_version):
382 """
383 Override of our parent class' method for test purposes.
384
385 Assert the following:
386 * The version being reported as unchanged is actually mapped.
387 * The reported old version matches the expected value.
388 * This is not a second report for this board.
389
390 @param board The board that is not changing.
391 @param old_version The board's version mapping.
392 """
393 self._testcase.assertIn(board, self._original_mappings)
394 self._testcase.assertEqual(old_version,
395 self._original_mappings[board])
396 self._testcase.assertNotIn(board, self._unchanged_boards)
397 self._unchanged_boards.add(board)
398
399 def _do_set_mapping(self, board, new_version):
400 """
401 Override of our parent class' method for test purposes.
402
403 Saves a record of the change for testing the final result in
404 `apply_upgrades()`, above.
405
406 Assert the following:
407 * This is not a second change for this board.
408 * If we're changing the default mapping, then every board
409 that will be changing to a non-default mapping has been
410 updated.
411
412 @param board The board with the changing version.
413 @param new_version The new version to be applied to the board.
414 """
415 self._mappings[board] = new_version
416 self._testcase.assertNotIn(board, self._updated_mappings)
417 self._updated_mappings[board] = new_version
418 if board == _DEFAULT_BOARD:
419 for board in self._expected_mappings:
420 self._testcase.assertIn(board, self._mappings)
421
422 def _do_delete_mapping(self, board):
423 """
424 Override of our parent class' method for test purposes.
425
426 Saves a record of the change for testing the final result in
427 `apply_upgrades()`, above.
428
429 Assert that the board has a mapping prior to deletion.
430
431 @param board The board with the version to be deleted.
432 """
433 self._testcase.assertNotEqual(board, _DEFAULT_BOARD)
434 self._testcase.assertIn(board, self._mappings)
435 del self._mappings[board]
436 self._actual_deletions.add(board)
437
438
439class ApplyCrOSUpgradesTests(unittest.TestCase):
440 """Tests for the `_apply_cros_upgrades()` function."""
441
442 def _apply_upgrades(self, old_versions, new_versions, change_default):
443 """
444 Test a single call to `_apply_cros_upgrades()`.
445
446 All assertions are handled by an instance of `_TestUpdater`.
447
448 @param old_versions Parameter to be passed to
449 `_apply_cros_upgrades()`.
450 @param new_versions Parameter to be passed to
451 `_apply_cros_upgrades()`.
452 @param change_default Whether the test should include a change
453 to the default version mapping.
454 """
455 old_versions[_DEFAULT_BOARD] = _OLD_DEFAULT
456 if change_default:
457 new_default = _NEW_DEFAULT
458 else:
459 new_default = _OLD_DEFAULT
460 expected_versions = {
461 b: v for b, v in new_versions.items() if v != new_default
462 }
463 expected_versions[_DEFAULT_BOARD] = new_default
464 updater = _TestUpdater(self)
465 updater.pretest_init(old_versions, expected_versions)
466 assign_stable_images._apply_cros_upgrades(
467 updater, old_versions, new_versions, new_default)
468 updater.check_results(change_default)
469
470 def test_no_changes(self):
471 """
472 Test an empty upgrade that does nothing.
473
474 Test the boundary case of an upgrade where there are no boards,
475 and the default does not change.
476 """
477 self._apply_upgrades({}, {}, False)
478
479 def test_change_default(self):
480 """
481 Test an empty upgrade that merely changes the default.
482
483 Test the boundary case of an upgrade where there are no boards,
484 but the default is upgraded.
485 """
486 self._apply_upgrades({}, {}, True)
487
488 def test_board_default_no_changes(self):
489 """
490 Test that a board at default stays with an unchanged default.
491
492 Test the case of a board that is mapped to the default, where
493 neither the board nor the default change.
494 """
495 self._apply_upgrades({}, {'board': _OLD_DEFAULT}, False)
496
497 def test_board_left_behind(self):
498 """
499 Test a board left at the old default after a default upgrade.
500
501 Test the case of a board that stays mapped to the old default as
502 the default board is upgraded.
503 """
504 self._apply_upgrades({}, {'board': _OLD_DEFAULT}, True)
505
506 def test_board_upgrade_from_default(self):
507 """
508 Test upgrading a board from a default that doesn't change.
509
510 Test the case of upgrading a board from default to non-default,
511 where the default doesn't change.
512 """
513 self._apply_upgrades({}, {'board': _NEW_VERSION}, False)
514
515 def test_board_and_default_diverge(self):
516 """
517 Test upgrading a board that diverges from the default.
518
519 Test the case of upgrading a board and default together from the
520 same to different versions.
521 """
522 self._apply_upgrades({}, {'board': _NEW_VERSION}, True)
523
524 def test_board_tracks_default(self):
525 """
526 Test upgrading a board to track a default upgrade.
527
528 Test the case of upgrading a board and the default together.
529 """
530 self._apply_upgrades({}, {'board': _NEW_DEFAULT}, True)
531
532 def test_board_non_default_no_changes(self):
533 """
534 Test an upgrade with no changes to a board or the default.
535
536 Test the case of an upgrade with a board in it, where neither
537 the board nor the default change.
538 """
539 self._apply_upgrades({'board': _NEW_VERSION},
540 {'board': _NEW_VERSION},
541 False)
542
543 def test_board_upgrade_and_keep_default(self):
544 """
545 Test a board upgrade with an unchanged default.
546
547 Test the case of upgrading a board while the default stays the
548 same.
549 """
550 self._apply_upgrades({'board': _OLD_VERSION},
551 {'board': _NEW_VERSION},
552 False)
553
554 def test_board_upgrade_and_change_default(self):
555 """
556 Test upgrading a board and the default separately.
557
558 Test the case of upgrading both a board and the default, each
559 from and to different versions.
560 """
561 self._apply_upgrades({'board': _OLD_VERSION},
562 {'board': _NEW_VERSION},
563 True)
564
565 def test_board_leads_default(self):
566 """
567 Test a board that upgrades ahead of the new default.
568
569 Test the case of upgrading both a board and the default, where
570 the board's old version is the new default version.
571 """
572 self._apply_upgrades({'board': _NEW_DEFAULT},
573 {'board': _NEW_VERSION},
574 True)
575
576 def test_board_lags_to_old_default(self):
577 """
578 Test a board that upgrades behind the old default.
579
580 Test the case of upgrading both a board and the default, where
581 the board's new version is the old default version.
582 """
583 self._apply_upgrades({'board': _OLD_VERSION},
584 {'board': _OLD_DEFAULT},
585 True)
586
587 def test_board_joins_old_default(self):
588 """
589 Test upgrading a board to a default that doesn't change.
590
591 Test the case of upgrading board to the default, where the
592 default mapping stays unchanged.
593 """
594 self._apply_upgrades({'board': _OLD_VERSION},
595 {'board': _OLD_DEFAULT},
596 False)
597
598 def test_board_joins_new_default(self):
599 """
600 Test upgrading a board to match the new default.
601
602 Test the case of upgrading board and the default to the same
603 version.
604 """
605 self._apply_upgrades({'board': _OLD_VERSION},
606 {'board': _NEW_DEFAULT},
607 True)
608
609 def test_board_becomes_default(self):
610 """
611 Test a board that becomes default after a default upgrade.
612
613 Test the case of upgrading the default to a version already
614 mapped for an existing board.
615 """
616 self._apply_upgrades({'board': _NEW_DEFAULT},
617 {'board': _NEW_DEFAULT},
618 True)
619
620
Richard Barnette728e36f2016-11-03 16:04:29 -0700621class ApplyFirmwareUpgradesTests(unittest.TestCase):
622 """Tests for the `_apply_firmware_upgrades()` function."""
623
624 def _apply_upgrades(self, old_versions, new_versions):
625 """
626 Test a single call to `_apply_firmware_upgrades()`.
627
628 All assertions are handled by an instance of `_TestUpdater`.
629
630 @param old_versions Parameter to be passed to
631 `_apply_firmware_upgrades()`.
632 @param new_versions Parameter to be passed to
633 `_apply_firmware_upgrades()`.
634 """
635 updater = _TestUpdater(self)
636 updater.pretest_init(old_versions, new_versions)
637 assign_stable_images._apply_firmware_upgrades(
638 updater, old_versions, new_versions)
639 updater.check_results(False)
640
641 def test_no_changes(self):
642 """
643 Test an empty upgrade that does nothing.
644
645 Test the boundary case of an upgrade where there are no boards.
646 """
647 self._apply_upgrades({}, {})
648
649 def test_board_added(self):
650 """
651 Test an upgrade that adds a new board.
652
653 Test the case of an upgrade where a board that was previously
654 unmapped is added.
655 """
656 self._apply_upgrades({}, {'board': _NEW_VERSION})
657
658 def test_board_unchanged(self):
659 """
660 Test an upgrade with no changes to a board.
661
662 Test the case of an upgrade with a board that stays the same.
663 """
664 self._apply_upgrades({'board': _NEW_VERSION},
665 {'board': _NEW_VERSION})
666
667 def test_board_upgrade_and_change_default(self):
668 """
669 Test upgrading a board.
670
671 Test the case of upgrading a board to a new version.
672 """
673 self._apply_upgrades({'board': _OLD_VERSION},
674 {'board': _NEW_VERSION})
675
676
Richard Barnette56844c02016-10-04 14:24:05 -0700677if __name__ == '__main__':
678 unittest.main()