]> git.proxmox.com Git - mirror_frr.git/blame - tests/topotests/conftest.py
Merge pull request #13506 from anlancs/fix/bfdd-vrf-check
[mirror_frr.git] / tests / topotests / conftest.py
CommitLineData
249ac6f0 1# -*- coding: utf-8 eval: (blacken-mode 1) -*-
1fca63c1
RZ
2"""
3Topotest conftest.py file.
4"""
1623dc4c 5# pylint: disable=consider-using-f-string
1fca63c1 6
e58133a7 7import glob
0def198c 8import logging
3f950192 9import os
e58133a7 10import re
60e03778 11import resource
1726edc3 12import subprocess
49581587
CH
13import sys
14import time
3f950192 15
49581587 16import lib.fixtures
7592b2cc 17import pytest
1a68b138 18from lib.micronet_compat import Mininet
49581587 19from lib.topogen import diagnose_env, get_topogen
0def198c 20from lib.topolog import get_test_logdir, logger
49581587 21from lib.topotest import json_cmp_result
7592b2cc 22from munet import cli
60e03778
CH
23from munet.base import Commander, proc_error
24from munet.cleanup import cleanup_current, cleanup_previous
1a68b138 25from munet.config import ConfigOptionsProxy
249ac6f0 26from munet.testing.util import pause_test
7592b2cc 27
249ac6f0 28from lib import topolog, topotest
787e7624 29
0def198c
CH
30try:
31 # Used by munet native tests
32 from munet.testing.fixtures import event_loop, unet # pylint: disable=all # noqa
33
34 @pytest.fixture(scope="module")
35 def rundir_module(pytestconfig):
36 d = os.path.join(pytestconfig.option.rundir, get_test_logdir())
37 logging.debug("rundir_module: test module rundir %s", d)
38 return d
39
40except (AttributeError, ImportError):
41 pass
42
0b25370e 43
1fca63c1
RZ
44def pytest_addoption(parser):
45 """
46 Add topology-only option to the topology tester. This option makes pytest
47 only run the setup_module() to setup the topology without running any tests.
48 """
0ba1d257
CH
49 parser.addoption(
50 "--asan-abort",
51 action="store_true",
52 help="Configure address sanitizer to abort process on error",
53 )
54
49581587
CH
55 parser.addoption(
56 "--cli-on-error",
57 action="store_true",
58 help="Mininet cli on test failure",
59 )
60
3f950192
CH
61 parser.addoption(
62 "--gdb-breakpoints",
63 metavar="SYMBOL[,SYMBOL...]",
64 help="Comma-separated list of functions to set gdb breakpoints on",
65 )
66
67 parser.addoption(
68 "--gdb-daemons",
69 metavar="DAEMON[,DAEMON...]",
70 help="Comma-separated list of daemons to spawn gdb on, or 'all'",
71 )
72
73 parser.addoption(
74 "--gdb-routers",
75 metavar="ROUTER[,ROUTER...]",
76 help="Comma-separated list of routers to spawn gdb on, or 'all'",
77 )
78
79 parser.addoption(
ff28990e
CH
80 "--logd",
81 action="append",
82 metavar="DAEMON[,ROUTER[,...]",
83 help=(
e6079f4f
CH
84 "Tail-F the DAEMON log file on all or a subset of ROUTERs."
85 " Option can be given multiple times."
ff28990e
CH
86 ),
87 )
88
d4977708
LB
89 parser.addoption(
90 "--memleaks",
91 action="store_true",
92 help="Report memstat results as errors",
93 )
94
ff28990e 95 parser.addoption(
49581587 96 "--pause",
3f950192 97 action="store_true",
49581587 98 help="Pause after each test",
3f950192
CH
99 )
100
773fd82e
CH
101 parser.addoption(
102 "--pause-at-end",
103 action="store_true",
104 help="Pause before taking munet down",
105 )
106
3f950192 107 parser.addoption(
49581587 108 "--pause-on-error",
3f950192 109 action="store_true",
49581587 110 help="Do not pause after (disables default when --shell or -vtysh given)",
3f950192
CH
111 )
112
49581587
CH
113 parser.addoption(
114 "--no-pause-on-error",
115 dest="pause_on_error",
116 action="store_false",
117 help="Do not pause after (disables default when --shell or -vtysh given)",
118 )
119
773fd82e
CH
120 parser.addoption(
121 "--pcap",
122 default="",
123 metavar="NET[,NET...]",
124 help="Comma-separated list of networks to capture packets on, or 'all'",
125 )
126
e6079f4f
CH
127 parser.addoption(
128 "--perf",
129 action="append",
130 metavar="DAEMON[,ROUTER[,...]",
131 help=(
132 "Collect performance data from given DAEMON on all or a subset of ROUTERs."
133 " Option can be given multiple times."
134 ),
135 )
136
137 parser.addoption(
138 "--perf-options",
139 metavar="OPTS",
140 default="-g",
141 help="Options to pass to `perf record`.",
142 )
143
a53c08bc 144 rundir_help = "directory for running in and log files"
49581587
CH
145 parser.addini("rundir", rundir_help, default="/tmp/topotests")
146 parser.addoption("--rundir", metavar="DIR", help=rundir_help)
147
3f950192
CH
148 parser.addoption(
149 "--shell",
150 metavar="ROUTER[,ROUTER...]",
151 help="Comma-separated list of routers to spawn shell on, or 'all'",
152 )
153
154 parser.addoption(
155 "--shell-on-error",
156 action="store_true",
157 help="Spawn shell on all routers on test failure",
158 )
159
0ba1d257
CH
160 parser.addoption(
161 "--strace-daemons",
162 metavar="DAEMON[,DAEMON...]",
163 help="Comma-separated list of daemons to strace, or 'all'",
164 )
165
787e7624 166 parser.addoption(
167 "--topology-only",
168 action="store_true",
c287dfd2 169 default=False,
787e7624 170 help="Only set up this topology, don't run tests",
171 )
172
e58133a7
CH
173 parser.addoption(
174 "--valgrind-extra",
175 action="store_true",
176 help="Generate suppression file, and enable more precise (slower) valgrind checks",
177 )
178
179 parser.addoption(
180 "--valgrind-memleaks",
181 action="store_true",
182 help="Run all daemons under valgrind for memleak detection",
183 )
184
3f950192
CH
185 parser.addoption(
186 "--vtysh",
187 metavar="ROUTER[,ROUTER...]",
188 help="Comma-separated list of routers to spawn vtysh on, or 'all'",
189 )
190
191 parser.addoption(
192 "--vtysh-on-error",
193 action="store_true",
194 help="Spawn vtysh on all routers on test failure",
195 )
196
1fca63c1 197
d4977708 198def check_for_valgrind_memleaks():
249ac6f0 199 assert topotest.g_pytest_config.option.valgrind_memleaks
e58133a7
CH
200
201 leaks = []
1623dc4c 202 tgen = get_topogen() # pylint: disable=redefined-outer-name
e58133a7
CH
203 latest = []
204 existing = []
205 if tgen is not None:
49581587 206 logdir = tgen.logdir
e58133a7
CH
207 if hasattr(tgen, "valgrind_existing_files"):
208 existing = tgen.valgrind_existing_files
209 latest = glob.glob(os.path.join(logdir, "*.valgrind.*"))
1623dc4c 210 latest = [x for x in latest if "core" not in x]
e58133a7 211
a15e5ac0 212 daemons = set()
e58133a7
CH
213 for vfile in latest:
214 if vfile in existing:
215 continue
1623dc4c
CH
216 # do not consider memleaks from parent fork (i.e., owned by root)
217 if os.stat(vfile).st_uid == 0:
218 existing.append(vfile) # do not check again
219 logger.debug("Skipping valgrind file %s owned by root", vfile)
220 continue
221 logger.debug("Checking valgrind file %s not owned by root", vfile)
a15e5ac0 222 with open(vfile, encoding="ascii") as vf:
e58133a7
CH
223 vfcontent = vf.read()
224 match = re.search(r"ERROR SUMMARY: (\d+) errors", vfcontent)
1623dc4c
CH
225 if match:
226 existing.append(vfile) # have summary don't check again
e58133a7 227 if match and match.group(1) != "0":
49581587 228 emsg = "{} in {}".format(match.group(1), vfile)
e58133a7 229 leaks.append(emsg)
1623dc4c
CH
230 daemon = re.match(r".*\.valgrind\.(.*)\.\d+", vfile).group(1)
231 daemons.add("{}({})".format(daemon, match.group(1)))
a15e5ac0
CH
232
233 if tgen is not None:
234 tgen.valgrind_existing_files = existing
e58133a7
CH
235
236 if leaks:
a15e5ac0
CH
237 logger.error("valgrind memleaks found:\n\t%s", "\n\t".join(leaks))
238 pytest.fail("valgrind memleaks found for daemons: " + " ".join(daemons))
e58133a7
CH
239
240
d4977708
LB
241def check_for_memleaks():
242 leaks = []
243 tgen = get_topogen() # pylint: disable=redefined-outer-name
244 latest = []
245 existing = []
246 if tgen is not None:
247 logdir = tgen.logdir
248 if hasattr(tgen, "memstat_existing_files"):
249 existing = tgen.memstat_existing_files
250 latest = glob.glob(os.path.join(logdir, "*/*.err"))
251
252 daemons = []
253 for vfile in latest:
254 if vfile in existing:
255 continue
256 with open(vfile, encoding="ascii") as vf:
257 vfcontent = vf.read()
258 num = vfcontent.count("memstats:")
259 if num:
260 existing.append(vfile) # have summary don't check again
261 emsg = "{} types in {}".format(num, vfile)
262 leaks.append(emsg)
263 daemon = re.match(r".*test[a-z_A-Z0-9\+]*/(.*)\.err", vfile).group(1)
264 daemons.append("{}({})".format(daemon, num))
265
266 if tgen is not None:
267 tgen.memstat_existing_files = existing
268
269 if leaks:
270 logger.error("memleaks found:\n\t%s", "\n\t".join(leaks))
271 pytest.fail("memleaks found for daemons: " + " ".join(daemons))
272
273
1623dc4c
CH
274@pytest.fixture(autouse=True, scope="module")
275def module_check_memtest(request):
1623dc4c 276 yield
249ac6f0 277 if request.config.option.valgrind_memleaks:
d4977708
LB
278 if get_topogen() is not None:
279 check_for_valgrind_memleaks()
280 if request.config.option.memleaks:
1623dc4c
CH
281 if get_topogen() is not None:
282 check_for_memleaks()
283
284
49581587
CH
285def pytest_runtest_logstart(nodeid, location):
286 # location is (filename, lineno, testname)
249ac6f0 287 topolog.logstart(nodeid, location, topotest.g_pytest_config.option.rundir)
49581587
CH
288
289
290def pytest_runtest_logfinish(nodeid, location):
291 # location is (filename, lineno, testname)
292 topolog.logfinish(nodeid, location)
293
294
a15e5ac0
CH
295@pytest.hookimpl(hookwrapper=True)
296def pytest_runtest_call(item: pytest.Item) -> None:
297 "Hook the function that is called to execute the test."
298
299 # For topology only run the CLI then exit
249ac6f0 300 if item.config.option.topology_only:
a15e5ac0
CH
301 get_topogen().cli()
302 pytest.exit("exiting after --topology-only")
8833a838 303
a15e5ac0
CH
304 # Let the default pytest_runtest_call execute the test function
305 yield
306
307 # Check for leaks if requested
249ac6f0 308 if item.config.option.valgrind_memleaks:
d4977708
LB
309 check_for_valgrind_memleaks()
310 if item.config.option.memleaks:
a15e5ac0 311 check_for_memleaks()
787e7624 312
3668ed8d
RZ
313
314def pytest_assertrepr_compare(op, left, right):
315 """
316 Show proper assertion error message for json_cmp results.
317 """
3f950192
CH
318 del op
319
3668ed8d
RZ
320 json_result = left
321 if not isinstance(json_result, json_cmp_result):
322 json_result = right
323 if not isinstance(json_result, json_cmp_result):
324 return None
325
849224d4 326 return json_result.gen_report()
007e7313 327
787e7624 328
007e7313 329def pytest_configure(config):
3f950192
CH
330 """
331 Assert that the environment is correctly configured, and get extra config.
332 """
249ac6f0 333 topotest.g_pytest_config = ConfigOptionsProxy(config)
80cb48d2 334
3d80bd11
DL
335 if config.getoption("--collect-only"):
336 return
337
49581587
CH
338 if "PYTEST_XDIST_WORKER" not in os.environ:
339 os.environ["PYTEST_XDIST_MODE"] = config.getoption("dist", "no")
340 os.environ["PYTEST_TOPOTEST_WORKER"] = ""
341 is_xdist = os.environ["PYTEST_XDIST_MODE"] != "no"
342 is_worker = False
343 else:
344 os.environ["PYTEST_TOPOTEST_WORKER"] = os.environ["PYTEST_XDIST_WORKER"]
345 is_xdist = True
346 is_worker = True
347
e2e677f6
DS
348 resource.setrlimit(
349 resource.RLIMIT_CORE, (resource.RLIM_INFINITY, resource.RLIM_INFINITY)
350 )
49581587
CH
351 # -----------------------------------------------------
352 # Set some defaults for the pytest.ini [pytest] section
353 # ---------------------------------------------------
354
249ac6f0 355 rundir = config.option.rundir
49581587
CH
356 if not rundir:
357 rundir = config.getini("rundir")
358 if not rundir:
359 rundir = "/tmp/topotests"
249ac6f0
CH
360 config.option.rundir = rundir
361
49581587
CH
362 if not config.getoption("--junitxml"):
363 config.option.xmlpath = os.path.join(rundir, "topotests.xml")
364 xmlpath = config.option.xmlpath
365
366 # Save an existing topotest.xml
367 if os.path.exists(xmlpath):
368 fmtime = time.localtime(os.path.getmtime(xmlpath))
369 suffix = "-" + time.strftime("%Y%m%d%H%M%S", fmtime)
370 commander = Commander("pytest")
371 mv_path = commander.get_exec_path("mv")
372 commander.cmd_status([mv_path, xmlpath, xmlpath + suffix])
373
49581587
CH
374 # Set the log_file (exec) to inside the rundir if not specified
375 if not config.getoption("--log-file") and not config.getini("log_file"):
376 config.option.log_file = os.path.join(rundir, "exec.log")
377
378 # Turn on live logging if user specified verbose and the config has a CLI level set
379 if config.getoption("--verbose") and not is_xdist and not config.getini("log_cli"):
380 if config.getoption("--log-cli-level", None) is None:
381 # By setting the CLI option to the ini value it enables log_cli=1
382 cli_level = config.getini("log_cli_level")
383 if cli_level is not None:
384 config.option.log_cli_level = cli_level
02547745 385
1726edc3
CH
386 have_tmux = bool(os.getenv("TMUX", ""))
387 have_screen = not have_tmux and bool(os.getenv("STY", ""))
388 have_xterm = not have_tmux and not have_screen and bool(os.getenv("DISPLAY", ""))
389 have_windows = have_tmux or have_screen or have_xterm
390 have_windows_pause = have_tmux or have_xterm
391 xdist_no_windows = is_xdist and not is_worker and not have_windows_pause
392
393 def assert_feature_windows(b, feature):
394 if b and xdist_no_windows:
395 pytest.exit(
396 "{} use requires byobu/TMUX/XTerm under dist {}".format(
397 feature, os.environ["PYTEST_XDIST_MODE"]
398 )
399 )
400 elif b and not is_xdist and not have_windows:
401 pytest.exit("{} use requires byobu/TMUX/SCREEN/XTerm".format(feature))
402
249ac6f0
CH
403 #
404 # Check for window capability if given options that require window
405 #
406 assert_feature_windows(config.option.gdb_routers, "GDB")
407 assert_feature_windows(config.option.gdb_daemons, "GDB")
408 assert_feature_windows(config.option.cli_on_error, "--cli-on-error")
409 assert_feature_windows(config.option.shell, "--shell")
410 assert_feature_windows(config.option.shell_on_error, "--shell-on-error")
411 assert_feature_windows(config.option.vtysh, "--vtysh")
412 assert_feature_windows(config.option.vtysh_on_error, "--vtysh-on-error")
413
414 if config.option.topology_only and is_xdist:
a15e5ac0 415 pytest.exit("Cannot use --topology-only with distributed test mode")
80cb48d2 416
d4977708
LB
417 pytest.exit("Cannot use --topology-only with distributed test mode")
418
49581587
CH
419 # Check environment now that we have config
420 if not diagnose_env(rundir):
e40b7130 421 pytest.exit("environment has errors, please read the logs in %s" % rundir)
49581587 422
d4977708
LB
423 # slave TOPOTESTS_CHECK_MEMLEAK to memleaks flag
424 if config.option.memleaks:
425 if "TOPOTESTS_CHECK_MEMLEAK" not in os.environ:
426 os.environ["TOPOTESTS_CHECK_MEMLEAK"] = "/dev/null"
427 else:
428 if "TOPOTESTS_CHECK_MEMLEAK" in os.environ:
429 del os.environ["TOPOTESTS_CHECK_MEMLEAK"]
430 if "TOPOTESTS_CHECK_STDERR" in os.environ:
431 del os.environ["TOPOTESTS_CHECK_STDERR"]
432
49581587
CH
433
434@pytest.fixture(autouse=True, scope="session")
435def setup_session_auto():
436 if "PYTEST_TOPOTEST_WORKER" not in os.environ:
437 is_worker = False
438 elif not os.environ["PYTEST_TOPOTEST_WORKER"]:
439 is_worker = False
440 else:
441 is_worker = True
442
443 logger.debug("Before the run (is_worker: %s)", is_worker)
444 if not is_worker:
445 cleanup_previous()
446 yield
447 if not is_worker:
448 cleanup_current()
449 logger.debug("After the run (is_worker: %s)", is_worker)
450
787e7624 451
02547745
CH
452def pytest_runtest_setup(item):
453 module = item.parent.module
454 script_dir = os.path.abspath(os.path.dirname(module.__file__))
455 os.environ["PYTEST_TOPOTEST_SCRIPTDIR"] = script_dir
456
457
e7ba3cd1
RZ
458def pytest_runtest_makereport(item, call):
459 "Log all assert messages to default logger with error level"
e7ba3cd1 460
cad55444 461 pause = bool(item.config.getoption("--pause"))
a53c08bc 462 title = "unset"
49581587 463
3f950192
CH
464 if call.excinfo is None:
465 error = False
466 else:
467 parent = item.parent
468 modname = parent.module.__name__
469
470 # Treat skips as non errors, don't pause after
4f99894d 471 if call.excinfo.typename == "Skipped":
3f950192
CH
472 pause = False
473 error = False
474 logger.info(
4f99894d 475 'test skipped at "{}/{}": {}'.format(
3f950192
CH
476 modname, item.name, call.excinfo.value
477 )
478 )
479 else:
480 error = True
481 # Handle assert failures
0b25370e 482 parent._previousfailed = item # pylint: disable=W0212
3f950192 483 logger.error(
4f99894d 484 'test failed at "{}/{}": {}'.format(
0b25370e
DS
485 modname, item.name, call.excinfo.value
486 )
787e7624 487 )
49581587 488 title = "{}/{}".format(modname, item.name)
c8c265f5 489
0ba1d257
CH
490 # We want to pause, if requested, on any error not just test cases
491 # (e.g., call.when == "setup")
492 if not pause:
249ac6f0 493 pause = item.config.option.pause_on_error or item.config.option.pause
0ba1d257 494
3f950192 495 # (topogen) Set topology error to avoid advancing in the test.
1623dc4c 496 tgen = get_topogen() # pylint: disable=redefined-outer-name
3f950192
CH
497 if tgen is not None:
498 # This will cause topogen to report error on `routers_have_failure`.
499 tgen.set_error("{}/{}".format(modname, item.name))
500
49581587
CH
501 commander = Commander("pytest")
502 isatty = sys.stdout.isatty()
503 error_cmd = None
3f950192 504
249ac6f0 505 if error and item.config.option.vtysh_on_error:
49581587 506 error_cmd = commander.get_exec_path(["vtysh"])
249ac6f0 507 elif error and item.config.option.shell_on_error:
49581587
CH
508 error_cmd = os.getenv("SHELL", commander.get_exec_path(["bash"]))
509
510 if error_cmd:
1726edc3
CH
511 is_tmux = bool(os.getenv("TMUX", ""))
512 is_screen = not is_tmux and bool(os.getenv("STY", ""))
513 is_xterm = not is_tmux and not is_screen and bool(os.getenv("DISPLAY", ""))
514
515 channel = None
49581587
CH
516 win_info = None
517 wait_for_channels = []
1726edc3
CH
518 wait_for_procs = []
519 # Really would like something better than using this global here.
520 # Not all tests use topogen though so get_topogen() won't work.
49581587 521 for node in Mininet.g_mnet_inst.hosts.values():
3f950192 522 pause = True
3f950192 523
1726edc3
CH
524 if is_tmux:
525 channel = (
526 "{}-{}".format(os.getpid(), Commander.tmux_wait_gen)
527 if not isatty
528 else None
529 )
530 Commander.tmux_wait_gen += 1
531 wait_for_channels.append(channel)
49581587
CH
532
533 pane_info = node.run_in_window(
534 error_cmd,
535 new_window=win_info is None,
536 background=True,
537 title="{} ({})".format(title, node.name),
538 name=title,
539 tmux_target=win_info,
a53c08bc 540 wait_for=channel,
49581587 541 )
1726edc3
CH
542 if is_tmux:
543 if win_info is None:
544 win_info = pane_info
545 elif is_xterm:
546 assert isinstance(pane_info, subprocess.Popen)
547 wait_for_procs.append(pane_info)
49581587
CH
548
549 # Now wait on any channels
550 for channel in wait_for_channels:
551 logger.debug("Waiting on TMUX channel %s", channel)
552 commander.cmd_raises([commander.get_exec_path("tmux"), "wait", channel])
1726edc3
CH
553 for p in wait_for_procs:
554 logger.debug("Waiting on TMUX xterm process %s", p)
555 o, e = p.communicate()
556 if p.wait():
557 logger.warning("xterm proc failed: %s:", proc_error(p, o, e))
49581587 558
249ac6f0 559 if error and item.config.option.cli_on_error:
49581587
CH
560 # Really would like something better than using this global here.
561 # Not all tests use topogen though so get_topogen() won't work.
562 if Mininet.g_mnet_inst:
60e03778 563 cli.cli(Mininet.g_mnet_inst, title=title, background=False)
49581587
CH
564 else:
565 logger.error("Could not launch CLI b/c no mininet exists yet")
3f950192 566
249ac6f0
CH
567 if pause and isatty:
568 pause_test()
49581587
CH
569
570
571#
572# Add common fixtures available to all tests as parameters
573#
e2e677f6 574
49581587
CH
575tgen = pytest.fixture(lib.fixtures.tgen)
576topo = pytest.fixture(lib.fixtures.topo)