# Lint as: python2, python3 # Copyright 2016 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Unit tests for the `repair` module.""" # pylint: disable=missing-docstring from __future__ import absolute_import from __future__ import division from __future__ import print_function import functools import logging import unittest import common from autotest_lib.client.common_lib import hosts from autotest_lib.client.common_lib.hosts import repair from autotest_lib.server import constants from autotest_lib.server.hosts import host_info from six.moves import range class _GoodVerifier(hosts.Verifier): """Verifier is always good""" def verify(self, host): pass class _BadVerifier(hosts.Verifier): """Verifier is always fail""" def verify(self, host): raise Exception('Just not your day') class _SkipVerifier(hosts.Verifier): """Verifier is always not applicable""" def verify(self, host): pass def _is_applicable(self, host): return False class _StubHost(object): """ Stub class to fill in the relevant methods of `Host`. This class provides mocking and stub behaviors for `Host` for use by tests within this module. The class implements only those methods that `Verifier` and `RepairAction` actually use. """ def __init__(self): self._record_sequence = [] fake_board_name = constants.Labels.BOARD_PREFIX + 'fubar' info = host_info.HostInfo(labels=[fake_board_name]) self.host_info_store = host_info.InMemoryHostInfoStore(info) self.hostname = 'unittest_host' def record(self, status_code, subdir, operation, status=''): """ Mock method to capture records written to `status.log`. Each record is remembered in order to be checked for correctness by individual tests later. @param status_code As for `Host.record()`. @param subdir As for `Host.record()`. @param operation As for `Host.record()`. @param status As for `Host.record()`. """ full_record = (status_code, subdir, operation, status) self._record_sequence.append(full_record) def get_log_records(self): """ Return the records logged for this fake host. The returned list of records excludes records where the `operation` parameter is not in `tagset`. @param tagset Only include log records with these tags. """ return self._record_sequence def reset_log_records(self): """Clear our history of log records to allow re-testing.""" self._record_sequence = [] class _StubVerifier(hosts.Verifier): """ Stub implementation of `Verifier` for testing purposes. This is a full implementation of a concrete `Verifier` subclass designed to allow calling unit tests control over whether verify passes or fails. A `_StubVerifier()` will pass whenever the value of `_fail_count` is non-zero. Calls to `try_repair()` (typically made by a `_StubRepairAction()`) will reduce this count, eventually "repairing" the verifier. @property verify_count The number of calls made to the instance's `verify()` method. @property message If verification fails, the exception raised, when converted to a string, will have this value. @property _fail_count The number of repair attempts required before this verifier will succeed. A non-zero value means verification will fail. @property _description The value of the `description` property. """ def __init__(self, tag, deps, fail_count): super(_StubVerifier, self).__init__(tag, deps) self.verify_count = 0 self.message = 'Failing "%s" by request' % tag self._fail_count = fail_count self._description = 'Testing verify() for "%s"' % tag self._log_record_map = { r[0]: r for r in [ ('GOOD', None, self._record_tag, ''), ('FAIL', None, self._record_tag, self.message), ] } def __repr__(self): return '_StubVerifier(%r, %r, %r)' % ( self.tag, self._dependency_list, self._fail_count) def verify(self, host): self.verify_count += 1 if self._fail_count: raise hosts.AutoservVerifyError(self.message) def try_repair(self): """Bring ourselves one step closer to working.""" if self._fail_count: self._fail_count -= 1 def unrepair(self): """Make ourselves more broken.""" self._fail_count += 1 def get_log_record(self, status): """ Return a host log record for this verifier. Calculates the arguments expected to be passed to `Host.record()` by `Verifier._verify_host()` when this verifier runs. The passed in `status` corresponds to the argument of the same name to be passed to `Host.record()`. @param status Status value of the log record. """ return self._log_record_map[status] @property def description(self): return self._description class _StubRepairFailure(Exception): """Exception to be raised by `_StubRepairAction.repair()`.""" pass class _StubRepairAction(hosts.RepairAction): """Stub implementation of `RepairAction` for testing purposes. This is a full implementation of a concrete `RepairAction` subclass designed to allow calling unit tests control over whether repair passes or fails. The behavior of `repair()` depends on the `_success` property of a `_StubRepairAction`. When the property is true, repair will call `try_repair()` for all triggers, and then report success. When the property is false, repair reports failure. @property repair_count The number of calls made to the instance's `repair()` method. @property message If repair fails, the exception raised, when converted to a string, will have this value. @property _success Whether repair will follow its "success" or "failure" paths. @property _description The value of the `description` property. """ def __init__(self, tag, deps, triggers, host_class, success): super(_StubRepairAction, self).__init__(tag, deps, triggers, host_class) self.repair_count = 0 self.message = 'Failed repair for "%s"' % tag self._success = success self._description = 'Testing repair for "%s"' % tag self._log_record_map = { r[0]: r for r in [ ('START', None, self._record_tag, ''), ('FAIL', None, self._record_tag, self.message), ('END FAIL', None, self._record_tag, ''), ('END GOOD', None, self._record_tag, ''), ] } def __repr__(self): return '_StubRepairAction(%r, %r, %r, %r)' % ( self.tag, self._dependency_list, self._trigger_list, self._success) def repair(self, host): self.repair_count += 1 if not self._success: raise _StubRepairFailure(self.message) for v in self._trigger_list: v.try_repair() def get_log_record(self, status): """ Return a host log record for this repair action. Calculates the arguments expected to be passed to `Host.record()` by `RepairAction._repair_host()` when repair runs. The passed in `status` corresponds to the argument of the same name to be passed to `Host.record()`. @param status Status value of the log record. """ return self._log_record_map[status] @property def description(self): return self._description class _DependencyNodeTestCase(unittest.TestCase): """ Abstract base class for `RepairAction` and `Verifier` test cases. This class provides `_make_verifier()` and `_make_repair_action()` methods to create `_StubVerifier` and `_StubRepairAction` instances, respectively, for testing. Constructed verifiers and repair actions are remembered in `self.nodes`, a dictionary indexed by the tag used to construct the object. """ def setUp(self): logging.disable(logging.CRITICAL) self._fake_host = _StubHost() self.nodes = {} def tearDown(self): logging.disable(logging.NOTSET) def _make_verifier(self, count, tag, deps): """ Make a `_StubVerifier` and remember it in `self.nodes`. @param count As for the `_StubVerifer` constructor. @param tag As for the `_StubVerifer` constructor. @param deps As for the `_StubVerifer` constructor. """ verifier = _StubVerifier(tag, deps, count) self.nodes[tag] = verifier return verifier def _make_repair_action(self, success, tag, deps, triggers, host_class='unittest'): """ Make a `_StubRepairAction` and remember it in `self.nodes`. @param success As for the `_StubRepairAction` constructor. @param tag As for the `_StubRepairAction` constructor. @param deps As for the `_StubRepairAction` constructor. @param triggers As for the `_StubRepairAction` constructor. @param host_class As for the `_StubRepairAction` constructor. """ repair_action = _StubRepairAction(tag, deps, triggers, host_class, success) self.nodes[tag] = repair_action return repair_action def _make_expected_failures(self, *verifiers): """ Make a set of `_DependencyFailure` objects from `verifiers`. Return the set of `_DependencyFailure` objects that we would expect to see in the `failures` attribute of an `AutoservVerifyDependencyError` if all of the given verifiers report failure. @param verifiers A list of `_StubVerifier` objects that are expected to fail. @return A set of `_DependencyFailure` objects. """ failures = [repair._DependencyFailure(v.description, v.message, v.tag) for v in verifiers] return set(failures) def _generate_silent(self): """ Iterator to test different settings of the `silent` parameter. This iterator exists to standardize testing assertions that This iterator exists to standardize testing common assertions about the `silent` parameter: * When the parameter is true, no calls are made to the `record()` method on the target host. * When the parameter is false, certain expected calls are made to the `record()` method on the target host. The iterator is meant to be used like this: for silent in self._generate_silent(): # run test case that uses the silent parameter self._check_log_records(silent, ... expected records ... ) The code above will run its test case twice, once with `silent=True` and once with `silent=False`. In between the calls, log records are cleared. @yields A boolean setting for `silent`. """ for silent in [False, True]: yield silent self._fake_host.reset_log_records() def _check_log_records(self, silent, *record_data): """ Assert that log records occurred as expected. Elements of `record_data` should be tuples of the form `(tag, status)`, describing one expected log record. The verifier or repair action for `tag` provides the expected log record based on the status value. The `silent` parameter is the value that was passed to the verifier or repair action that did the logging. When true, it indicates that no records should have been logged. @param record_data List describing the expected record events. @param silent When true, ignore `record_data` and assert that nothing was logged. """ expected_records = [] if not silent: for tag, status in record_data: expected_records.append( self.nodes[tag].get_log_record(status)) actual_records = self._fake_host.get_log_records() self.assertEqual(expected_records, actual_records) class VerifyTests(_DependencyNodeTestCase): """ Unit tests for `Verifier`. The tests in this class test the fundamental behaviors of the `Verifier` class: * Results from the `verify()` method are cached; the method is only called the first time that `_verify_host()` is called. * The `_verify_host()` method makes the expected calls to `Host.record()` for every call to the `verify()` method. * When a dependency fails, the dependent verifier isn't called. * Verifier calls are made in the order required by the DAG. The test cases don't use `RepairStrategy` to build DAG structures, but instead rely on custom-built DAGs. """ def _generate_verify_count(self, verifier): """ Iterator to force a standard sequence with calls to `_reverify()`. This iterator exists to standardize testing two common assertions: * The side effects from calling `_verify_host()` only happen on the first call to the method, except... * Calling `_reverify()` resets a verifier so that the next call to `_verify_host()` will repeat the side effects. The iterator is meant to be used like this: for count in self._generate_verify_cases(verifier): # run a verifier._verify_host() test case self.assertEqual(verifier.verify_count, count) self._check_log_records(silent, ... expected records ... ) The code above will run the `_verify_host()` test case twice, then call `_reverify()` to clear cached results, then re-run the test case two more times. @param verifier The verifier to be tested and reverified. @yields Each iteration yields the number of times `_reverify()` has been called. """ for i in range(1, 3): for _ in range(0, 2): yield i verifier._reverify() self._fake_host.reset_log_records() def test_success(self): """ Test proper handling of a successful verification. Construct and call a simple, single-node verification that will pass. Assert the following: * The `verify()` method is called once. * The expected 'GOOD' record is logged via `Host.record()`. * If `_verify_host()` is called more than once, there are no visible side-effects after the first call. * Calling `_reverify()` clears all cached results. """ for silent in self._generate_silent(): verifier = self._make_verifier(0, 'pass', []) for count in self._generate_verify_count(verifier): verifier._verify_host(self._fake_host, silent) self.assertEqual(verifier.verify_count, count) self._check_log_records(silent, ('pass', 'GOOD')) def test_fail(self): """ Test proper handling of verification failure. Construct and call a simple, single-node verification that will fail. Assert the following: * The failure is reported with the actual exception raised by the verifier. * The `verify()` method is called once. * The expected 'FAIL' record is logged via `Host.record()`. * If `_verify_host()` is called more than once, there are no visible side-effects after the first call. * Calling `_reverify()` clears all cached results. """ for silent in self._generate_silent(): verifier = self._make_verifier(1, 'fail', []) for count in self._generate_verify_count(verifier): with self.assertRaises(hosts.AutoservVerifyError) as e: verifier._verify_host(self._fake_host, silent) self.assertEqual(verifier.verify_count, count) self.assertEqual(verifier.message, str(e.exception)) self._check_log_records(silent, ('fail', 'FAIL')) def test_dependency_success(self): """ Test proper handling of dependencies that succeed. Construct and call a two-node verification with one node dependent on the other, where both nodes will pass. Assert the following: * The `verify()` method for both nodes is called once. * The expected 'GOOD' record is logged via `Host.record()` for both nodes. * If `_verify_host()` is called more than once, there are no visible side-effects after the first call. * Calling `_reverify()` clears all cached results. """ for silent in self._generate_silent(): child = self._make_verifier(0, 'pass', []) parent = self._make_verifier(0, 'parent', [child]) for count in self._generate_verify_count(parent): parent._verify_host(self._fake_host, silent) self.assertEqual(parent.verify_count, count) self.assertEqual(child.verify_count, count) self._check_log_records(silent, ('pass', 'GOOD'), ('parent', 'GOOD')) def test_dependency_fail(self): """ Test proper handling of dependencies that fail. Construct and call a two-node verification with one node dependent on the other, where the dependency will fail. Assert the following: * The verification exception is `AutoservVerifyDependencyError`, and the exception argument is the description of the failed node. * The `verify()` method for the failing node is called once, and for the other node, not at all. * The expected 'FAIL' record is logged via `Host.record()` for the single failed node. * If `_verify_host()` is called more than once, there are no visible side-effects after the first call. * Calling `_reverify()` clears all cached results. """ for silent in self._generate_silent(): child = self._make_verifier(1, 'fail', []) parent = self._make_verifier(0, 'parent', [child]) failures = self._make_expected_failures(child) for count in self._generate_verify_count(parent): expected_exception = hosts.AutoservVerifyDependencyError with self.assertRaises(expected_exception) as e: parent._verify_host(self._fake_host, silent) self.assertEqual(e.exception.failures, failures) self.assertEqual(child.verify_count, count) self.assertEqual(parent.verify_count, 0) self._check_log_records(silent, ('fail', 'FAIL')) def test_two_dependencies_pass(self): """ Test proper handling with two passing dependencies. Construct and call a three-node verification with one node dependent on the other two, where all nodes will pass. Assert the following: * The `verify()` method for all nodes is called once. * The expected 'GOOD' records are logged via `Host.record()` for all three nodes. * If `_verify_host()` is called more than once, there are no visible side-effects after the first call. * Calling `_reverify()` clears all cached results. """ for silent in self._generate_silent(): left = self._make_verifier(0, 'left', []) right = self._make_verifier(0, 'right', []) top = self._make_verifier(0, 'top', [left, right]) for count in self._generate_verify_count(top): top._verify_host(self._fake_host, silent) self.assertEqual(top.verify_count, count) self.assertEqual(left.verify_count, count) self.assertEqual(right.verify_count, count) self._check_log_records(silent, ('left', 'GOOD'), ('right', 'GOOD'), ('top', 'GOOD')) def test_two_dependencies_fail(self): """ Test proper handling with two failing dependencies. Construct and call a three-node verification with one node dependent on the other two, where both dependencies will fail. Assert the following: * The verification exception is `AutoservVerifyDependencyError`, and the exception argument has the descriptions of both the failed nodes. * The `verify()` method for each failing node is called once, and for the parent node not at all. * The expected 'FAIL' records are logged via `Host.record()` for the failing nodes. * If `_verify_host()` is called more than once, there are no visible side-effects after the first call. * Calling `_reverify()` clears all cached results. """ for silent in self._generate_silent(): left = self._make_verifier(1, 'left', []) right = self._make_verifier(1, 'right', []) top = self._make_verifier(0, 'top', [left, right]) failures = self._make_expected_failures(left, right) for count in self._generate_verify_count(top): expected_exception = hosts.AutoservVerifyDependencyError with self.assertRaises(expected_exception) as e: top._verify_host(self._fake_host, silent) self.assertEqual(e.exception.failures, failures) self.assertEqual(top.verify_count, 0) self.assertEqual(left.verify_count, count) self.assertEqual(right.verify_count, count) self._check_log_records(silent, ('left', 'FAIL'), ('right', 'FAIL')) def test_two_dependencies_mixed(self): """ Test proper handling with mixed dependencies. Construct and call a three-node verification with one node dependent on the other two, where one dependency will pass, and one will fail. Assert the following: * The verification exception is `AutoservVerifyDependencyError`, and the exception argument has the descriptions of the single failed node. * The `verify()` method for each dependency is called once, and for the parent node not at all. * The expected 'GOOD' and 'FAIL' records are logged via `Host.record()` for the dependencies. * If `_verify_host()` is called more than once, there are no visible side-effects after the first call. * Calling `_reverify()` clears all cached results. """ for silent in self._generate_silent(): left = self._make_verifier(1, 'left', []) right = self._make_verifier(0, 'right', []) top = self._make_verifier(0, 'top', [left, right]) failures = self._make_expected_failures(left) for count in self._generate_verify_count(top): expected_exception = hosts.AutoservVerifyDependencyError with self.assertRaises(expected_exception) as e: top._verify_host(self._fake_host, silent) self.assertEqual(e.exception.failures, failures) self.assertEqual(top.verify_count, 0) self.assertEqual(left.verify_count, count) self.assertEqual(right.verify_count, count) self._check_log_records(silent, ('left', 'FAIL'), ('right', 'GOOD')) def test_diamond_pass(self): """ Test a "diamond" structure DAG with all nodes passing. Construct and call a "diamond" structure DAG where all nodes will pass: TOP / \ LEFT RIGHT \ / BOTTOM Assert the following: * The `verify()` method for all nodes is called once. * The expected 'GOOD' records are logged via `Host.record()` for all nodes. * If `_verify_host()` is called more than once, there are no visible side-effects after the first call. * Calling `_reverify()` clears all cached results. """ for silent in self._generate_silent(): bottom = self._make_verifier(0, 'bottom', []) left = self._make_verifier(0, 'left', [bottom]) right = self._make_verifier(0, 'right', [bottom]) top = self._make_verifier(0, 'top', [left, right]) for count in self._generate_verify_count(top): top._verify_host(self._fake_host, silent) self.assertEqual(top.verify_count, count) self.assertEqual(left.verify_count, count) self.assertEqual(right.verify_count, count) self.assertEqual(bottom.verify_count, count) self._check_log_records(silent, ('bottom', 'GOOD'), ('left', 'GOOD'), ('right', 'GOOD'), ('top', 'GOOD')) def test_diamond_fail(self): """ Test a "diamond" structure DAG with the bottom node failing. Construct and call a "diamond" structure DAG where the bottom node will fail: TOP / \ LEFT RIGHT \ / BOTTOM Assert the following: * The verification exception is `AutoservVerifyDependencyError`, and the exception argument has the description of the "bottom" node. * The `verify()` method for the "bottom" node is called once, and for the other nodes not at all. * The expected 'FAIL' record is logged via `Host.record()` for the "bottom" node. * If `_verify_host()` is called more than once, there are no visible side-effects after the first call. * Calling `_reverify()` clears all cached results. """ for silent in self._generate_silent(): bottom = self._make_verifier(1, 'bottom', []) left = self._make_verifier(0, 'left', [bottom]) right = self._make_verifier(0, 'right', [bottom]) top = self._make_verifier(0, 'top', [left, right]) failures = self._make_expected_failures(bottom) for count in self._generate_verify_count(top): expected_exception = hosts.AutoservVerifyDependencyError with self.assertRaises(expected_exception) as e: top._verify_host(self._fake_host, silent) self.assertEqual(e.exception.failures, failures) self.assertEqual(top.verify_count, 0) self.assertEqual(left.verify_count, 0) self.assertEqual(right.verify_count, 0) self.assertEqual(bottom.verify_count, count) self._check_log_records(silent, ('bottom', 'FAIL')) class RepairActionTests(_DependencyNodeTestCase): """ Unit tests for `RepairAction`. The tests in this class test the fundamental behaviors of the `RepairAction` class: * Repair doesn't run unless all dependencies pass. * Repair doesn't run unless at least one trigger fails. * Repair reports the expected value of `status` for metrics. * The `_repair_host()` method makes the expected calls to `Host.record()` for every call to the `repair()` method. The test cases don't use `RepairStrategy` to build repair graphs, but instead rely on custom-built structures. """ def test_repair_not_triggered(self): """ Test a repair that doesn't trigger. Construct and call a repair action with a verification trigger that passes. Assert the following: * The `verify()` method for the trigger is called. * The `repair()` method is not called. * The repair action's `status` field is 'untriggered'. * The verifier logs the expected 'GOOD' message with `Host.record()`. * The repair action logs no messages with `Host.record()`. """ for silent in self._generate_silent(): verifier = self._make_verifier(0, 'check', []) repair_action = self._make_repair_action(True, 'unneeded', [], [verifier]) repair_action._repair_host(self._fake_host, silent) self.assertEqual(verifier.verify_count, 1) self.assertEqual(repair_action.repair_count, 0) self.assertEqual(repair_action.status, 'skipped') self._check_log_records(silent, ('check', 'GOOD')) def test_repair_fails(self): """ Test a repair that triggers and fails. Construct and call a repair action with a verification trigger that fails. The repair fails by raising `_StubRepairFailure`. Assert the following: * The repair action fails with the `_StubRepairFailure` raised by `repair()`. * The `verify()` method for the trigger is called once. * The `repair()` method is called once. * The repair action's `status` field is 'failed-action'. * The expected 'START', 'FAIL', and 'END FAIL' messages are logged with `Host.record()` for the failed verifier and the failed repair. """ for silent in self._generate_silent(): verifier = self._make_verifier(1, 'fail', []) repair_action = self._make_repair_action(False, 'nofix', [], [verifier]) with self.assertRaises(_StubRepairFailure) as e: repair_action._repair_host(self._fake_host, silent) self.assertEqual(repair_action.message, str(e.exception)) self.assertEqual(verifier.verify_count, 1) self.assertEqual(repair_action.repair_count, 1) self.assertEqual(repair_action.status, 'repair_failure') self._check_log_records(silent, ('fail', 'FAIL'), ('nofix', 'START'), ('nofix', 'FAIL'), ('nofix', 'END FAIL')) def test_repair_success(self): """ Test a repair that fixes its trigger. Construct and call a repair action that raises no exceptions, using a repair trigger that fails first, then passes after repair. Assert the following: * The `repair()` method is called once. * The trigger's `verify()` method is called twice. * The repair action's `status` field is 'repaired'. * The expected 'START', 'FAIL', 'GOOD', and 'END GOOD' messages are logged with `Host.record()` for the verifier and the repair. """ for silent in self._generate_silent(): verifier = self._make_verifier(1, 'fail', []) repair_action = self._make_repair_action(True, 'fix', [], [verifier]) repair_action._repair_host(self._fake_host, silent) self.assertEqual(repair_action.repair_count, 1) self.assertEqual(verifier.verify_count, 2) self.assertEqual(repair_action.status, 'repaired') self._check_log_records(silent, ('fail', 'FAIL'), ('fix', 'START'), ('fail', 'GOOD'), ('fix', 'END GOOD')) def test_repair_noop(self): """ Test a repair that doesn't fix a failing trigger. Construct and call a repair action with a trigger that fails. The repair action raises no exceptions, and after repair, the trigger still fails. Assert the following: * The `_repair_host()` call fails with `AutoservRepairError`. * The `repair()` method is called once. * The trigger's `verify()` method is called twice. * The repair action's `status` field is 'failed-trigger'. * The expected 'START', 'FAIL', and 'END FAIL' messages are logged with `Host.record()` for the verifier and the repair. """ for silent in self._generate_silent(): verifier = self._make_verifier(2, 'fail', []) repair_action = self._make_repair_action(True, 'nofix', [], [verifier]) with self.assertRaises(hosts.AutoservRepairError) as e: repair_action._repair_host(self._fake_host, silent) self.assertEqual(repair_action.repair_count, 1) self.assertEqual(verifier.verify_count, 2) self.assertEqual(repair_action.status, 'verify_failure') self._check_log_records(silent, ('fail', 'FAIL'), ('nofix', 'START'), ('fail', 'FAIL'), ('nofix', 'END FAIL')) def test_dependency_pass(self): """ Test proper handling of repair dependencies that pass. Construct and call a repair action with a dependency and a trigger. The dependency will pass and the trigger will fail and be repaired. Assert the following: * Repair passes. * The `verify()` method for the dependency is called once. * The `verify()` method for the trigger is called twice. * The `repair()` method is called once. * The repair action's `status` field is 'repaired'. * The expected records are logged via `Host.record()` for the successful dependency, the failed trigger, and the successful repair. """ for silent in self._generate_silent(): dep = self._make_verifier(0, 'dep', []) trigger = self._make_verifier(1, 'trig', []) repair = self._make_repair_action(True, 'fixit', [dep], [trigger]) repair._repair_host(self._fake_host, silent) self.assertEqual(dep.verify_count, 1) self.assertEqual(trigger.verify_count, 2) self.assertEqual(repair.repair_count, 1) self.assertEqual(repair.status, 'repaired') self._check_log_records(silent, ('dep', 'GOOD'), ('trig', 'FAIL'), ('fixit', 'START'), ('trig', 'GOOD'), ('fixit', 'END GOOD')) def test_dependency_fail(self): """ Test proper handling of repair dependencies that fail. Construct and call a repair action with a dependency and a trigger, both of which fail. Assert the following: * Repair fails with `AutoservVerifyDependencyError`, and the exception argument is the description of the failed dependency. * The `verify()` method for the failing dependency is called once. * The trigger and the repair action aren't invoked at all. * The repair action's `status` field is 'blocked'. * The expected 'FAIL' record is logged via `Host.record()` for the single failed dependency. """ for silent in self._generate_silent(): dep = self._make_verifier(1, 'dep', []) trigger = self._make_verifier(1, 'trig', []) repair = self._make_repair_action(True, 'fixit', [dep], [trigger]) expected_exception = hosts.AutoservVerifyDependencyError with self.assertRaises(expected_exception) as e: repair._repair_host(self._fake_host, silent) self.assertEqual(e.exception.failures, self._make_expected_failures(dep)) self.assertEqual(dep.verify_count, 1) self.assertEqual(trigger.verify_count, 0) self.assertEqual(repair.repair_count, 0) self.assertEqual(repair.status, 'blocked') self._check_log_records(silent, ('dep', 'FAIL')) class _RepairStrategyTestCase(_DependencyNodeTestCase): """Shared base class for testing `RepairStrategy` methods.""" def _make_verify_data(self, *input_data): """ Create `verify_data` for the `RepairStrategy` constructor. `RepairStrategy` expects `verify_data` as a list of tuples of the form `(constructor, tag, deps)`. Each item in `input_data` is a tuple of the form `(tag, count, deps)` that creates one entry in the returned list of `verify_data` tuples as follows: * `count` is used to create a constructor function that calls `self._make_verifier()` with that value plus plus the arguments provided by the `RepairStrategy` constructor. * `tag` and `deps` will be passed as-is to the `RepairStrategy` constructor. @param input_data A list of tuples, each representing one tuple in the `verify_data` list. @return A list suitable to be the `verify_data` parameter for the `RepairStrategy` constructor. """ strategy_data = [] for tag, count, deps in input_data: construct = functools.partial(self._make_verifier, count) strategy_data.append((construct, tag, deps)) return strategy_data def _make_repair_data(self, *input_data): """ Create `repair_data` for the `RepairStrategy` constructor. `RepairStrategy` expects `repair_data` as a list of tuples of the form `(constructor, tag, deps, triggers)`. Each item in `input_data` is a tuple of the form `(tag, success, deps, triggers)` that creates one entry in the returned list of `repair_data` tuples as follows: * `success` is used to create a constructor function that calls `self._make_verifier()` with that value plus plus the arguments provided by the `RepairStrategy` constructor. * `tag`, `deps`, and `triggers` will be passed as-is to the `RepairStrategy` constructor. @param input_data A list of tuples, each representing one tuple in the `repair_data` list. @return A list suitable to be the `repair_data` parameter for the `RepairStrategy` constructor. """ strategy_data = [] for tag, success, deps, triggers in input_data: construct = functools.partial(self._make_repair_action, success) strategy_data.append((construct, tag, deps, triggers)) return strategy_data def _make_strategy(self, verify_input, repair_input): """ Create a `RepairStrategy` from the given arguments. @param verify_input As for `input_data` in `_make_verify_data()`. @param repair_input As for `input_data` in `_make_repair_data()`. """ verify_data = self._make_verify_data(*verify_input) repair_data = self._make_repair_data(*repair_input) return hosts.RepairStrategy(verify_data, repair_data, 'unittest') def _check_silent_records(self, silent): """ Check that logging honored the `silent` parameter. Asserts that logging with `Host.record()` occurred (or did not occur) in accordance with the value of `silent`. This method only asserts the presence or absence of log records. Coverage for the contents of the log records is handled in other test cases. @param silent When true, there should be no log records; otherwise there should be records present. """ log_records = self._fake_host.get_log_records() if silent: self.assertEqual(log_records, []) else: self.assertNotEqual(log_records, []) class RepairStrategyVerifyTests(_RepairStrategyTestCase): """ Unit tests for `RepairStrategy.verify()`. These unit tests focus on verifying that the `RepairStrategy` constructor creates the expected DAG structure from given `verify_data`. Functional testing here is mainly confined to asserting that `RepairStrategy.verify()` properly distinguishes success from failure. Testing the behavior of specific DAG structures is left to tests in `VerifyTests`. """ def test_single_node(self): """ Test construction of a single-node verification DAG. Assert that the structure looks like this: Root Node -> Main Node """ verify_data = self._make_verify_data(('main', 0, ())) strategy = hosts.RepairStrategy(verify_data, [], 'unittest') verifier = self.nodes['main'] self.assertEqual( strategy._verify_root._dependency_list, [verifier]) self.assertEqual(verifier._dependency_list, []) def test_single_dependency(self): """ Test construction of a two-node dependency chain. Assert that the structure looks like this: Root Node -> Parent Node -> Child Node """ verify_data = self._make_verify_data( ('child', 0, ()), ('parent', 0, ('child',))) strategy = hosts.RepairStrategy(verify_data, [], 'unittest') parent = self.nodes['parent'] child = self.nodes['child'] self.assertEqual( strategy._verify_root._dependency_list, [parent]) self.assertEqual( parent._dependency_list, [child]) self.assertEqual( child._dependency_list, []) def test_two_nodes_and_dependency(self): """ Test construction of two nodes with a shared dependency. Assert that the structure looks like this: Root Node -> Left Node ---\ \ -> Bottom Node -> Right Node / """ verify_data = self._make_verify_data( ('bottom', 0, ()), ('left', 0, ('bottom',)), ('right', 0, ('bottom',))) strategy = hosts.RepairStrategy(verify_data, [], 'unittest') bottom = self.nodes['bottom'] left = self.nodes['left'] right = self.nodes['right'] self.assertEqual( strategy._verify_root._dependency_list, [left, right]) self.assertEqual(left._dependency_list, [bottom]) self.assertEqual(right._dependency_list, [bottom]) self.assertEqual(bottom._dependency_list, []) def test_three_nodes(self): """ Test construction of three nodes with no dependencies. Assert that the structure looks like this: -> Node One / Root Node -> Node Two \ -> Node Three N.B. This test exists to enforce ordering expectations of root-level DAG nodes. Three nodes are used to make it unlikely that randomly ordered roots will match expectations. """ verify_data = self._make_verify_data( ('one', 0, ()), ('two', 0, ()), ('three', 0, ())) strategy = hosts.RepairStrategy(verify_data, [], 'unittest') one = self.nodes['one'] two = self.nodes['two'] three = self.nodes['three'] self.assertEqual( strategy._verify_root._dependency_list, [one, two, three]) self.assertEqual(one._dependency_list, []) self.assertEqual(two._dependency_list, []) self.assertEqual(three._dependency_list, []) def test_verify(self): """ Test behavior of the `verify()` method. Build a `RepairStrategy` with a single verifier. Assert the following: * If the verifier passes, `verify()` passes. * If the verifier fails, `verify()` fails. * The verifier is reinvoked with every call to `verify()`; cached results are not re-used. """ verify_data = self._make_verify_data(('tester', 0, ())) strategy = hosts.RepairStrategy(verify_data, [], 'unittest') verifier = self.nodes['tester'] count = 0 for silent in self._generate_silent(): for i in range(0, 2): for j in range(0, 2): strategy.verify(self._fake_host, silent) self._check_silent_records(silent) count += 1 self.assertEqual(verifier.verify_count, count) verifier.unrepair() for j in range(0, 2): with self.assertRaises(Exception) as e: strategy.verify(self._fake_host, silent) self._check_silent_records(silent) count += 1 self.assertEqual(verifier.verify_count, count) verifier.try_repair() class RepairStrategyRepairTests(_RepairStrategyTestCase): """ Unit tests for `RepairStrategy.repair()`. These unit tests focus on verifying that the `RepairStrategy` constructor creates the expected repair list from given `repair_data`. Functional testing here is confined to asserting that `RepairStrategy.repair()` properly distinguishes success from failure. Testing the behavior of specific repair structures is left to tests in `RepairActionTests`. """ def _check_common_trigger(self, strategy, repair_tags, triggers): self.assertEqual(strategy._repair_actions, [self.nodes[tag] for tag in repair_tags]) for tag in repair_tags: self.assertEqual(self.nodes[tag]._trigger_list, triggers) self.assertEqual(self.nodes[tag]._dependency_list, []) def test_single_repair_with_trigger(self): """ Test constructing a strategy with a single repair trigger. Build a `RepairStrategy` with a single repair action and a single trigger. Assert that the trigger graph looks like this: Repair -> Trigger Assert that there are no repair dependencies. """ verify_input = (('base', 0, ()),) repair_input = (('fixit', True, (), ('base',)),) strategy = self._make_strategy(verify_input, repair_input) self._check_common_trigger(strategy, ['fixit'], [self.nodes['base']]) def test_repair_with_root_trigger(self): """ Test construction of a repair triggering on the root verifier. Build a `RepairStrategy` with a single repair action that triggers on the root verifier. Assert that the trigger graph looks like this: Repair -> Root Verifier Assert that there are no repair dependencies. """ root_tag = hosts.RepairStrategy.ROOT_TAG repair_input = (('fixit', True, (), (root_tag,)),) strategy = self._make_strategy([], repair_input) self._check_common_trigger(strategy, ['fixit'], [strategy._verify_root]) def test_three_repairs(self): """ Test constructing a strategy with three repair actions. Build a `RepairStrategy` with a three repair actions sharing a single trigger. Assert that the trigger graph looks like this: Repair A -> Trigger Repair B -> Trigger Repair C -> Trigger Assert that there are no repair dependencies. N.B. This test exists to enforce ordering expectations of repair nodes. Three nodes are used to make it unlikely that randomly ordered actions will match expectations. """ verify_input = (('base', 0, ()),) repair_tags = ['a', 'b', 'c'] repair_input = ( (tag, True, (), ('base',)) for tag in repair_tags) strategy = self._make_strategy(verify_input, repair_input) self._check_common_trigger(strategy, repair_tags, [self.nodes['base']]) def test_repair_dependency(self): """ Test construction of a repair with a dependency. Build a `RepairStrategy` with a single repair action that depends on a single verifier. Assert that the dependency graph looks like this: Repair -> Verifier Assert that there are no repair triggers. """ verify_input = (('base', 0, ()),) repair_input = (('fixit', True, ('base',), ()),) strategy = self._make_strategy(verify_input, repair_input) self.assertEqual(strategy._repair_actions, [self.nodes['fixit']]) self.assertEqual(self.nodes['fixit']._trigger_list, []) self.assertEqual(self.nodes['fixit']._dependency_list, [self.nodes['base']]) def _check_repair_failure(self, strategy, silent): """ Check the effects of a call to `repair()` that fails. For the given strategy object, call the `repair()` method; the call is expected to fail and all repair actions are expected to trigger. Assert the following: * The call raises an exception. * For each repair action in the strategy, its `repair()` method is called exactly once. @param strategy The strategy to be tested. """ action_counts = [(a, a.repair_count) for a in strategy._repair_actions] with self.assertRaises(Exception) as e: strategy.repair(self._fake_host, silent) self._check_silent_records(silent) for action, count in action_counts: self.assertEqual(action.repair_count, count + 1) def _check_repair_success(self, strategy, silent): """ Check the effects of a call to `repair()` that succeeds. For the given strategy object, call the `repair()` method; the call is expected to succeed without raising an exception and all repair actions are expected to trigger. Assert that for each repair action in the strategy, its `repair()` method is called exactly once. @param strategy The strategy to be tested. """ action_counts = [(a, a.repair_count) for a in strategy._repair_actions] strategy.repair(self._fake_host, silent) self._check_silent_records(silent) for action, count in action_counts: self.assertEqual(action.repair_count, count + 1) def test_repair(self): """ Test behavior of the `repair()` method. Build a `RepairStrategy` with two repair actions each depending on its own verifier. Set up calls to `repair()` for each of the following conditions: * Both repair actions trigger and fail. * Both repair actions trigger and succeed. * Both repair actions trigger; the first one fails, but the second one succeeds. * Both repair actions trigger; the first one succeeds, but the second one fails. Assert the following: * When both repair actions succeed, `repair()` succeeds. * When either repair action fails, `repair()` fails. * After each call to the strategy's `repair()` method, each repair action triggered exactly once. """ verify_input = (('a', 2, ()), ('b', 2, ())) repair_input = (('afix', True, (), ('a',)), ('bfix', True, (), ('b',))) strategy = self._make_strategy(verify_input, repair_input) for silent in self._generate_silent(): # call where both 'afix' and 'bfix' fail self._check_repair_failure(strategy, silent) # repair counts are now 1 for both verifiers # call where both 'afix' and 'bfix' succeed self._check_repair_success(strategy, silent) # repair counts are now 0 for both verifiers # call where 'afix' fails and 'bfix' succeeds for tag in ['a', 'a', 'b']: self.nodes[tag].unrepair() self._check_repair_failure(strategy, silent) # 'a' repair count is 1; 'b' count is 0 # call where 'afix' succeeds and 'bfix' fails for tag in ['b', 'b']: self.nodes[tag].unrepair() self._check_repair_failure(strategy, silent) # 'a' repair count is 0; 'b' count is 1 for tag in ['a', 'a', 'b']: self.nodes[tag].unrepair() # repair counts are now 2 for both verifiers class VerifierResultTestCases(_DependencyNodeTestCase): """ Test to check that we can find correct verifier and get the result of it """ def test_find_verifier_by_tag(self): """Test to find correct verifier by tag""" verify_data = [ (_GoodVerifier, 'v1', []), (_GoodVerifier, 'v2', ['v1']), (_BadVerifier, 'v3', []), (_BadVerifier, 'v4', []), (_SkipVerifier, 'v5', ['v4']), (_SkipVerifier, 'v6', ['v2', 'v3']) ] strategy = hosts.RepairStrategy(verify_data, (), 'unittest') for v in range(1,6): tag = 'v%s' % v verifier = strategy._verify_root._get_node_by_tag(tag) self.assertIsNotNone(verifier) self.assertEqual(tag, verifier.tag) verifier = strategy._verify_root._get_node_by_tag('v0') self.assertEqual(repair.VERIFY_NOT_RUN, verifier) def test_run_verifier_and_get_results(self): """Check the result of verifiers""" verify_data = [ (_GoodVerifier, 'v1', []), (_BadVerifier, 'v2', []), (_SkipVerifier, 'v3', []), ] strategy = hosts.RepairStrategy(verify_data, (), 'unittest') try: strategy.verify(self._fake_host, silent=True) except Exception as e: pass self.assertEqual(repair.VERIFY_NOT_RUN, strategy.verifier_is_good('v0')) self.assertEqual(repair.VERIFY_SUCCESS, strategy.verifier_is_good('v1')) self.assertEqual(repair.VERIFY_FAILED, strategy.verifier_is_good('v2')) self.assertEqual(repair.VERIFY_NOT_RUN, strategy.verifier_is_good('v3')) self.assertEqual(repair.VERIFY_NOT_RUN, strategy.verifier_is_good('v4')) def test_run_verifier_with_dependencies(self): """Check the result if dependency fail or not applicable.""" verify_data = [ (_GoodVerifier, 'v1', []), (_BadVerifier, 'v2', []), (_SkipVerifier, 'v3', []), (_GoodVerifier, 'v4', ['v2']), (_GoodVerifier, 'v5', ['v3']), ] strategy = hosts.RepairStrategy(verify_data, (), 'unittest') try: strategy.verify(self._fake_host, silent=True) except Exception as e: pass self.assertEqual(repair.VERIFY_NOT_RUN, strategy.verifier_is_good('v0')) self.assertEqual(repair.VERIFY_SUCCESS, strategy.verifier_is_good('v1')) self.assertEqual(repair.VERIFY_FAILED, strategy.verifier_is_good('v2')) self.assertEqual(repair.VERIFY_NOT_RUN, strategy.verifier_is_good('v3')) # if dependencies fail then the verifier mark as not run self.assertEqual(repair.VERIFY_NOT_RUN, strategy.verifier_is_good('v4')) # if dependencies not applicable then run only verifier self.assertEqual(repair.VERIFY_SUCCESS, strategy.verifier_is_good('v5')) self.assertEqual(repair.VERIFY_NOT_RUN, strategy.verifier_is_good('v6')) if __name__ == '__main__': unittest.main()