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