# -*- coding: utf-8 eval: (blacken-mode 1) -*- # SPDX-License-Identifier: GPL-2.0-or-later # # April 22 2022, Christian Hopps # # Copyright (c) 2022, LabN Consulting, L.L.C # """Utility functions useful when using munet testing functionailty in pytest.""" import asyncio import datetime import fcntl import functools import logging import os import re import select import sys import time from ..base import BaseMunet from ..base import Timeout from ..cli import async_cli # ================= # Utility Functions # ================= async def async_pause_test(desc=""): """Pause the running of a test offering options for CLI or PDB.""" isatty = sys.stdout.isatty() if not isatty: desc = f" for {desc}" if desc else "" logging.info("NO PAUSE on non-tty terminal%s", desc) return while True: if desc: print(f"\n== PAUSING: {desc} ==") try: user = input('PAUSED, "cli" for CLI, "pdb" to debug, "Enter" to continue: ') except EOFError: print("^D...continuing") break user = user.strip() if user == "cli": await async_cli(BaseMunet.g_unet) elif user == "pdb": breakpoint() # pylint: disable=W1515 elif user: print(f'Unrecognized input: "{user}"') else: break def pause_test(desc=""): """Pause the running of a test offering options for CLI or PDB.""" asyncio.run(async_pause_test(desc)) def retry(retry_timeout, initial_wait=0, retry_sleep=2, expected=True): """Retry decorated function until it returns None, raises an exception, or timeout. * `retry_timeout`: Retry for at least this many seconds; after waiting initial_wait seconds * `initial_wait`: Sleeps for this many seconds before first executing function * `retry_sleep`: The time to sleep between retries. * `expected`: if False then the return logic is inverted, except for exceptions, (i.e., a non None ends the retry loop, and returns that value) """ def _retry(func): @functools.wraps(func) def func_retry(*args, **kwargs): # Allow the wrapped function's args to override the fixtures _retry_sleep = float(kwargs.pop("retry_sleep", retry_sleep)) _retry_timeout = kwargs.pop("retry_timeout", retry_timeout) _expected = kwargs.pop("expected", expected) _initial_wait = kwargs.pop("initial_wait", initial_wait) retry_until = datetime.datetime.now() + datetime.timedelta( seconds=_retry_timeout + _initial_wait ) if initial_wait > 0: logging.info("Waiting for [%s]s as initial delay", initial_wait) time.sleep(initial_wait) while True: seconds_left = (retry_until - datetime.datetime.now()).total_seconds() try: try: ret = func(*args, seconds_left=seconds_left, **kwargs) except TypeError as error: if "seconds_left" not in str(error): raise ret = func(*args, **kwargs) logging.debug("Function returned %s", ret) positive_result = ret is None if _expected == positive_result: logging.debug("Function succeeds") return ret except Exception as error: logging.info('Function raised exception: "%s"', error) ret = error if seconds_left < 0: logging.info("Retry timeout of %ds reached", _retry_timeout) if isinstance(ret, Exception): raise ret return ret logging.info( "Sleeping %ds until next retry with %.1f retry time left", _retry_sleep, seconds_left, ) time.sleep(_retry_sleep) func_retry._original = func # pylint: disable=W0212 return func_retry return _retry def readline(f, timeout=None): """Read a line or timeout. This function will take over the file object, the file object should not be used outside of calling this function once you begin. Return: A line, remaining buffer if EOF (subsequent calls will return ""), or None for timeout. """ fd = f.fileno() if not hasattr(f, "munet_non_block_set"): flags = fcntl.fcntl(fd, fcntl.F_GETFL) fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) f.munet_non_block_set = True f.munet_lines = [] f.munet_buf = "" if f.munet_lines: return f.munet_lines.pop(0) timeout = Timeout(timeout) remaining = timeout.remaining() while remaining > 0: ready, _, _ = select.select([fd], [], [], remaining) if not ready: return None c = f.read() if c is None: logging.error("munet readline: unexpected None during read") return None if not c: logging.debug("munet readline: got eof") c = f.munet_buf f.munet_buf = "" return c f.munet_buf += c while "\n" in f.munet_buf: a, f.munet_buf = f.munet_buf.split("\n", 1) f.munet_lines.append(a + "\n") if f.munet_lines: return f.munet_lines.pop(0) remaining = timeout.remaining() return None def waitline(f, regex, timeout=120): """Match a regex within lines from a file with a timeout. This function will take over the file object (by calling `readline` above), the file object should not be used outside of calling these functions once you begin. Return: the match object or None. """ timeo = Timeout(timeout) while not timeo.is_expired(): line = readline(f, timeo.remaining()) if line is None: break if line == "": logging.warning("waitline: got eof while matching '%s'", regex) return None assert line[-1] == "\n" line = line[:-1] if not line: continue logging.debug("waitline: searching: '%s' for '%s'", line, regex) m = re.search(regex, line) if m: logging.debug("waitline: matched '%s'", m.group(0)) return m logging.warning( "Timeout while getting output matching '%s' within %ss (actual %ss)", regex, timeout, timeo.elapsed(), ) return None