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