]>
Commit | Line | Data |
---|---|---|
acddc0ed | 1 | # SPDX-License-Identifier: GPL-2.0-or-later |
cbbf41cb CF |
2 | # |
3 | # Test helpers for FRR | |
4 | # | |
5 | # Copyright (C) 2017 by David Lamparter & Christian Franke, | |
6 | # Open Source Routing / NetDEF Inc. | |
7 | # | |
447a8fe9 | 8 | # This file is part of FRRouting (FRR) |
cbbf41cb | 9 | # |
cbbf41cb CF |
10 | |
11 | import subprocess | |
12 | import sys | |
13 | import re | |
14 | import inspect | |
15 | import os | |
1ea8289e | 16 | import difflib |
cbbf41cb CF |
17 | |
18 | import frrsix | |
19 | ||
20 | # | |
21 | # These are the gritty internals of the TestMultiOut implementation. | |
22 | # See below for the definition of actual TestMultiOut tests. | |
23 | # | |
24 | ||
43dac2ba DL |
25 | srcbase = os.path.abspath(inspect.getsourcefile(frrsix)) |
26 | for i in range(0, 3): | |
27 | srcbase = os.path.dirname(srcbase) | |
701a0192 | 28 | |
29 | ||
43dac2ba DL |
30 | def binpath(srcpath): |
31 | return os.path.relpath(os.path.abspath(srcpath), srcbase) | |
32 | ||
701a0192 | 33 | |
cbbf41cb CF |
34 | class MultiTestFailure(Exception): |
35 | pass | |
36 | ||
701a0192 | 37 | |
cbbf41cb CF |
38 | class MetaTestMultiOut(type): |
39 | def __getattr__(cls, name): | |
701a0192 | 40 | if name.startswith("_"): |
cbbf41cb CF |
41 | raise AttributeError |
42 | ||
701a0192 | 43 | internal_name = "_{}".format(name) |
cbbf41cb CF |
44 | if internal_name not in dir(cls): |
45 | raise AttributeError | |
46 | ||
47 | def registrar(*args, **kwargs): | |
701a0192 | 48 | cls._add_test(getattr(cls, internal_name), *args, **kwargs) |
49 | ||
cbbf41cb CF |
50 | return registrar |
51 | ||
701a0192 | 52 | |
cbbf41cb CF |
53 | @frrsix.add_metaclass(MetaTestMultiOut) |
54 | class _TestMultiOut(object): | |
55 | def _run_tests(self): | |
701a0192 | 56 | if "tests_run" in dir(self.__class__) and self.tests_run: |
cbbf41cb CF |
57 | return |
58 | self.__class__.tests_run = True | |
59 | basedir = os.path.dirname(inspect.getsourcefile(type(self))) | |
60 | program = os.path.join(basedir, self.program) | |
43dac2ba | 61 | proc = subprocess.Popen([binpath(program)], stdout=subprocess.PIPE) |
701a0192 | 62 | self.output, _ = proc.communicate("") |
cbbf41cb CF |
63 | self.exitcode = proc.wait() |
64 | ||
65 | self.__class__.testresults = {} | |
66 | for test in self.tests: | |
67 | try: | |
68 | test(self) | |
69 | except MultiTestFailure: | |
70 | self.testresults[test] = sys.exc_info() | |
71 | else: | |
72 | self.testresults[test] = None | |
73 | ||
74 | def _exit_cleanly(self): | |
75 | if self.exitcode != 0: | |
76 | raise MultiTestFailure("Program did not terminate with exit code 0") | |
77 | ||
78 | @classmethod | |
79 | def _add_test(cls, method, *args, **kwargs): | |
701a0192 | 80 | if "tests" not in dir(cls): |
81 | setattr(cls, "tests", []) | |
9d83fa42 CF |
82 | if method is not cls._exit_cleanly: |
83 | cls._add_test(cls._exit_cleanly) | |
cbbf41cb CF |
84 | |
85 | def matchfunction(self): | |
86 | method(self, *args, **kwargs) | |
701a0192 | 87 | |
cbbf41cb CF |
88 | cls.tests.append(matchfunction) |
89 | ||
90 | def testfunction(self): | |
91 | self._run_tests() | |
92 | result = self.testresults[matchfunction] | |
93 | if result is not None: | |
94 | frrsix.reraise(*result) | |
95 | ||
701a0192 | 96 | testname = re.sub(r"[^A-Za-z0-9]", "_", "%r%r" % (args, kwargs)) |
97 | testname = re.sub(r"__*", "_", testname) | |
98 | testname = testname.strip("_") | |
cbbf41cb | 99 | if not testname: |
701a0192 | 100 | testname = method.__name__.strip("_") |
cbbf41cb CF |
101 | if "test_%s" % testname in dir(cls): |
102 | index = 2 | |
701a0192 | 103 | while "test_%s_%d" % (testname, index) in dir(cls): |
cbbf41cb CF |
104 | index += 1 |
105 | testname = "%s_%d" % (testname, index) | |
701a0192 | 106 | setattr(cls, "test_%s" % testname, testfunction) |
107 | ||
cbbf41cb CF |
108 | |
109 | # | |
110 | # This class houses the actual TestMultiOut tests types. | |
111 | # If you want to add a new test type, you probably do it here. | |
112 | # | |
113 | # Say you want to add a test type called foobarlicious. Then define | |
114 | # a function _foobarlicious here that takes self and the test arguments | |
115 | # when called. That function should check the output in self.output | |
116 | # to see whether it matches the expectation of foobarlicious with the | |
117 | # given arguments and should then adjust self.output according to how | |
118 | # much output it consumed. | |
119 | # If the output doesn't meet the expectations, MultiTestFailure can be | |
120 | # raised, however that should only be done after self.output has been | |
121 | # modified according to consumed content. | |
122 | # | |
123 | ||
701a0192 | 124 | re_okfail = re.compile(r"(?:[3[12]m|^)?(?P<ret>OK|failed)".encode("utf8"), re.MULTILINE) |
125 | ||
126 | ||
cbbf41cb CF |
127 | class TestMultiOut(_TestMultiOut): |
128 | def _onesimple(self, line): | |
129 | if type(line) is str: | |
701a0192 | 130 | line = line.encode("utf8") |
cbbf41cb CF |
131 | idx = self.output.find(line) |
132 | if idx != -1: | |
701a0192 | 133 | self.output = self.output[idx + len(line) :] |
cbbf41cb CF |
134 | else: |
135 | raise MultiTestFailure("%r could not be found" % line) | |
136 | ||
137 | def _okfail(self, line, okfail=re_okfail): | |
138 | self._onesimple(line) | |
139 | ||
140 | m = okfail.search(self.output) | |
141 | if m is None: | |
701a0192 | 142 | raise MultiTestFailure("OK/fail not found") |
143 | self.output = self.output[m.end() :] | |
144 | ||
145 | if m.group("ret") != "OK".encode("utf8"): | |
146 | raise MultiTestFailure("Test output indicates failure") | |
cbbf41cb | 147 | |
cbbf41cb CF |
148 | |
149 | # | |
150 | # This class implements a test comparing the output of a program against | |
151 | # an existing reference output | |
152 | # | |
153 | ||
701a0192 | 154 | |
cbbf41cb | 155 | class TestRefMismatch(Exception): |
1ea8289e | 156 | def __init__(self, _test, outtext, reftext): |
6fc170c5 G |
157 | self.outtext = outtext |
158 | self.reftext = reftext | |
1ea8289e CF |
159 | |
160 | def __str__(self): | |
701a0192 | 161 | rv = "Expected output and actual output differ:\n" |
162 | rv += "\n".join( | |
163 | difflib.unified_diff( | |
164 | self.reftext.splitlines(), | |
165 | self.outtext.splitlines(), | |
166 | "outtext", | |
167 | "reftext", | |
168 | lineterm="", | |
169 | ) | |
170 | ) | |
1ea8289e CF |
171 | return rv |
172 | ||
701a0192 | 173 | |
cbbf41cb CF |
174 | class TestExitNonzero(Exception): |
175 | pass | |
176 | ||
701a0192 | 177 | |
cbbf41cb CF |
178 | class TestRefOut(object): |
179 | def test_refout(self): | |
180 | basedir = os.path.dirname(inspect.getsourcefile(type(self))) | |
181 | program = os.path.join(basedir, self.program) | |
182 | ||
701a0192 | 183 | if getattr(self, "built_refin", False): |
184 | refin = binpath(program) + ".in" | |
b37ccace | 185 | else: |
701a0192 | 186 | refin = program + ".in" |
187 | if getattr(self, "built_refout", False): | |
188 | refout = binpath(program) + ".refout" | |
b37ccace | 189 | else: |
701a0192 | 190 | refout = program + ".refout" |
cbbf41cb | 191 | |
701a0192 | 192 | intext = "" |
cbbf41cb | 193 | if os.path.exists(refin): |
701a0192 | 194 | with open(refin, "rb") as f: |
cbbf41cb | 195 | intext = f.read() |
701a0192 | 196 | with open(refout, "rb") as f: |
cbbf41cb CF |
197 | reftext = f.read() |
198 | ||
701a0192 | 199 | proc = subprocess.Popen( |
200 | [binpath(program)], stdin=subprocess.PIPE, stdout=subprocess.PIPE | |
201 | ) | |
202 | outtext, _ = proc.communicate(intext) | |
6fc170c5 G |
203 | |
204 | # Get rid of newline problems (Windows vs Unix Style) | |
205 | outtext_str = outtext.decode("utf8").replace("\r\n", "\n").replace("\r", "\n") | |
206 | reftext_str = reftext.decode("utf8").replace("\r\n", "\n").replace("\r", "\n") | |
207 | ||
208 | if outtext_str != reftext_str: | |
209 | raise TestRefMismatch(self, outtext_str, reftext_str) | |
cbbf41cb CF |
210 | if proc.wait() != 0: |
211 | raise TestExitNonzero(self) |