3b7dec170286fbebd79d35d46281cf176be6df8d
[tinc] / check.py
1 """Simple assertions which print the expected and received values on failure."""
2
3 import os.path
4 import typing as T
5 from pathlib import Path
6
7 from .log import log
8
9 Val = T.TypeVar("Val")
10 Num = T.TypeVar("Num", int, float)
11
12
13 def blank(value: T.AnyStr) -> None:
14     """Check that value is an empty or blank string."""
15     if not isinstance(value, str) or value.strip():
16         raise ValueError(f'expected "{value!r}" to be a blank string')
17
18
19 def false(value: T.Any) -> None:
20     """Check that value is falsy."""
21     if value:
22         raise ValueError(f'expected "{value}" to be falsy')
23
24
25 def true(value: T.Any) -> None:
26     """Check that value is truthy."""
27     if not value:
28         raise ValueError(f'expected "{value}" to be truthy', value)
29
30
31 def port(value: int) -> None:
32     """Check that value resembles a port."""
33     if not isinstance(value, int) or value < 1 or value > 65535:
34         raise ValueError(f'expected "{value}" to be be a port')
35
36
37 def equals(expected: Val, actual: Val) -> None:
38     """Check that the two values are equal."""
39     if expected != actual:
40         raise ValueError(f'expected "{expected}", got "{actual}"')
41
42
43 def has_prefix(text: T.AnyStr, prefix: T.AnyStr) -> None:
44     """Check that text has prefix."""
45     if not text.startswith(prefix):
46         raise ValueError(f"expected {text!r} to start with {prefix!r}")
47
48
49 def greater(value: Num, than: Num) -> None:
50     """Check that value is greater than the other value."""
51     if value <= than:
52         raise ValueError(f"value {value} must be greater than {than}")
53
54
55 def in_range(value: Num, gte: Num, lte: Num) -> None:
56     """Check that value lies in the range [min, max]."""
57     if not gte >= value >= lte:
58         raise ValueError(f"value {value} must be between {gte} and {lte}")
59
60
61 def lines(text: T.AnyStr, num: int) -> None:
62     """Check that text splits into `num` lines."""
63     rows = text.splitlines()
64     if len(rows) != num:
65         raise ValueError(f"expected {num} lines, got {len(rows)}: {rows}")
66
67
68 def is_in(needle: Val, *haystacks: T.Container[Val]) -> None:
69     """Check that at least one haystack includes needle."""
70     for haystack in haystacks:
71         if needle in haystack:
72             return
73     raise ValueError(f'expected any of "{haystacks}" to include "{needle}"')
74
75
76 def not_in(needle: Val, *haystacks: T.Container[Val]) -> None:
77     """Check that all haystacks do not include needle."""
78     for haystack in haystacks:
79         if needle in haystack:
80             raise ValueError(f'expected all "{haystacks}" NOT to include "{needle}"')
81
82
83 def nodes(node, want_nodes: int) -> None:
84     """Check that node can reach exactly N nodes (including itself)."""
85     log.debug("want %d reachable nodes from tinc %s", want_nodes, node)
86     stdout, _ = node.cmd("dump", "reachable", "nodes")
87     equals(want_nodes, len(stdout.splitlines()))
88
89
90 def files_eq(path0: str, path1: str) -> None:
91     """Compare file contents, ignoring whitespace at both ends."""
92     log.debug("comparing files %s and %s", path0, path1)
93
94     def read(path: str) -> str:
95         log.debug("reading file %s", path)
96         with open(path, "r", encoding="utf-8") as f:
97             return f.read().strip()
98
99     content0 = read(path0)
100     content1 = read(path1)
101
102     if content0 != content1:
103         raise ValueError(f"expected files {path0} and {path1} to match")
104
105
106 def file_exists(path: T.Union[str, Path]) -> None:
107     """Check that file or directory exists."""
108     if not os.path.exists(path):
109         raise ValueError("expected path '{path}' to exist")