]>
git.proxmox.com Git - mirror_frr.git/blob - tests/topotests/conftest.py
8e4d13df7d14826d2e6f71696e269474365eb670
1 # -*- coding: utf-8 eval: (blacken-mode 1) -*-
3 Topotest conftest.py file.
5 # pylint: disable=consider-using-f-string
17 from lib
.micronet_compat
import ConfigOptionsProxy
, Mininet
18 from lib
.topogen
import diagnose_env
, get_topogen
19 from lib
.topolog
import logger
20 from lib
.topotest
import json_cmp_result
22 from munet
.base
import Commander
, proc_error
23 from munet
.cleanup
import cleanup_current
, cleanup_previous
24 from munet
.testing
.util
import pause_test
26 from lib
import topolog
, topotest
29 def pytest_addoption(parser
):
31 Add topology-only option to the topology tester. This option makes pytest
32 only run the setup_module() to setup the topology without running any tests.
37 help="Configure address sanitizer to abort process on error",
43 help="Mininet cli on test failure",
48 metavar
="SYMBOL[,SYMBOL...]",
49 help="Comma-separated list of functions to set gdb breakpoints on",
54 metavar
="DAEMON[,DAEMON...]",
55 help="Comma-separated list of daemons to spawn gdb on, or 'all'",
60 metavar
="ROUTER[,ROUTER...]",
61 help="Comma-separated list of routers to spawn gdb on, or 'all'",
67 metavar
="DAEMON[,ROUTER[,...]",
69 "Tail-F of DAEMON log file. Specify routers in comma-separated list after "
70 "daemon to limit to a subset of routers"
77 help="Pause after each test",
83 help="Pause before taking munet down",
89 help="Do not pause after (disables default when --shell or -vtysh given)",
93 "--no-pause-on-error",
94 dest
="pause_on_error",
96 help="Do not pause after (disables default when --shell or -vtysh given)",
102 metavar
="NET[,NET...]",
103 help="Comma-separated list of networks to capture packets on, or 'all'",
106 rundir_help
= "directory for running in and log files"
107 parser
.addini("rundir", rundir_help
, default
="/tmp/topotests")
108 parser
.addoption("--rundir", metavar
="DIR", help=rundir_help
)
112 metavar
="ROUTER[,ROUTER...]",
113 help="Comma-separated list of routers to spawn shell on, or 'all'",
119 help="Spawn shell on all routers on test failure",
124 metavar
="DAEMON[,DAEMON...]",
125 help="Comma-separated list of daemons to strace, or 'all'",
132 help="Only set up this topology, don't run tests",
138 help="Generate suppression file, and enable more precise (slower) valgrind checks",
142 "--valgrind-memleaks",
144 help="Run all daemons under valgrind for memleak detection",
149 metavar
="ROUTER[,ROUTER...]",
150 help="Comma-separated list of routers to spawn vtysh on, or 'all'",
156 help="Spawn vtysh on all routers on test failure",
160 def check_for_memleaks():
161 assert topotest
.g_pytest_config
.option
.valgrind_memleaks
164 tgen
= get_topogen() # pylint: disable=redefined-outer-name
169 if hasattr(tgen
, "valgrind_existing_files"):
170 existing
= tgen
.valgrind_existing_files
171 latest
= glob
.glob(os
.path
.join(logdir
, "*.valgrind.*"))
172 latest
= [x
for x
in latest
if "core" not in x
]
176 if vfile
in existing
:
178 # do not consider memleaks from parent fork (i.e., owned by root)
179 if os
.stat(vfile
).st_uid
== 0:
180 existing
.append(vfile
) # do not check again
181 logger
.debug("Skipping valgrind file %s owned by root", vfile
)
183 logger
.debug("Checking valgrind file %s not owned by root", vfile
)
184 with
open(vfile
, encoding
="ascii") as vf
:
185 vfcontent
= vf
.read()
186 match
= re
.search(r
"ERROR SUMMARY: (\d+) errors", vfcontent
)
188 existing
.append(vfile
) # have summary don't check again
189 if match
and match
.group(1) != "0":
190 emsg
= "{} in {}".format(match
.group(1), vfile
)
192 daemon
= re
.match(r
".*\.valgrind\.(.*)\.\d+", vfile
).group(1)
193 daemons
.add("{}({})".format(daemon
, match
.group(1)))
196 tgen
.valgrind_existing_files
= existing
199 logger
.error("valgrind memleaks found:\n\t%s", "\n\t".join(leaks
))
200 pytest
.fail("valgrind memleaks found for daemons: " + " ".join(daemons
))
203 @pytest.fixture(autouse
=True, scope
="module")
204 def module_check_memtest(request
):
206 if request
.config
.option
.valgrind_memleaks
:
207 if get_topogen() is not None:
211 def pytest_runtest_logstart(nodeid
, location
):
212 # location is (filename, lineno, testname)
213 topolog
.logstart(nodeid
, location
, topotest
.g_pytest_config
.option
.rundir
)
216 def pytest_runtest_logfinish(nodeid
, location
):
217 # location is (filename, lineno, testname)
218 topolog
.logfinish(nodeid
, location
)
221 @pytest.hookimpl(hookwrapper
=True)
222 def pytest_runtest_call(item
: pytest
.Item
) -> None:
223 "Hook the function that is called to execute the test."
225 # For topology only run the CLI then exit
226 if item
.config
.option
.topology_only
:
228 pytest
.exit("exiting after --topology-only")
230 # Let the default pytest_runtest_call execute the test function
233 # Check for leaks if requested
234 if item
.config
.option
.valgrind_memleaks
:
238 def pytest_assertrepr_compare(op
, left
, right
):
240 Show proper assertion error message for json_cmp results.
245 if not isinstance(json_result
, json_cmp_result
):
247 if not isinstance(json_result
, json_cmp_result
):
250 return json_result
.gen_report()
253 def pytest_configure(config
):
255 Assert that the environment is correctly configured, and get extra config.
257 topotest
.g_pytest_config
= ConfigOptionsProxy(config
)
259 if config
.getoption("--collect-only"):
262 if "PYTEST_XDIST_WORKER" not in os
.environ
:
263 os
.environ
["PYTEST_XDIST_MODE"] = config
.getoption("dist", "no")
264 os
.environ
["PYTEST_TOPOTEST_WORKER"] = ""
265 is_xdist
= os
.environ
["PYTEST_XDIST_MODE"] != "no"
268 os
.environ
["PYTEST_TOPOTEST_WORKER"] = os
.environ
["PYTEST_XDIST_WORKER"]
273 resource
.RLIMIT_CORE
, (resource
.RLIM_INFINITY
, resource
.RLIM_INFINITY
)
275 # -----------------------------------------------------
276 # Set some defaults for the pytest.ini [pytest] section
277 # ---------------------------------------------------
279 rundir
= config
.option
.rundir
281 rundir
= config
.getini("rundir")
283 rundir
= "/tmp/topotests"
284 config
.option
.rundir
= rundir
286 if not config
.getoption("--junitxml"):
287 config
.option
.xmlpath
= os
.path
.join(rundir
, "topotests.xml")
288 xmlpath
= config
.option
.xmlpath
290 # Save an existing topotest.xml
291 if os
.path
.exists(xmlpath
):
292 fmtime
= time
.localtime(os
.path
.getmtime(xmlpath
))
293 suffix
= "-" + time
.strftime("%Y%m%d%H%M%S", fmtime
)
294 commander
= Commander("pytest")
295 mv_path
= commander
.get_exec_path("mv")
296 commander
.cmd_status([mv_path
, xmlpath
, xmlpath
+ suffix
])
298 # Set the log_file (exec) to inside the rundir if not specified
299 if not config
.getoption("--log-file") and not config
.getini("log_file"):
300 config
.option
.log_file
= os
.path
.join(rundir
, "exec.log")
302 # Turn on live logging if user specified verbose and the config has a CLI level set
303 if config
.getoption("--verbose") and not is_xdist
and not config
.getini("log_cli"):
304 if config
.getoption("--log-cli-level", None) is None:
305 # By setting the CLI option to the ini value it enables log_cli=1
306 cli_level
= config
.getini("log_cli_level")
307 if cli_level
is not None:
308 config
.option
.log_cli_level
= cli_level
310 have_tmux
= bool(os
.getenv("TMUX", ""))
311 have_screen
= not have_tmux
and bool(os
.getenv("STY", ""))
312 have_xterm
= not have_tmux
and not have_screen
and bool(os
.getenv("DISPLAY", ""))
313 have_windows
= have_tmux
or have_screen
or have_xterm
314 have_windows_pause
= have_tmux
or have_xterm
315 xdist_no_windows
= is_xdist
and not is_worker
and not have_windows_pause
317 def assert_feature_windows(b
, feature
):
318 if b
and xdist_no_windows
:
320 "{} use requires byobu/TMUX/XTerm under dist {}".format(
321 feature
, os
.environ
["PYTEST_XDIST_MODE"]
324 elif b
and not is_xdist
and not have_windows
:
325 pytest
.exit("{} use requires byobu/TMUX/SCREEN/XTerm".format(feature
))
328 # Check for window capability if given options that require window
330 assert_feature_windows(config
.option
.gdb_routers
, "GDB")
331 assert_feature_windows(config
.option
.gdb_daemons
, "GDB")
332 assert_feature_windows(config
.option
.cli_on_error
, "--cli-on-error")
333 assert_feature_windows(config
.option
.shell
, "--shell")
334 assert_feature_windows(config
.option
.shell_on_error
, "--shell-on-error")
335 assert_feature_windows(config
.option
.vtysh
, "--vtysh")
336 assert_feature_windows(config
.option
.vtysh_on_error
, "--vtysh-on-error")
338 if config
.option
.topology_only
and is_xdist
:
339 pytest
.exit("Cannot use --topology-only with distributed test mode")
341 # Check environment now that we have config
342 if not diagnose_env(rundir
):
343 pytest
.exit("environment has errors, please read the logs in %s" % rundir
)
346 @pytest.fixture(autouse
=True, scope
="session")
347 def setup_session_auto():
348 if "PYTEST_TOPOTEST_WORKER" not in os
.environ
:
350 elif not os
.environ
["PYTEST_TOPOTEST_WORKER"]:
355 logger
.debug("Before the run (is_worker: %s)", is_worker
)
361 logger
.debug("After the run (is_worker: %s)", is_worker
)
364 def pytest_runtest_setup(item
):
365 module
= item
.parent
.module
366 script_dir
= os
.path
.abspath(os
.path
.dirname(module
.__file
__))
367 os
.environ
["PYTEST_TOPOTEST_SCRIPTDIR"] = script_dir
370 def pytest_runtest_makereport(item
, call
):
371 "Log all assert messages to default logger with error level"
373 pause
= bool(item
.config
.getoption("--pause"))
376 if call
.excinfo
is None:
380 modname
= parent
.module
.__name
__
382 # Treat skips as non errors, don't pause after
383 if call
.excinfo
.typename
== "Skipped":
387 'test skipped at "{}/{}": {}'.format(
388 modname
, item
.name
, call
.excinfo
.value
393 # Handle assert failures
394 parent
._previousfailed
= item
# pylint: disable=W0212
396 'test failed at "{}/{}": {}'.format(
397 modname
, item
.name
, call
.excinfo
.value
400 title
= "{}/{}".format(modname
, item
.name
)
402 # We want to pause, if requested, on any error not just test cases
403 # (e.g., call.when == "setup")
405 pause
= item
.config
.option
.pause_on_error
or item
.config
.option
.pause
407 # (topogen) Set topology error to avoid advancing in the test.
408 tgen
= get_topogen() # pylint: disable=redefined-outer-name
410 # This will cause topogen to report error on `routers_have_failure`.
411 tgen
.set_error("{}/{}".format(modname
, item
.name
))
413 commander
= Commander("pytest")
414 isatty
= sys
.stdout
.isatty()
417 if error
and item
.config
.option
.vtysh_on_error
:
418 error_cmd
= commander
.get_exec_path(["vtysh"])
419 elif error
and item
.config
.option
.shell_on_error
:
420 error_cmd
= os
.getenv("SHELL", commander
.get_exec_path(["bash"]))
423 is_tmux
= bool(os
.getenv("TMUX", ""))
424 is_screen
= not is_tmux
and bool(os
.getenv("STY", ""))
425 is_xterm
= not is_tmux
and not is_screen
and bool(os
.getenv("DISPLAY", ""))
429 wait_for_channels
= []
431 # Really would like something better than using this global here.
432 # Not all tests use topogen though so get_topogen() won't work.
433 for node
in Mininet
.g_mnet_inst
.hosts
.values():
438 "{}-{}".format(os
.getpid(), Commander
.tmux_wait_gen
)
442 Commander
.tmux_wait_gen
+= 1
443 wait_for_channels
.append(channel
)
445 pane_info
= node
.run_in_window(
447 new_window
=win_info
is None,
449 title
="{} ({})".format(title
, node
.name
),
451 tmux_target
=win_info
,
458 assert isinstance(pane_info
, subprocess
.Popen
)
459 wait_for_procs
.append(pane_info
)
461 # Now wait on any channels
462 for channel
in wait_for_channels
:
463 logger
.debug("Waiting on TMUX channel %s", channel
)
464 commander
.cmd_raises([commander
.get_exec_path("tmux"), "wait", channel
])
465 for p
in wait_for_procs
:
466 logger
.debug("Waiting on TMUX xterm process %s", p
)
467 o
, e
= p
.communicate()
469 logger
.warning("xterm proc failed: %s:", proc_error(p
, o
, e
))
471 if error
and item
.config
.option
.cli_on_error
:
472 # Really would like something better than using this global here.
473 # Not all tests use topogen though so get_topogen() won't work.
474 if Mininet
.g_mnet_inst
:
475 cli
.cli(Mininet
.g_mnet_inst
, title
=title
, background
=False)
477 logger
.error("Could not launch CLI b/c no mininet exists yet")
484 # Add common fixtures available to all tests as parameters
487 tgen
= pytest
.fixture(lib
.fixtures
.tgen
)
488 topo
= pytest
.fixture(lib
.fixtures
.topo
)