From mboxrd@z Thu Jan 1 00:00:00 1970 Authentication-Results: passt.top; dmarc=none (p=none dis=none) header.from=gibson.dropbear.id.au Authentication-Results: passt.top; dkim=pass (2048-bit key; secure) header.d=gibson.dropbear.id.au header.i=@gibson.dropbear.id.au header.a=rsa-sha256 header.s=202408 header.b=qCH/EV26; dkim-atps=neutral Received: from mail.ozlabs.org (mail.ozlabs.org [IPv6:2404:9400:2221:ea00::3]) by passt.top (Postfix) with ESMTPS id E8FDA5A027B for ; Mon, 26 Aug 2024 04:09:58 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gibson.dropbear.id.au; s=202408; t=1724638184; bh=2iPhMTCKA2+PqWSC5zsrOPxLvEOsggCpcCf6LKaYaVE=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=qCH/EV26GSxReWJ2Lrdjjtxd++zz/BFMG/hpjZG0SADGabQwZH2qhOHKFVti0a6fy bidcG33y0OFE/WAXot2Cvp32PMU4ZXyet6tN6rx+FclPpNzj72DrWFxh7ILAT4av8K 3EF5fqDgoHvCAWykEsi0R5EZsfpDn2V8BuNoMRsdEqDPfkUDleuIvVjhtUkZIQH/vX HdN15h6TVjJb01+ClCoEFO95l1pCmsnp+pDEMKTOJCsK6u2Db1+D1zsUoZNWpLMlop rTPwsurl68yTKxfTUZNm0z5VH+rlhO6ePqlJ1gKlwW0it3VfYK9Zy7/LfTK6lkOJY6 RYRp2s2PJgFAQ== Received: by gandalf.ozlabs.org (Postfix, from userid 1007) id 4WsYyw5XVlz4x8V; Mon, 26 Aug 2024 12:09:44 +1000 (AEST) From: David Gibson To: Stefano Brivio , passt-dev@passt.top Subject: [PATCH v3 06/15] tasst/cmdsite: Base helpers for running shell commands in various places Date: Mon, 26 Aug 2024 12:09:33 +1000 Message-ID: <20240826020942.545155-7-david@gibson.dropbear.id.au> X-Mailer: git-send-email 2.46.0 In-Reply-To: <20240826020942.545155-1-david@gibson.dropbear.id.au> References: <20240826020942.545155-1-david@gibson.dropbear.id.au> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Message-ID-Hash: 65XJOQ6WTRMMTTRONNGJ3OJBF6ZUH7YZ X-Message-ID-Hash: 65XJOQ6WTRMMTTRONNGJ3OJBF6ZUH7YZ X-MailFrom: dgibson@gandalf.ozlabs.org X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; emergency; loop; banned-address; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Cleber Rosa , David Gibson X-Mailman-Version: 3.3.8 Precedence: list List-Id: Development discussion and patches for passt Archived-At: Archived-At: List-Archive: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Signed-off-by: David Gibson --- test/Makefile | 3 +- test/tasst/__main__.py | 19 +++- test/tasst/cmdsite.py | 193 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 210 insertions(+), 5 deletions(-) create mode 100644 test/tasst/cmdsite.py diff --git a/test/Makefile b/test/Makefile index 1daf1999..f1632f4d 100644 --- a/test/Makefile +++ b/test/Makefile @@ -70,7 +70,8 @@ EXETER_JOBS = $(EXETER_SH:%.sh=%.json) AVOCADO_JOBS = $(EXETER_JOBS) avocado/static_checkers.json -TASST_SRCS = __init__.py __main__.py +TASST_MODS = $(shell python3 -m tasst --modules) +TASST_SRCS = __init__.py __main__.py $(TASST_MODS) EXETER_META = meta/lint.json meta/tasst.json META_JOBS = $(EXETER_META) diff --git a/test/tasst/__main__.py b/test/tasst/__main__.py index 310e31d7..4ea4c593 100644 --- a/test/tasst/__main__.py +++ b/test/tasst/__main__.py @@ -10,13 +10,24 @@ Test A Simple Socket Transport library of test helpers for passt & pasta """ -import exeter +import importlib +import sys +import exeter -@exeter.test -def placeholder() -> None: - pass +MODULES = [ + 'cmdsite', +] if __name__ == '__main__': + if sys.argv[1:] == ["--modules"]: + for m in MODULES: + print(m.replace('.', '/') + '.py') + sys.exit(0) + + for m in MODULES: + mod = importlib.import_module('.' + m, __package__) + mod.selftests() + exeter.main() diff --git a/test/tasst/cmdsite.py b/test/tasst/cmdsite.py new file mode 100644 index 00000000..ea2bdaa3 --- /dev/null +++ b/test/tasst/cmdsite.py @@ -0,0 +1,193 @@ +#! /usr/bin/env python3 + +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Copyright Red Hat +# Author: David Gibson + +""" +Test A Simple Socket Transport + +tasst/snh.py - Simulated network hosts for testing +""" + +from __future__ import annotations + +import contextlib +import enum +import subprocess +import sys +from typing import Any, Iterator, Optional + +import exeter + + +class Capture(enum.Enum): + STDOUT = 1 + + +# We might need our own versions of these eventually, but for now we +# can just alias the ones in subprocess +CompletedCmd = subprocess.CompletedProcess[bytes] +TimeoutExpired = subprocess.TimeoutExpired +CmdError = subprocess.CalledProcessError + + +class RunningCmd: + """ + A background process running on a CmdSite + """ + site: CmdSite + cmd: tuple[str, ...] + check: bool + popen: subprocess.Popen[bytes] + + def __init__(self, site: CmdSite, popen: subprocess.Popen[bytes], + *cmd: str, check: bool = True) -> None: + self.site = site + self.popen = popen + self.cmd = cmd + self.check = check + + def run(self, **kwargs: Any) -> CompletedCmd: + stdout, stderr = self.popen.communicate(**kwargs) + cp = CompletedCmd(self.popen.args, self.popen.returncode, + stdout, stderr) + if self.check: + cp.check_returncode() + return cp + + def terminate(self) -> None: + self.popen.terminate() + + def kill(self) -> None: + self.popen.kill() + + +class CmdSite(exeter.Scenario): + """ + A (usually virtual or simulated) location where we can execute + commands and configure networks. + + """ + name: str + + def __init__(self, name: str) -> None: + self.name = name # For debugging + + def output(self, *cmd: str, **kwargs: Any) -> bytes: + proc = self.fg(*cmd, capture=Capture.STDOUT, **kwargs) + return proc.stdout + + def fg(self, *cmd: str, timeout: Optional[float] = None, **kwargs: Any) \ + -> CompletedCmd: + # We don't use subprocess.run() because it kills without + # attempting to terminate on timeout + with self.bg(*cmd, **kwargs) as proc: + res = proc.run(timeout=timeout) + return res + + def sh(self, script: str, **kwargs: Any) -> None: + for cmd in script.splitlines(): + self.fg(cmd, shell=True, **kwargs) + + @contextlib.contextmanager + def bg(self, *cmd: str, capture: Optional[Capture] = None, + check: bool = True, context_timeout: float = 1.0, **kwargs: Any) \ + -> Iterator[RunningCmd]: + if capture == Capture.STDOUT: + kwargs['stdout'] = subprocess.PIPE + print(f"Site {self.name}: {cmd}", file=sys.stderr) + with self.popen(*cmd, **kwargs) as popen: + proc = RunningCmd(self, popen, *cmd, check=check) + try: + yield proc + finally: + try: + popen.wait(timeout=context_timeout) + except subprocess.TimeoutExpired as e: + popen.terminate() + try: + popen.wait(timeout=context_timeout) + except subprocess.TimeoutExpired: + popen.kill() + raise e + + def popen(self, *cmd: str, **kwargs: Any) -> subprocess.Popen[bytes]: + raise NotImplementedError + + @exeter.scenariotest + def test_true(self) -> None: + self.fg('true') + + @exeter.scenariotest + def test_false(self) -> None: + exeter.assert_raises(CmdError, self.fg, 'false') + + @exeter.scenariotest + def test_echo(self) -> None: + msg = 'Hello tasst' + out = self.output('echo', f'{msg}') + exeter.assert_eq(out, msg.encode('utf-8') + b'\n') + + @exeter.scenariotest + def test_timeout(self) -> None: + exeter.assert_raises(TimeoutExpired, self.fg, + 'sleep', 'infinity', timeout=0.1, check=False) + + @exeter.scenariotest + def test_bg_true(self) -> None: + with self.bg('true') as proc: + proc.run() + + @exeter.scenariotest + def test_bg_false(self) -> None: + with self.bg('false') as proc: + exeter.assert_raises(CmdError, proc.run) + + @exeter.scenariotest + def test_bg_echo(self) -> None: + msg = 'Hello tasst' + with self.bg('echo', f'{msg}', capture=Capture.STDOUT) as proc: + res = proc.run() + exeter.assert_eq(res.stdout, msg.encode('utf-8') + b'\n') + + @exeter.scenariotest + def test_bg_timeout(self) -> None: + with self.bg('sleep', 'infinity') as proc: + exeter.assert_raises(TimeoutExpired, proc.run, timeout=0.1) + proc.terminate() + + @exeter.scenariotest + def test_bg_context_timeout(self) -> None: + def run_timeout() -> None: + with self.bg('sleep', 'infinity', context_timeout=0.1): + pass + exeter.assert_raises(TimeoutExpired, run_timeout) + + +class BuildHost(CmdSite): + """ + Represents the host on which the tests are running (as opposed + to some simulated host created by the tests) + """ + + def __init__(self) -> None: + super().__init__('BUILD_HOST') + + def popen(self, *cmd: str, privilege: bool = False, **kwargs: Any) \ + -> subprocess.Popen[bytes]: + assert not privilege, \ + "BUG: Shouldn't run commands with privilege on host" + return subprocess.Popen(cmd, **kwargs) + + +BUILD_HOST = BuildHost() + + +def build_host() -> Iterator[BuildHost]: + yield BUILD_HOST + + +def selftests() -> None: + CmdSite.test(build_host) -- 2.46.0