File size: 5,389 Bytes
79cf149
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
"""Shared harness for the verification scripts.

Three suites -- reporting, ingestion and Drive delivery -- each accumulated
pass/fail, padded a listing and printed a banner in its own way, one of them
through module-level counters. The mechanics were identical, so they live here
and each suite is left holding only its own assertions.

A check records rather than raises. A verification run should report every
finding in one pass: stopping at the first failure hides how much else broke,
which is exactly what you need to know when deciding whether a change is
salvageable.
"""

from dataclasses import dataclass, field
from typing import Callable, List, Optional, Tuple, Type

RULE: str = "=" * 72
DETAIL_LIMIT: int = 70


class Failure(Exception):
    """Raised inside a check to report that it did not hold."""


def require(condition: bool, message: str) -> None:
    """Raises `Failure` with `message` unless `condition` holds."""
    if not condition:
        raise Failure(message)


@dataclass
class Check:
    """One recorded check, or a section heading when `passed` is None.

    Headings share the list so that rendering keeps them interleaved with the
    checks they introduce, rather than needing a second structure.
    """

    name: str
    passed: Optional[bool]
    detail: str = ""

    @property
    def is_heading(self) -> bool:
        """Whether this entry is a section heading rather than a check."""
        return self.passed is None


@dataclass
class Report:
    """Accumulates checks and renders them as one listing."""

    entries: List[Check] = field(default_factory=list)

    # -- recording ---------------------------------------------------------

    def section(self, title: str) -> None:
        """Opens a titled group in the listing."""
        self.entries.append(Check(title, None))

    def check(self, name: str, condition: bool, detail: str = "") -> bool:
        """Records one check and returns its outcome.

        The outcome is returned so a caller can skip dependent checks that
        would only produce noise once their precondition has failed.
        """
        held = bool(condition)
        self.entries.append(Check(name, held, detail))
        return held

    def raises(self, name: str, kind: Type[BaseException],
               call: Callable[[], object], detail: str = "") -> bool:
        """Records that `call` raises `kind`, and nothing else.

        A wrong exception type is a distinct failure from no exception at all,
        and both are reported as what actually happened.
        """
        try:
            call()
        except kind as exc:
            return self.check(name, True, detail or str(exc)[:DETAIL_LIMIT])
        except Exception as exc:  # noqa: BLE001 - the wrong type is the finding
            return self.check(name, False,
                              "raised %s instead: %s" % (type(exc).__name__, exc))
        return self.check(name, False, "did not raise %s" % kind.__name__)

    def run(self, name: str, call: Callable[[], str]) -> bool:
        """Records a coarse check that returns its own detail, or raises.

        For suites whose checks assert internally with `require` and describe
        what they observed on the way through.
        """
        try:
            return self.check(name, True, call())
        except Failure as exc:
            return self.check(name, False, str(exc))
        except Exception as exc:  # noqa: BLE001 - report and keep going
            return self.check(name, False, "%s: %s" % (type(exc).__name__, exc))

    def extend(self, other: "Report") -> None:
        """Absorbs another report's entries, preserving order."""
        self.entries.extend(other.entries)

    # -- reporting ---------------------------------------------------------

    @property
    def results(self) -> List[Check]:
        """Every recorded check, headings excluded."""
        return [e for e in self.entries if not e.is_heading]

    @property
    def failures(self) -> List[Check]:
        """The checks that did not hold."""
        return [e for e in self.results if not e.passed]

    def render(self) -> str:
        """Returns the listing, one line per check, headings interleaved."""
        width = max([len(e.name) for e in self.results] + [4])
        lines = []
        for entry in self.entries:
            if entry.is_heading:
                lines.extend(["", entry.name])
            else:
                lines.append("  [%s] %-*s  %s" % (
                    "PASS" if entry.passed else "FAIL", width, entry.name, entry.detail))
        results, failed = self.results, self.failures
        lines.extend(["", "  %d of %d checks passed."
                      % (len(results) - len(failed), len(results))])
        return "\n".join(lines)

    def finish(self) -> int:
        """Prints the closing banner and returns a process exit code."""
        print("\n" + RULE)
        if not self.failures:
            print("All %d checks passed." % len(self.results))
            return 0
        print("FAILED: %d of %d checks did not hold."
              % (len(self.failures), len(self.results)))
        for entry in self.failures:
            print("  - %s: %s" % (entry.name, entry.detail))
        return 1


def banner(title: str) -> None:
    """Prints a suite's opening banner."""
    print(RULE)
    print(title)
    print(RULE)