Add tests for some device & address variables
[tinc] / test / integration / testlib / util.py
1 """Miscellaneous utility functions."""
2
3 import os
4 import sys
5 import subprocess as subp
6 import random
7 import string
8 import socket
9 import typing as T
10 import tempfile
11 from pathlib import Path
12
13 from . import check
14 from .log import log
15 from .const import EXIT_SKIP
16
17 _ALPHA_NUMERIC = string.ascii_lowercase + string.digits
18
19
20 def random_port() -> int:
21     """Return an unused TCP port in the unprivileged range.
22     Note that this function releases the port before returning, and it can be
23     overtaken by something else before you use it.
24     """
25     while True:
26         port = random.randint(1024, 65535)
27         try:
28             with socket.socket() as sock:
29                 sock.bind(("0.0.0.0", port))
30                 sock.listen()
31             return port
32         except OSError as ex:
33             log.debug("could not bind to random port %d", port, exc_info=ex)
34
35
36 def temp_file(content: str) -> str:
37     """Create a temporary file and write text content into it."""
38     file = tempfile.mktemp()
39     with open(file, "w", encoding="utf-8") as f:
40         f.write(content)
41     return file
42
43
44 def remove_file(path: T.Union[str, Path]) -> bool:
45     """Try to remove file without failing if it does not exist."""
46     try:
47         os.remove(path)
48         return True
49     except FileNotFoundError:
50         return False
51
52
53 def random_string(k: int) -> str:
54     """Generate a random alphanumeric string of length k."""
55     return "".join(random.choices(_ALPHA_NUMERIC, k=k))
56
57
58 def find_line(filename: str, prefix: str) -> str:
59     """Find a line with the prefix in a text file.
60     Check that only one line matches.
61     """
62     with open(filename, "r", encoding="utf-8") as f:
63         keylines = [line for line in f.readlines() if line.startswith(prefix)]
64     check.equals(1, len(keylines))
65     return keylines[0].rstrip()
66
67
68 def require_root() -> None:
69     """Check that test is running with root privileges.
70     Exit with code 77 otherwise.
71     """
72     euid = os.geteuid()
73     if euid:
74         log.info("this test requires root (but running under UID %d)", euid)
75         sys.exit(EXIT_SKIP)
76
77
78 def require_command(*args: str) -> None:
79     """Check that command args runs with exit code 0.
80     Exit with code 77 otherwise.
81     """
82     try:
83         if subp.run(args, check=False).returncode == 0:
84             return
85     except FileNotFoundError:
86         pass
87     log.info('this test requires command "%s" to work', " ".join(args))
88     sys.exit(EXIT_SKIP)
89
90
91 def require_path(path: str) -> None:
92     """Check that path exists in your file system.
93     Exit with code 77 otherwise.
94     """
95     if not os.path.exists(path):
96         log.warning("this test requires path %s to be present", path)
97         sys.exit(EXIT_SKIP)
98
99
100 # Thin wrappers around `with open(...) as f: f.do_something()`
101 # Don't do much, besides saving quite a bit of space because of how frequently they're needed.
102
103
104 def read_text(path: str) -> str:
105     """Return the text contents of a file."""
106     with open(path, encoding="utf-8") as f:
107         return f.read()
108
109
110 def write_text(path: str, text: str) -> str:
111     """Write text to a file, replacing its content. Return the text added."""
112     with open(path, "w", encoding="utf-8") as f:
113         f.write(text)
114     return text
115
116
117 def read_lines(path: str) -> T.List[str]:
118     """Read file as a list of lines."""
119     with open(path, encoding="utf-8") as f:
120         return f.read().splitlines()
121
122
123 def write_lines(path: str, lines: T.List[str]) -> T.List[str]:
124     """Write text lines to a file, replacing it content. Return the line added."""
125     with open(path, "w", encoding="utf-8") as f:
126         f.write(os.linesep.join(lines))
127         f.write(os.linesep)
128     return lines
129
130
131 def append_line(path: str, line: str) -> str:
132     """Append a line to the end of the file. Return the line added."""
133     line = f"{os.linesep}{line}{os.linesep}"
134     with open(path, "a", encoding="utf-8") as f:
135         f.write(line)
136     return line