1 # 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 5 """Unit tests for the `repair` module.""" 6 7 # pylint: disable=missing-docstring 8 9 import functools 10 import logging 11 import unittest 12 13 import common 14 from autotest_lib.client.common_lib import hosts 15 from autotest_lib.client.common_lib.hosts import repair 16 from autotest_lib.server import constants 17 from autotest_lib.server.hosts import host_info 18 19 20 class _StubHost(object): 21 """ 22 Stub class to fill in the relevant methods of `Host`. 23 24 This class provides mocking and stub behaviors for `Host` for use by 25 tests within this module. The class implements only those methods 26 that `Verifier` and `RepairAction` actually use. 27 """ 28 29 def __init__(self): 30 self._record_sequence = [] 31 fake_board_name = constants.Labels.BOARD_PREFIX + 'fubar' 32 self.host_info_store = host_info.HostInfo(labels=[fake_board_name]) 33 34 35 def record(self, status_code, subdir, operation, status=''): 36 """ 37 Mock method to capture records written to `status.log`. 38 39 Each record is remembered in order to be checked for correctness 40 by individual tests later. 41 42 @param status_code As for `Host.record()`. 43 @param subdir As for `Host.record()`. 44 @param operation As for `Host.record()`. 45 @param status As for `Host.record()`. 46 """ 47 full_record = (status_code, subdir, operation, status) 48 self._record_sequence.append(full_record) 49 50 51 def get_log_records(self): 52 """ 53 Return the records logged for this fake host. 54 55 The returned list of records excludes records where the 56 `operation` parameter is not in `tagset`. 57 58 @param tagset Only include log records with these tags. 59 """ 60 return self._record_sequence 61 62 63 def reset_log_records(self): 64 """Clear our history of log records to allow re-testing.""" 65 self._record_sequence = [] 66 67 68 class _StubVerifier(hosts.Verifier): 69 """ 70 Stub implementation of `Verifier` for testing purposes. 71 72 This is a full implementation of a concrete `Verifier` subclass 73 designed to allow calling unit tests control over whether verify 74 passes or fails. 75 76 A `_StubVerifier()` will pass whenever the value of `_fail_count` 77 is non-zero. Calls to `try_repair()` (typically made by a 78 `_StubRepairAction()`) will reduce this count, eventually 79 "repairing" the verifier. 80 81 @property verify_count The number of calls made to the instance's 82 `verify()` method. 83 @property message If verification fails, the exception raised, 84 when converted to a string, will have this 85 value. 86 @property _fail_count The number of repair attempts required 87 before this verifier will succeed. A 88 non-zero value means verification will fail. 89 @property _description The value of the `description` property. 90 """ 91 92 def __init__(self, tag, deps, fail_count): 93 super(_StubVerifier, self).__init__(tag, deps) 94 self.verify_count = 0 95 self.message = 'Failing "%s" by request' % tag 96 self._fail_count = fail_count 97 self._description = 'Testing verify() for "%s"' % tag 98 self._log_record_map = { 99 r[0]: r for r in [ 100 ('GOOD', None, self._record_tag, ''), 101 ('FAIL', None, self._record_tag, self.message), 102 ] 103 } 104 105 106 def __repr__(self): 107 return '_StubVerifier(%r, %r, %r)' % ( 108 self.tag, self._dependency_list, self._fail_count) 109 110 111 def verify(self, host): 112 self.verify_count += 1 113 if self._fail_count: 114 raise hosts.AutoservVerifyError(self.message) 115 116 117 def try_repair(self): 118 """Bring ourselves one step closer to working.""" 119 if self._fail_count: 120 self._fail_count -= 1 121 122 123 def unrepair(self): 124 """Make ourselves more broken.""" 125 self._fail_count += 1 126 127 128 def get_log_record(self, status): 129 """ 130 Return a host log record for this verifier. 131 132 Calculates the arguments expected to be passed to 133 `Host.record()` by `Verifier._verify_host()` when this verifier 134 runs. The passed in `status` corresponds to the argument of the 135 same name to be passed to `Host.record()`. 136 137 @param status Status value of the log record. 138 """ 139 return self._log_record_map[status] 140 141 142 @property 143 def description(self): 144 return self._description 145 146 147 class _StubRepairFailure(Exception): 148 """Exception to be raised by `_StubRepairAction.repair()`.""" 149 pass 150 151 152 class _StubRepairAction(hosts.RepairAction): 153 """Stub implementation of `RepairAction` for testing purposes. 154 155 This is a full implementation of a concrete `RepairAction` subclass 156 designed to allow calling unit tests control over whether repair 157 passes or fails. 158 159 The behavior of `repair()` depends on the `_success` property of a 160 `_StubRepairAction`. When the property is true, repair will call 161 `try_repair()` for all triggers, and then report success. When the 162 property is false, repair reports failure. 163 164 @property repair_count The number of calls made to the instance's 165 `repair()` method. 166 @property message If repair fails, the exception raised, when 167 converted to a string, will have this value. 168 @property _success Whether repair will follow its "success" or 169 "failure" paths. 170 @property _description The value of the `description` property. 171 """ 172 173 def __init__(self, tag, deps, triggers, success): 174 super(_StubRepairAction, self).__init__(tag, deps, triggers) 175 self.repair_count = 0 176 self.message = 'Failed repair for "%s"' % tag 177 self._success = success 178 self._description = 'Testing repair for "%s"' % tag 179 self._log_record_map = { 180 r[0]: r for r in [ 181 ('START', None, self._record_tag, ''), 182 ('FAIL', None, self._record_tag, self.message), 183 ('END FAIL', None, self._record_tag, ''), 184 ('END GOOD', None, self._record_tag, ''), 185 ] 186 } 187 188 189 def __repr__(self): 190 return '_StubRepairAction(%r, %r, %r, %r)' % ( 191 self.tag, self._dependency_list, 192 self._trigger_list, self._success) 193 194 195 def repair(self, host): 196 self.repair_count += 1 197 if not self._success: 198 raise _StubRepairFailure(self.message) 199 for v in self._trigger_list: 200 v.try_repair() 201 202 203 def get_log_record(self, status): 204 """ 205 Return a host log record for this repair action. 206 207 Calculates the arguments expected to be passed to 208 `Host.record()` by `RepairAction._repair_host()` when repair 209 runs. The passed in `status` corresponds to the argument of the 210 same name to be passed to `Host.record()`. 211 212 @param status Status value of the log record. 213 """ 214 return self._log_record_map[status] 215 216 217 @property 218 def description(self): 219 return self._description 220 221 222 class _DependencyNodeTestCase(unittest.TestCase): 223 """ 224 Abstract base class for `RepairAction` and `Verifier` test cases. 225 226 This class provides `_make_verifier()` and `_make_repair_action()` 227 methods to create `_StubVerifier` and `_StubRepairAction` instances, 228 respectively, for testing. Constructed verifiers and repair actions 229 are remembered in `self.nodes`, a dictionary indexed by the tag 230 used to construct the object. 231 """ 232 233 def setUp(self): 234 logging.disable(logging.CRITICAL) 235 self._fake_host = _StubHost() 236 self.nodes = {} 237 238 239 def tearDown(self): 240 logging.disable(logging.NOTSET) 241 242 243 def _make_verifier(self, count, tag, deps): 244 """ 245 Make a `_StubVerifier` and remember it in `self.nodes`. 246 247 @param count As for the `_StubVerifer` constructor. 248 @param tag As for the `_StubVerifer` constructor. 249 @param deps As for the `_StubVerifer` constructor. 250 """ 251 verifier = _StubVerifier(tag, deps, count) 252 self.nodes[tag] = verifier 253 return verifier 254 255 256 def _make_repair_action(self, success, tag, deps, triggers): 257 """ 258 Make a `_StubRepairAction` and remember it in `self.nodes`. 259 260 @param success As for the `_StubRepairAction` constructor. 261 @param tag As for the `_StubRepairAction` constructor. 262 @param deps As for the `_StubRepairAction` constructor. 263 @param triggers As for the `_StubRepairAction` constructor. 264 """ 265 repair_action = _StubRepairAction(tag, deps, triggers, success) 266 self.nodes[tag] = repair_action 267 return repair_action 268 269 270 def _make_expected_failures(self, *verifiers): 271 """ 272 Make a set of `_DependencyFailure` objects from `verifiers`. 273 274 Return the set of `_DependencyFailure` objects that we would 275 expect to see in the `failures` attribute of an 276 `AutoservVerifyDependencyError` if all of the given verifiers 277 report failure. 278 279 @param verifiers A list of `_StubVerifier` objects that are 280 expected to fail. 281 282 @return A set of `_DependencyFailure` objects. 283 """ 284 failures = [repair._DependencyFailure(v.description, v.message) 285 for v in verifiers] 286 return set(failures) 287 288 289 def _generate_silent(self): 290 """ 291 Iterator to test different settings of the `silent` parameter. 292 293 This iterator exists to standardize testing assertions that 294 This iterator exists to standardize testing common 295 assertions about the `silent` parameter: 296 * When the parameter is true, no calls are made to the 297 `record()` method on the target host. 298 * When the parameter is false, certain expected calls are made 299 to the `record()` method on the target host. 300 301 The iterator is meant to be used like this: 302 303 for silent in self._generate_silent(): 304 # run test case that uses the silent parameter 305 self._check_log_records(silent, ... expected records ... ) 306 307 The code above will run its test case twice, once with 308 `silent=True` and once with `silent=False`. In between the 309 calls, log records are cleared. 310 311 @yields A boolean setting for `silent`. 312 """ 313 for silent in [False, True]: 314 yield silent 315 self._fake_host.reset_log_records() 316 317 318 def _check_log_records(self, silent, *record_data): 319 """ 320 Assert that log records occurred as expected. 321 322 Elements of `record_data` should be tuples of the form 323 `(tag, status)`, describing one expected log record. 324 The verifier or repair action for `tag` provides the expected 325 log record based on the status value. 326 327 The `silent` parameter is the value that was passed to the 328 verifier or repair action that did the logging. When true, 329 it indicates that no records should have been logged. 330 331 @param record_data List describing the expected record events. 332 @param silent When true, ignore `record_data` and assert 333 that nothing was logged. 334 """ 335 expected_records = [] 336 if not silent: 337 for tag, status in record_data: 338 expected_records.append( 339 self.nodes[tag].get_log_record(status)) 340 actual_records = self._fake_host.get_log_records() 341 self.assertEqual(expected_records, actual_records) 342 343 344 class VerifyTests(_DependencyNodeTestCase): 345 """ 346 Unit tests for `Verifier`. 347 348 The tests in this class test the fundamental behaviors of the 349 `Verifier` class: 350 * Results from the `verify()` method are cached; the method is 351 only called the first time that `_verify_host()` is called. 352 * The `_verify_host()` method makes the expected calls to 353 `Host.record()` for every call to the `verify()` method. 354 * When a dependency fails, the dependent verifier isn't called. 355 * Verifier calls are made in the order required by the DAG. 356 357 The test cases don't use `RepairStrategy` to build DAG structures, 358 but instead rely on custom-built DAGs. 359 """ 360 361 def _generate_verify_count(self, verifier): 362 """ 363 Iterator to force a standard sequence with calls to `_reverify()`. 364 365 This iterator exists to standardize testing two common 366 assertions: 367 * The side effects from calling `_verify_host()` only 368 happen on the first call to the method, except... 369 * Calling `_reverify()` resets a verifier so that the 370 next call to `_verify_host()` will repeat the side 371 effects. 372 373 The iterator is meant to be used like this: 374 375 for count in self._generate_verify_cases(verifier): 376 # run a verifier._verify_host() test case 377 self.assertEqual(verifier.verify_count, count) 378 self._check_log_records(silent, ... expected records ... ) 379 380 The code above will run the `_verify_host()` test case twice, 381 then call `_reverify()` to clear cached results, then re-run 382 the test case two more times. 383 384 @param verifier The verifier to be tested and reverified. 385 @yields Each iteration yields the number of times `_reverify()` 386 has been called. 387 """ 388 for i in range(1, 3): 389 for _ in range(0, 2): 390 yield i 391 verifier._reverify() 392 self._fake_host.reset_log_records() 393 394 395 def test_success(self): 396 """ 397 Test proper handling of a successful verification. 398 399 Construct and call a simple, single-node verification that will 400 pass. Assert the following: 401 * The `verify()` method is called once. 402 * The expected 'GOOD' record is logged via `Host.record()`. 403 * If `_verify_host()` is called more than once, there are no 404 visible side-effects after the first call. 405 * Calling `_reverify()` clears all cached results. 406 """ 407 for silent in self._generate_silent(): 408 verifier = self._make_verifier(0, 'pass', []) 409 for count in self._generate_verify_count(verifier): 410 verifier._verify_host(self._fake_host, silent) 411 self.assertEqual(verifier.verify_count, count) 412 self._check_log_records(silent, ('pass', 'GOOD')) 413 414 415 def test_fail(self): 416 """ 417 Test proper handling of verification failure. 418 419 Construct and call a simple, single-node verification that will 420 fail. Assert the following: 421 * The failure is reported with the actual exception raised 422 by the verifier. 423 * The `verify()` method is called once. 424 * The expected 'FAIL' record is logged via `Host.record()`. 425 * If `_verify_host()` is called more than once, there are no 426 visible side-effects after the first call. 427 * Calling `_reverify()` clears all cached results. 428 """ 429 for silent in self._generate_silent(): 430 verifier = self._make_verifier(1, 'fail', []) 431 for count in self._generate_verify_count(verifier): 432 with self.assertRaises(hosts.AutoservVerifyError) as e: 433 verifier._verify_host(self._fake_host, silent) 434 self.assertEqual(verifier.verify_count, count) 435 self.assertEqual(verifier.message, str(e.exception)) 436 self._check_log_records(silent, ('fail', 'FAIL')) 437 438 439 def test_dependency_success(self): 440 """ 441 Test proper handling of dependencies that succeed. 442 443 Construct and call a two-node verification with one node 444 dependent on the other, where both nodes will pass. Assert the 445 following: 446 * The `verify()` method for both nodes is called once. 447 * The expected 'GOOD' record is logged via `Host.record()` 448 for both nodes. 449 * If `_verify_host()` is called more than once, there are no 450 visible side-effects after the first call. 451 * Calling `_reverify()` clears all cached results. 452 """ 453 for silent in self._generate_silent(): 454 child = self._make_verifier(0, 'pass', []) 455 parent = self._make_verifier(0, 'parent', [child]) 456 for count in self._generate_verify_count(parent): 457 parent._verify_host(self._fake_host, silent) 458 self.assertEqual(parent.verify_count, count) 459 self.assertEqual(child.verify_count, count) 460 self._check_log_records(silent, 461 ('pass', 'GOOD'), 462 ('parent', 'GOOD')) 463 464 465 def test_dependency_fail(self): 466 """ 467 Test proper handling of dependencies that fail. 468 469 Construct and call a two-node verification with one node 470 dependent on the other, where the dependency will fail. Assert 471 the following: 472 * The verification exception is `AutoservVerifyDependencyError`, 473 and the exception argument is the description of the failed 474 node. 475 * The `verify()` method for the failing node is called once, 476 and for the other node, not at all. 477 * The expected 'FAIL' record is logged via `Host.record()` 478 for the single failed node. 479 * If `_verify_host()` is called more than once, there are no 480 visible side-effects after the first call. 481 * Calling `_reverify()` clears all cached results. 482 """ 483 for silent in self._generate_silent(): 484 child = self._make_verifier(1, 'fail', []) 485 parent = self._make_verifier(0, 'parent', [child]) 486 failures = self._make_expected_failures(child) 487 for count in self._generate_verify_count(parent): 488 expected_exception = hosts.AutoservVerifyDependencyError 489 with self.assertRaises(expected_exception) as e: 490 parent._verify_host(self._fake_host, silent) 491 self.assertEqual(e.exception.failures, failures) 492 self.assertEqual(child.verify_count, count) 493 self.assertEqual(parent.verify_count, 0) 494 self._check_log_records(silent, ('fail', 'FAIL')) 495 496 497 def test_two_dependencies_pass(self): 498 """ 499 Test proper handling with two passing dependencies. 500 501 Construct and call a three-node verification with one node 502 dependent on the other two, where all nodes will pass. Assert 503 the following: 504 * The `verify()` method for all nodes is called once. 505 * The expected 'GOOD' records are logged via `Host.record()` 506 for all three nodes. 507 * If `_verify_host()` is called more than once, there are no 508 visible side-effects after the first call. 509 * Calling `_reverify()` clears all cached results. 510 """ 511 for silent in self._generate_silent(): 512 left = self._make_verifier(0, 'left', []) 513 right = self._make_verifier(0, 'right', []) 514 top = self._make_verifier(0, 'top', [left, right]) 515 for count in self._generate_verify_count(top): 516 top._verify_host(self._fake_host, silent) 517 self.assertEqual(top.verify_count, count) 518 self.assertEqual(left.verify_count, count) 519 self.assertEqual(right.verify_count, count) 520 self._check_log_records(silent, 521 ('left', 'GOOD'), 522 ('right', 'GOOD'), 523 ('top', 'GOOD')) 524 525 526 def test_two_dependencies_fail(self): 527 """ 528 Test proper handling with two failing dependencies. 529 530 Construct and call a three-node verification with one node 531 dependent on the other two, where both dependencies will fail. 532 Assert the following: 533 * The verification exception is `AutoservVerifyDependencyError`, 534 and the exception argument has the descriptions of both the 535 failed nodes. 536 * The `verify()` method for each failing node is called once, 537 and for the parent node not at all. 538 * The expected 'FAIL' records are logged via `Host.record()` 539 for the failing nodes. 540 * If `_verify_host()` is called more than once, there are no 541 visible side-effects after the first call. 542 * Calling `_reverify()` clears all cached results. 543 """ 544 for silent in self._generate_silent(): 545 left = self._make_verifier(1, 'left', []) 546 right = self._make_verifier(1, 'right', []) 547 top = self._make_verifier(0, 'top', [left, right]) 548 failures = self._make_expected_failures(left, right) 549 for count in self._generate_verify_count(top): 550 expected_exception = hosts.AutoservVerifyDependencyError 551 with self.assertRaises(expected_exception) as e: 552 top._verify_host(self._fake_host, silent) 553 self.assertEqual(e.exception.failures, failures) 554 self.assertEqual(top.verify_count, 0) 555 self.assertEqual(left.verify_count, count) 556 self.assertEqual(right.verify_count, count) 557 self._check_log_records(silent, 558 ('left', 'FAIL'), 559 ('right', 'FAIL')) 560 561 562 def test_two_dependencies_mixed(self): 563 """ 564 Test proper handling with mixed dependencies. 565 566 Construct and call a three-node verification with one node 567 dependent on the other two, where one dependency will pass, 568 and one will fail. Assert the following: 569 * The verification exception is `AutoservVerifyDependencyError`, 570 and the exception argument has the descriptions of the 571 single failed node. 572 * The `verify()` method for each dependency is called once, 573 and for the parent node not at all. 574 * The expected 'GOOD' and 'FAIL' records are logged via 575 `Host.record()` for the dependencies. 576 * If `_verify_host()` is called more than once, there are no 577 visible side-effects after the first call. 578 * Calling `_reverify()` clears all cached results. 579 """ 580 for silent in self._generate_silent(): 581 left = self._make_verifier(1, 'left', []) 582 right = self._make_verifier(0, 'right', []) 583 top = self._make_verifier(0, 'top', [left, right]) 584 failures = self._make_expected_failures(left) 585 for count in self._generate_verify_count(top): 586 expected_exception = hosts.AutoservVerifyDependencyError 587 with self.assertRaises(expected_exception) as e: 588 top._verify_host(self._fake_host, silent) 589 self.assertEqual(e.exception.failures, failures) 590 self.assertEqual(top.verify_count, 0) 591 self.assertEqual(left.verify_count, count) 592 self.assertEqual(right.verify_count, count) 593 self._check_log_records(silent, 594 ('left', 'FAIL'), 595 ('right', 'GOOD')) 596 597 598 def test_diamond_pass(self): 599 """ 600 Test a "diamond" structure DAG with all nodes passing. 601 602 Construct and call a "diamond" structure DAG where all nodes 603 will pass: 604 605 TOP 606 / \ 607 LEFT RIGHT 608 \ / 609 BOTTOM 610 611 Assert the following: 612 * The `verify()` method for all nodes is called once. 613 * The expected 'GOOD' records are logged via `Host.record()` 614 for all nodes. 615 * If `_verify_host()` is called more than once, there are no 616 visible side-effects after the first call. 617 * Calling `_reverify()` clears all cached results. 618 """ 619 for silent in self._generate_silent(): 620 bottom = self._make_verifier(0, 'bottom', []) 621 left = self._make_verifier(0, 'left', [bottom]) 622 right = self._make_verifier(0, 'right', [bottom]) 623 top = self._make_verifier(0, 'top', [left, right]) 624 for count in self._generate_verify_count(top): 625 top._verify_host(self._fake_host, silent) 626 self.assertEqual(top.verify_count, count) 627 self.assertEqual(left.verify_count, count) 628 self.assertEqual(right.verify_count, count) 629 self.assertEqual(bottom.verify_count, count) 630 self._check_log_records(silent, 631 ('bottom', 'GOOD'), 632 ('left', 'GOOD'), 633 ('right', 'GOOD'), 634 ('top', 'GOOD')) 635 636 637 def test_diamond_fail(self): 638 """ 639 Test a "diamond" structure DAG with the bottom node failing. 640 641 Construct and call a "diamond" structure DAG where the bottom 642 node will fail: 643 644 TOP 645 / \ 646 LEFT RIGHT 647 \ / 648 BOTTOM 649 650 Assert the following: 651 * The verification exception is `AutoservVerifyDependencyError`, 652 and the exception argument has the description of the 653 "bottom" node. 654 * The `verify()` method for the "bottom" node is called once, 655 and for the other nodes not at all. 656 * The expected 'FAIL' record is logged via `Host.record()` 657 for the "bottom" node. 658 * If `_verify_host()` is called more than once, there are no 659 visible side-effects after the first call. 660 * Calling `_reverify()` clears all cached results. 661 """ 662 for silent in self._generate_silent(): 663 bottom = self._make_verifier(1, 'bottom', []) 664 left = self._make_verifier(0, 'left', [bottom]) 665 right = self._make_verifier(0, 'right', [bottom]) 666 top = self._make_verifier(0, 'top', [left, right]) 667 failures = self._make_expected_failures(bottom) 668 for count in self._generate_verify_count(top): 669 expected_exception = hosts.AutoservVerifyDependencyError 670 with self.assertRaises(expected_exception) as e: 671 top._verify_host(self._fake_host, silent) 672 self.assertEqual(e.exception.failures, failures) 673 self.assertEqual(top.verify_count, 0) 674 self.assertEqual(left.verify_count, 0) 675 self.assertEqual(right.verify_count, 0) 676 self.assertEqual(bottom.verify_count, count) 677 self._check_log_records(silent, ('bottom', 'FAIL')) 678 679 680 class RepairActionTests(_DependencyNodeTestCase): 681 """ 682 Unit tests for `RepairAction`. 683 684 The tests in this class test the fundamental behaviors of the 685 `RepairAction` class: 686 * Repair doesn't run unless all dependencies pass. 687 * Repair doesn't run unless at least one trigger fails. 688 * Repair reports the expected value of `status` for metrics. 689 * The `_repair_host()` method makes the expected calls to 690 `Host.record()` for every call to the `repair()` method. 691 692 The test cases don't use `RepairStrategy` to build repair 693 graphs, but instead rely on custom-built structures. 694 """ 695 696 def test_repair_not_triggered(self): 697 """ 698 Test a repair that doesn't trigger. 699 700 Construct and call a repair action with a verification trigger 701 that passes. Assert the following: 702 * The `verify()` method for the trigger is called. 703 * The `repair()` method is not called. 704 * The repair action's `status` field is 'untriggered'. 705 * The verifier logs the expected 'GOOD' message with 706 `Host.record()`. 707 * The repair action logs no messages with `Host.record()`. 708 """ 709 for silent in self._generate_silent(): 710 verifier = self._make_verifier(0, 'check', []) 711 repair_action = self._make_repair_action(True, 'unneeded', 712 [], [verifier]) 713 repair_action._repair_host(self._fake_host, silent) 714 self.assertEqual(verifier.verify_count, 1) 715 self.assertEqual(repair_action.repair_count, 0) 716 self.assertEqual(repair_action.status, 'untriggered') 717 self._check_log_records(silent, ('check', 'GOOD')) 718 719 720 def test_repair_fails(self): 721 """ 722 Test a repair that triggers and fails. 723 724 Construct and call a repair action with a verification trigger 725 that fails. The repair fails by raising `_StubRepairFailure`. 726 Assert the following: 727 * The repair action fails with the `_StubRepairFailure` raised 728 by `repair()`. 729 * The `verify()` method for the trigger is called once. 730 * The `repair()` method is called once. 731 * The repair action's `status` field is 'failed-action'. 732 * The expected 'START', 'FAIL', and 'END FAIL' messages are 733 logged with `Host.record()` for the failed verifier and the 734 failed repair. 735 """ 736 for silent in self._generate_silent(): 737 verifier = self._make_verifier(1, 'fail', []) 738 repair_action = self._make_repair_action(False, 'nofix', 739 [], [verifier]) 740 with self.assertRaises(_StubRepairFailure) as e: 741 repair_action._repair_host(self._fake_host, silent) 742 self.assertEqual(repair_action.message, str(e.exception)) 743 self.assertEqual(verifier.verify_count, 1) 744 self.assertEqual(repair_action.repair_count, 1) 745 self.assertEqual(repair_action.status, 'failed-action') 746 self._check_log_records(silent, 747 ('fail', 'FAIL'), 748 ('nofix', 'START'), 749 ('nofix', 'FAIL'), 750 ('nofix', 'END FAIL')) 751 752 753 def test_repair_success(self): 754 """ 755 Test a repair that fixes its trigger. 756 757 Construct and call a repair action that raises no exceptions, 758 using a repair trigger that fails first, then passes after 759 repair. Assert the following: 760 * The `repair()` method is called once. 761 * The trigger's `verify()` method is called twice. 762 * The repair action's `status` field is 'repaired'. 763 * The expected 'START', 'FAIL', 'GOOD', and 'END GOOD' 764 messages are logged with `Host.record()` for the verifier 765 and the repair. 766 """ 767 for silent in self._generate_silent(): 768 verifier = self._make_verifier(1, 'fail', []) 769 repair_action = self._make_repair_action(True, 'fix', 770 [], [verifier]) 771 repair_action._repair_host(self._fake_host, silent) 772 self.assertEqual(repair_action.repair_count, 1) 773 self.assertEqual(verifier.verify_count, 2) 774 self.assertEqual(repair_action.status, 'repaired') 775 self._check_log_records(silent, 776 ('fail', 'FAIL'), 777 ('fix', 'START'), 778 ('fail', 'GOOD'), 779 ('fix', 'END GOOD')) 780 781 782 def test_repair_noop(self): 783 """ 784 Test a repair that doesn't fix a failing trigger. 785 786 Construct and call a repair action with a trigger that fails. 787 The repair action raises no exceptions, and after repair, the 788 trigger still fails. Assert the following: 789 * The `_repair_host()` call fails with `AutoservRepairError`. 790 * The `repair()` method is called once. 791 * The trigger's `verify()` method is called twice. 792 * The repair action's `status` field is 'failed-trigger'. 793 * The expected 'START', 'FAIL', and 'END FAIL' messages are 794 logged with `Host.record()` for the verifier and the repair. 795 """ 796 for silent in self._generate_silent(): 797 verifier = self._make_verifier(2, 'fail', []) 798 repair_action = self._make_repair_action(True, 'nofix', 799 [], [verifier]) 800 with self.assertRaises(hosts.AutoservRepairError) as e: 801 repair_action._repair_host(self._fake_host, silent) 802 self.assertEqual(repair_action.repair_count, 1) 803 self.assertEqual(verifier.verify_count, 2) 804 self.assertEqual(repair_action.status, 'failed-trigger') 805 self._check_log_records(silent, 806 ('fail', 'FAIL'), 807 ('nofix', 'START'), 808 ('fail', 'FAIL'), 809 ('nofix', 'END FAIL')) 810 811 812 def test_dependency_pass(self): 813 """ 814 Test proper handling of repair dependencies that pass. 815 816 Construct and call a repair action with a dependency and a 817 trigger. The dependency will pass and the trigger will fail and 818 be repaired. Assert the following: 819 * Repair passes. 820 * The `verify()` method for the dependency is called once. 821 * The `verify()` method for the trigger is called twice. 822 * The `repair()` method is called once. 823 * The repair action's `status` field is 'repaired'. 824 * The expected records are logged via `Host.record()` 825 for the successful dependency, the failed trigger, and 826 the successful repair. 827 """ 828 for silent in self._generate_silent(): 829 dep = self._make_verifier(0, 'dep', []) 830 trigger = self._make_verifier(1, 'trig', []) 831 repair = self._make_repair_action(True, 'fixit', 832 [dep], [trigger]) 833 repair._repair_host(self._fake_host, silent) 834 self.assertEqual(dep.verify_count, 1) 835 self.assertEqual(trigger.verify_count, 2) 836 self.assertEqual(repair.repair_count, 1) 837 self.assertEqual(repair.status, 'repaired') 838 self._check_log_records(silent, 839 ('dep', 'GOOD'), 840 ('trig', 'FAIL'), 841 ('fixit', 'START'), 842 ('trig', 'GOOD'), 843 ('fixit', 'END GOOD')) 844 845 846 def test_dependency_fail(self): 847 """ 848 Test proper handling of repair dependencies that fail. 849 850 Construct and call a repair action with a dependency and a 851 trigger, both of which fail. Assert the following: 852 * Repair fails with `AutoservVerifyDependencyError`, 853 and the exception argument is the description of the failed 854 dependency. 855 * The `verify()` method for the failing dependency is called 856 once. 857 * The trigger and the repair action aren't invoked at all. 858 * The repair action's `status` field is 'blocked'. 859 * The expected 'FAIL' record is logged via `Host.record()` 860 for the single failed dependency. 861 """ 862 for silent in self._generate_silent(): 863 dep = self._make_verifier(1, 'dep', []) 864 trigger = self._make_verifier(1, 'trig', []) 865 repair = self._make_repair_action(True, 'fixit', 866 [dep], [trigger]) 867 expected_exception = hosts.AutoservVerifyDependencyError 868 with self.assertRaises(expected_exception) as e: 869 repair._repair_host(self._fake_host, silent) 870 self.assertEqual(e.exception.failures, 871 self._make_expected_failures(dep)) 872 self.assertEqual(dep.verify_count, 1) 873 self.assertEqual(trigger.verify_count, 0) 874 self.assertEqual(repair.repair_count, 0) 875 self.assertEqual(repair.status, 'blocked') 876 self._check_log_records(silent, ('dep', 'FAIL')) 877 878 879 class _RepairStrategyTestCase(_DependencyNodeTestCase): 880 """Shared base class for testing `RepairStrategy` methods.""" 881 882 def _make_verify_data(self, *input_data): 883 """ 884 Create `verify_data` for the `RepairStrategy` constructor. 885 886 `RepairStrategy` expects `verify_data` as a list of tuples 887 of the form `(constructor, tag, deps)`. Each item in 888 `input_data` is a tuple of the form `(tag, count, deps)` that 889 creates one entry in the returned list of `verify_data` tuples 890 as follows: 891 * `count` is used to create a constructor function that calls 892 `self._make_verifier()` with that value plus plus the 893 arguments provided by the `RepairStrategy` constructor. 894 * `tag` and `deps` will be passed as-is to the `RepairStrategy` 895 constructor. 896 897 @param input_data A list of tuples, each representing one 898 tuple in the `verify_data` list. 899 @return A list suitable to be the `verify_data` parameter for 900 the `RepairStrategy` constructor. 901 """ 902 strategy_data = [] 903 for tag, count, deps in input_data: 904 construct = functools.partial(self._make_verifier, count) 905 strategy_data.append((construct, tag, deps)) 906 return strategy_data 907 908 909 def _make_repair_data(self, *input_data): 910 """ 911 Create `repair_data` for the `RepairStrategy` constructor. 912 913 `RepairStrategy` expects `repair_data` as a list of tuples 914 of the form `(constructor, tag, deps, triggers)`. Each item in 915 `input_data` is a tuple of the form `(tag, success, deps, triggers)` 916 that creates one entry in the returned list of `repair_data` 917 tuples as follows: 918 * `success` is used to create a constructor function that calls 919 `self._make_verifier()` with that value plus plus the 920 arguments provided by the `RepairStrategy` constructor. 921 * `tag`, `deps`, and `triggers` will be passed as-is to the 922 `RepairStrategy` constructor. 923 924 @param input_data A list of tuples, each representing one 925 tuple in the `repair_data` list. 926 @return A list suitable to be the `repair_data` parameter for 927 the `RepairStrategy` constructor. 928 """ 929 strategy_data = [] 930 for tag, success, deps, triggers in input_data: 931 construct = functools.partial(self._make_repair_action, success) 932 strategy_data.append((construct, tag, deps, triggers)) 933 return strategy_data 934 935 936 def _make_strategy(self, verify_input, repair_input): 937 """ 938 Create a `RepairStrategy` from the given arguments. 939 940 @param verify_input As for `input_data` in 941 `_make_verify_data()`. 942 @param repair_input As for `input_data` in 943 `_make_repair_data()`. 944 """ 945 verify_data = self._make_verify_data(*verify_input) 946 repair_data = self._make_repair_data(*repair_input) 947 return hosts.RepairStrategy(verify_data, repair_data) 948 949 def _check_silent_records(self, silent): 950 """ 951 Check that logging honored the `silent` parameter. 952 953 Asserts that logging with `Host.record()` occurred (or did not 954 occur) in accordance with the value of `silent`. 955 956 This method only asserts the presence or absence of log records. 957 Coverage for the contents of the log records is handled in other 958 test cases. 959 960 @param silent When true, there should be no log records; 961 otherwise there should be records present. 962 """ 963 log_records = self._fake_host.get_log_records() 964 if silent: 965 self.assertEqual(log_records, []) 966 else: 967 self.assertNotEqual(log_records, []) 968 969 970 class RepairStrategyVerifyTests(_RepairStrategyTestCase): 971 """ 972 Unit tests for `RepairStrategy.verify()`. 973 974 These unit tests focus on verifying that the `RepairStrategy` 975 constructor creates the expected DAG structure from given 976 `verify_data`. Functional testing here is mainly confined to 977 asserting that `RepairStrategy.verify()` properly distinguishes 978 success from failure. Testing the behavior of specific DAG 979 structures is left to tests in `VerifyTests`. 980 """ 981 982 def test_single_node(self): 983 """ 984 Test construction of a single-node verification DAG. 985 986 Assert that the structure looks like this: 987 988 Root Node -> Main Node 989 """ 990 verify_data = self._make_verify_data(('main', 0, ())) 991 strategy = hosts.RepairStrategy(verify_data, []) 992 verifier = self.nodes['main'] 993 self.assertEqual( 994 strategy._verify_root._dependency_list, 995 [verifier]) 996 self.assertEqual(verifier._dependency_list, []) 997 998 999 def test_single_dependency(self): 1000 """ 1001 Test construction of a two-node dependency chain. 1002 1003 Assert that the structure looks like this: 1004 1005 Root Node -> Parent Node -> Child Node 1006 """ 1007 verify_data = self._make_verify_data( 1008 ('child', 0, ()), 1009 ('parent', 0, ('child',))) 1010 strategy = hosts.RepairStrategy(verify_data, []) 1011 parent = self.nodes['parent'] 1012 child = self.nodes['child'] 1013 self.assertEqual( 1014 strategy._verify_root._dependency_list, [parent]) 1015 self.assertEqual( 1016 parent._dependency_list, [child]) 1017 self.assertEqual( 1018 child._dependency_list, []) 1019 1020 1021 def test_two_nodes_and_dependency(self): 1022 """ 1023 Test construction of two nodes with a shared dependency. 1024 1025 Assert that the structure looks like this: 1026 1027 Root Node -> Left Node ---\ 1028 \ -> Bottom Node 1029 -> Right Node / 1030 """ 1031 verify_data = self._make_verify_data( 1032 ('bottom', 0, ()), 1033 ('left', 0, ('bottom',)), 1034 ('right', 0, ('bottom',))) 1035 strategy = hosts.RepairStrategy(verify_data, []) 1036 bottom = self.nodes['bottom'] 1037 left = self.nodes['left'] 1038 right = self.nodes['right'] 1039 self.assertEqual( 1040 strategy._verify_root._dependency_list, 1041 [left, right]) 1042 self.assertEqual(left._dependency_list, [bottom]) 1043 self.assertEqual(right._dependency_list, [bottom]) 1044 self.assertEqual(bottom._dependency_list, []) 1045 1046 1047 def test_three_nodes(self): 1048 """ 1049 Test construction of three nodes with no dependencies. 1050 1051 Assert that the structure looks like this: 1052 1053 -> Node One 1054 / 1055 Root Node -> Node Two 1056 \ 1057 -> Node Three 1058 1059 N.B. This test exists to enforce ordering expectations of 1060 root-level DAG nodes. Three nodes are used to make it unlikely 1061 that randomly ordered roots will match expectations. 1062 """ 1063 verify_data = self._make_verify_data( 1064 ('one', 0, ()), 1065 ('two', 0, ()), 1066 ('three', 0, ())) 1067 strategy = hosts.RepairStrategy(verify_data, []) 1068 one = self.nodes['one'] 1069 two = self.nodes['two'] 1070 three = self.nodes['three'] 1071 self.assertEqual( 1072 strategy._verify_root._dependency_list, 1073 [one, two, three]) 1074 self.assertEqual(one._dependency_list, []) 1075 self.assertEqual(two._dependency_list, []) 1076 self.assertEqual(three._dependency_list, []) 1077 1078 1079 def test_verify(self): 1080 """ 1081 Test behavior of the `verify()` method. 1082 1083 Build a `RepairStrategy` with a single verifier. Assert the 1084 following: 1085 * If the verifier passes, `verify()` passes. 1086 * If the verifier fails, `verify()` fails. 1087 * The verifier is reinvoked with every call to `verify()`; 1088 cached results are not re-used. 1089 """ 1090 verify_data = self._make_verify_data(('tester', 0, ())) 1091 strategy = hosts.RepairStrategy(verify_data, []) 1092 verifier = self.nodes['tester'] 1093 count = 0 1094 for silent in self._generate_silent(): 1095 for i in range(0, 2): 1096 for j in range(0, 2): 1097 strategy.verify(self._fake_host, silent) 1098 self._check_silent_records(silent) 1099 count += 1 1100 self.assertEqual(verifier.verify_count, count) 1101 verifier.unrepair() 1102 for j in range(0, 2): 1103 with self.assertRaises(Exception) as e: 1104 strategy.verify(self._fake_host, silent) 1105 self._check_silent_records(silent) 1106 count += 1 1107 self.assertEqual(verifier.verify_count, count) 1108 verifier.try_repair() 1109 1110 1111 class RepairStrategyRepairTests(_RepairStrategyTestCase): 1112 """ 1113 Unit tests for `RepairStrategy.repair()`. 1114 1115 These unit tests focus on verifying that the `RepairStrategy` 1116 constructor creates the expected repair list from given 1117 `repair_data`. Functional testing here is confined to asserting 1118 that `RepairStrategy.repair()` properly distinguishes success from 1119 failure. Testing the behavior of specific repair structures is left 1120 to tests in `RepairActionTests`. 1121 """ 1122 1123 def _check_common_trigger(self, strategy, repair_tags, triggers): 1124 self.assertEqual(strategy._repair_actions, 1125 [self.nodes[tag] for tag in repair_tags]) 1126 for tag in repair_tags: 1127 self.assertEqual(self.nodes[tag]._trigger_list, 1128 triggers) 1129 self.assertEqual(self.nodes[tag]._dependency_list, []) 1130 1131 1132 def test_single_repair_with_trigger(self): 1133 """ 1134 Test constructing a strategy with a single repair trigger. 1135 1136 Build a `RepairStrategy` with a single repair action and a 1137 single trigger. Assert that the trigger graph looks like this: 1138 1139 Repair -> Trigger 1140 1141 Assert that there are no repair dependencies. 1142 """ 1143 verify_input = (('base', 0, ()),) 1144 repair_input = (('fixit', True, (), ('base',)),) 1145 strategy = self._make_strategy(verify_input, repair_input) 1146 self._check_common_trigger(strategy, 1147 ['fixit'], 1148 [self.nodes['base']]) 1149 1150 1151 def test_repair_with_root_trigger(self): 1152 """ 1153 Test construction of a repair triggering on the root verifier. 1154 1155 Build a `RepairStrategy` with a single repair action that 1156 triggers on the root verifier. Assert that the trigger graph 1157 looks like this: 1158 1159 Repair -> Root Verifier 1160 1161 Assert that there are no repair dependencies. 1162 """ 1163 root_tag = hosts.RepairStrategy.ROOT_TAG 1164 repair_input = (('fixit', True, (), (root_tag,)),) 1165 strategy = self._make_strategy([], repair_input) 1166 self._check_common_trigger(strategy, 1167 ['fixit'], 1168 [strategy._verify_root]) 1169 1170 1171 def test_three_repairs(self): 1172 """ 1173 Test constructing a strategy with three repair actions. 1174 1175 Build a `RepairStrategy` with a three repair actions sharing a 1176 single trigger. Assert that the trigger graph looks like this: 1177 1178 Repair A -> Trigger 1179 Repair B -> Trigger 1180 Repair C -> Trigger 1181 1182 Assert that there are no repair dependencies. 1183 1184 N.B. This test exists to enforce ordering expectations of 1185 repair nodes. Three nodes are used to make it unlikely that 1186 randomly ordered actions will match expectations. 1187 """ 1188 verify_input = (('base', 0, ()),) 1189 repair_tags = ['a', 'b', 'c'] 1190 repair_input = ( 1191 (tag, True, (), ('base',)) for tag in repair_tags) 1192 strategy = self._make_strategy(verify_input, repair_input) 1193 self._check_common_trigger(strategy, 1194 repair_tags, 1195 [self.nodes['base']]) 1196 1197 1198 def test_repair_dependency(self): 1199 """ 1200 Test construction of a repair with a dependency. 1201 1202 Build a `RepairStrategy` with a single repair action that 1203 depends on a single verifier. Assert that the dependency graph 1204 looks like this: 1205 1206 Repair -> Verifier 1207 1208 Assert that there are no repair triggers. 1209 """ 1210 verify_input = (('base', 0, ()),) 1211 repair_input = (('fixit', True, ('base',), ()),) 1212 strategy = self._make_strategy(verify_input, repair_input) 1213 self.assertEqual(strategy._repair_actions, 1214 [self.nodes['fixit']]) 1215 self.assertEqual(self.nodes['fixit']._trigger_list, []) 1216 self.assertEqual(self.nodes['fixit']._dependency_list, 1217 [self.nodes['base']]) 1218 1219 1220 def _check_repair_failure(self, strategy, silent): 1221 """ 1222 Check the effects of a call to `repair()` that fails. 1223 1224 For the given strategy object, call the `repair()` method; the 1225 call is expected to fail and all repair actions are expected to 1226 trigger. 1227 1228 Assert the following: 1229 * The call raises an exception. 1230 * For each repair action in the strategy, its `repair()` 1231 method is called exactly once. 1232 1233 @param strategy The strategy to be tested. 1234 """ 1235 action_counts = [(a, a.repair_count) 1236 for a in strategy._repair_actions] 1237 with self.assertRaises(Exception) as e: 1238 strategy.repair(self._fake_host, silent) 1239 self._check_silent_records(silent) 1240 for action, count in action_counts: 1241 self.assertEqual(action.repair_count, count + 1) 1242 1243 1244 def _check_repair_success(self, strategy, silent): 1245 """ 1246 Check the effects of a call to `repair()` that succeeds. 1247 1248 For the given strategy object, call the `repair()` method; the 1249 call is expected to succeed without raising an exception and all 1250 repair actions are expected to trigger. 1251 1252 Assert that for each repair action in the strategy, its 1253 `repair()` method is called exactly once. 1254 1255 @param strategy The strategy to be tested. 1256 """ 1257 action_counts = [(a, a.repair_count) 1258 for a in strategy._repair_actions] 1259 strategy.repair(self._fake_host, silent) 1260 self._check_silent_records(silent) 1261 for action, count in action_counts: 1262 self.assertEqual(action.repair_count, count + 1) 1263 1264 1265 def test_repair(self): 1266 """ 1267 Test behavior of the `repair()` method. 1268 1269 Build a `RepairStrategy` with two repair actions each depending 1270 on its own verifier. Set up calls to `repair()` for each of 1271 the following conditions: 1272 * Both repair actions trigger and fail. 1273 * Both repair actions trigger and succeed. 1274 * Both repair actions trigger; the first one fails, but the 1275 second one succeeds. 1276 * Both repair actions trigger; the first one succeeds, but the 1277 second one fails. 1278 1279 Assert the following: 1280 * When both repair actions succeed, `repair()` succeeds. 1281 * When either repair action fails, `repair()` fails. 1282 * After each call to the strategy's `repair()` method, each 1283 repair action triggered exactly once. 1284 """ 1285 verify_input = (('a', 2, ()), ('b', 2, ())) 1286 repair_input = (('afix', True, (), ('a',)), 1287 ('bfix', True, (), ('b',))) 1288 strategy = self._make_strategy(verify_input, repair_input) 1289 1290 for silent in self._generate_silent(): 1291 # call where both 'afix' and 'bfix' fail 1292 self._check_repair_failure(strategy, silent) 1293 # repair counts are now 1 for both verifiers 1294 1295 # call where both 'afix' and 'bfix' succeed 1296 self._check_repair_success(strategy, silent) 1297 # repair counts are now 0 for both verifiers 1298 1299 # call where 'afix' fails and 'bfix' succeeds 1300 for tag in ['a', 'a', 'b']: 1301 self.nodes[tag].unrepair() 1302 self._check_repair_failure(strategy, silent) 1303 # 'a' repair count is 1; 'b' count is 0 1304 1305 # call where 'afix' succeeds and 'bfix' fails 1306 for tag in ['b', 'b']: 1307 self.nodes[tag].unrepair() 1308 self._check_repair_failure(strategy, silent) 1309 # 'a' repair count is 0; 'b' count is 1 1310 1311 for tag in ['a', 'a', 'b']: 1312 self.nodes[tag].unrepair() 1313 # repair counts are now 2 for both verifiers 1314 1315 1316 if __name__ == '__main__': 1317 unittest.main() 1318