Jakub Kicinski <kuba@xxxxxxxxxx> writes: > Add glue code for accessing the YNL library which lives under > tools/net and YAML spec files from under Documentation/. > Automatically figure out if tests are run in tree or not. > Since we'll want to use this library both from net and > drivers/net test targets make the library a target as well, > and automatically include it when net or drivers/net are > included. Making net/lib a target ensures that we end up > with only one copy of it, and saves us some path guessing. > > Add a tiny bit of formatting support to be able to output KTAP > from the start. > > Signed-off-by: Jakub Kicinski <kuba@xxxxxxxxxx> > --- > CC: shuah@xxxxxxxxxx > CC: linux-kselftest@xxxxxxxxxxxxxxx > --- > tools/testing/selftests/Makefile | 7 ++ > tools/testing/selftests/net/lib/Makefile | 8 ++ > .../testing/selftests/net/lib/py/__init__.py | 6 ++ > tools/testing/selftests/net/lib/py/consts.py | 9 ++ > tools/testing/selftests/net/lib/py/ksft.py | 96 +++++++++++++++++++ > tools/testing/selftests/net/lib/py/utils.py | 47 +++++++++ > tools/testing/selftests/net/lib/py/ynl.py | 49 ++++++++++ > 7 files changed, 222 insertions(+) > create mode 100644 tools/testing/selftests/net/lib/Makefile > create mode 100644 tools/testing/selftests/net/lib/py/__init__.py > create mode 100644 tools/testing/selftests/net/lib/py/consts.py > create mode 100644 tools/testing/selftests/net/lib/py/ksft.py > create mode 100644 tools/testing/selftests/net/lib/py/utils.py > create mode 100644 tools/testing/selftests/net/lib/py/ynl.py > > diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile > index e1504833654d..0cffdfb4b116 100644 > --- a/tools/testing/selftests/Makefile > +++ b/tools/testing/selftests/Makefile > @@ -116,6 +116,13 @@ TARGETS += zram > TARGETS_HOTPLUG = cpu-hotplug > TARGETS_HOTPLUG += memory-hotplug > > +# Networking tests want the net/lib target, include it automatically > +ifneq ($(filter net ,$(TARGETS)),) > +ifeq ($(filter net/lib,$(TARGETS)),) > + override TARGETS := $(TARGETS) net/lib > +endif > +endif > + > # User can optionally provide a TARGETS skiplist. By default we skip > # BPF since it has cutting edge build time dependencies which require > # more effort to install. > diff --git a/tools/testing/selftests/net/lib/Makefile b/tools/testing/selftests/net/lib/Makefile > new file mode 100644 > index 000000000000..5730682aeffb > --- /dev/null > +++ b/tools/testing/selftests/net/lib/Makefile > @@ -0,0 +1,8 @@ > +# SPDX-License-Identifier: GPL-2.0 > + > +TEST_FILES := ../../../../../Documentation/netlink/s* > +TEST_FILES += ../../../../net/* > + > +TEST_INCLUDES := $(wildcard py/*.py) > + > +include ../../lib.mk > diff --git a/tools/testing/selftests/net/lib/py/__init__.py b/tools/testing/selftests/net/lib/py/__init__.py > new file mode 100644 > index 000000000000..81a8d14b68f0 > --- /dev/null > +++ b/tools/testing/selftests/net/lib/py/__init__.py > @@ -0,0 +1,6 @@ > +# SPDX-License-Identifier: GPL-2.0 > + > +from .ksft import * > +from .ynl import NlError, YnlFamily, EthtoolFamily, NetdevFamily, RtnlFamily > +from .consts import KSRC > +from .utils import * > diff --git a/tools/testing/selftests/net/lib/py/consts.py b/tools/testing/selftests/net/lib/py/consts.py > new file mode 100644 > index 000000000000..f518ce79d82c > --- /dev/null > +++ b/tools/testing/selftests/net/lib/py/consts.py > @@ -0,0 +1,9 @@ > +# SPDX-License-Identifier: GPL-2.0 > + > +import sys > +from pathlib import Path > + > +KSFT_DIR = (Path(__file__).parent / "../../..").resolve() > +KSRC = (Path(__file__).parent / "../../../../../..").resolve() > + > +KSFT_MAIN_NAME = Path(sys.argv[0]).with_suffix("").name > diff --git a/tools/testing/selftests/net/lib/py/ksft.py b/tools/testing/selftests/net/lib/py/ksft.py > new file mode 100644 > index 000000000000..7c296fe5e438 > --- /dev/null > +++ b/tools/testing/selftests/net/lib/py/ksft.py > @@ -0,0 +1,96 @@ > +# SPDX-License-Identifier: GPL-2.0 > + > +import builtins > +from .consts import KSFT_MAIN_NAME > + > +KSFT_RESULT = None > + > + > +class KsftSkipEx(Exception): > + pass > + > + > +class KsftXfailEx(Exception): > + pass > + > + > +def ksft_pr(*objs, **kwargs): > + print("#", *objs, **kwargs) > + > + > +def ksft_eq(a, b, comment=""): > + global KSFT_RESULT > + if a != b: > + KSFT_RESULT = False > + ksft_pr("Check failed", a, "!=", b, comment) > + > + > +def ksft_true(a, comment=""): > + global KSFT_RESULT > + if not a: > + KSFT_RESULT = False > + ksft_pr("Check failed", a, "does not eval to True", comment) > + > + > +def ksft_in(a, b, comment=""): > + global KSFT_RESULT > + if a not in b: > + KSFT_RESULT = False > + ksft_pr("Check failed", a, "not in", b, comment) > + > + > +def ksft_ge(a, b, comment=""): > + global KSFT_RESULT > + if a < b: > + KSFT_RESULT = False Hmm, instead of this global KSFT_RESULT business, have you considered adding and raising an XsftFailEx, like for the other outcomes? We need to use KSFT_RESULT-like approach in bash tests, because, well, bash. Doing it all through exceptions likely requires consistent use of context managers for resource clean-up. But if we do, we'll get guaranteed cleanups as well. I see that you use __del__ and explicit "finally: del cfg" later on, which is exactly the sort of lifetime management boilerplate that context managers encapsulate. This stuff is going to get cut'n'pasted around, and I worry we'll end up with a mess of mutable globals and forgotten cleanups if the right patterns are not introduced early on. > + ksft_pr("Check failed", a, "<", b, comment) > + > + > +def ktap_result(ok, cnt=1, case="", comment=""): > + res = "" > + if not ok: > + res += "not " > + res += "ok " > + res += str(cnt) + " " > + res += KSFT_MAIN_NAME > + if case: > + res += "." + str(case.__name__) > + if comment: > + res += " # " + comment > + print(res) > + > + > +def ksft_run(cases): > + totals = {"pass": 0, "fail": 0, "skip": 0, "xfail": 0} > + > + print("KTAP version 1") > + print("1.." + str(len(cases))) > + > + global KSFT_RESULT > + cnt = 0 > + for case in cases: > + KSFT_RESULT = True > + cnt += 1 > + try: > + case() > + except KsftSkipEx as e: > + ktap_result(True, cnt, case, comment="SKIP " + str(e)) > + totals['skip'] += 1 > + continue > + except KsftXfailEx as e: > + ktap_result(True, cnt, case, comment="XFAIL " + str(e)) > + totals['xfail'] += 1 > + continue > + except Exception as e: > + for line in str(e).split('\n'): > + ksft_pr("Exception|", line) > + ktap_result(False, cnt, case) > + totals['fail'] += 1 > + continue > + > + ktap_result(KSFT_RESULT, cnt, case) > + totals['pass'] += 1 > + > + print( > + f"# Totals: pass:{totals['pass']} fail:{totals['fail']} xfail:{totals['xfail']} xpass:0 skip:{totals['skip']} error:0" > + ) > diff --git a/tools/testing/selftests/net/lib/py/utils.py b/tools/testing/selftests/net/lib/py/utils.py > new file mode 100644 > index 000000000000..f0d425731fd4 > --- /dev/null > +++ b/tools/testing/selftests/net/lib/py/utils.py > @@ -0,0 +1,47 @@ > +# SPDX-License-Identifier: GPL-2.0 > + > +import json as _json > +import subprocess > + > +class cmd: > + def __init__(self, comm, shell=True, fail=True, ns=None, background=False): > + if ns: > + if isinstance(ns, NetNS): > + ns = ns.name > + comm = f'ip netns exec {ns} ' + comm > + > + self.stdout = None > + self.stderr = None > + self.ret = None > + > + self.comm = comm > + self.proc = subprocess.Popen(comm, shell=shell, stdout=subprocess.PIPE, > + stderr=subprocess.PIPE) > + if not background: > + self.process(terminate=False, fail=fail) > + > + def process(self, terminate=True, fail=None): > + if terminate: > + self.proc.terminate() > + stdout, stderr = self.proc.communicate() > + self.stdout = stdout.decode("utf-8") > + self.stderr = stderr.decode("utf-8") > + self.proc.stdout.close() > + self.proc.stderr.close() > + self.ret = self.proc.returncode > + > + if self.proc.returncode != 0 and fail: > + if len(stderr) > 0 and stderr[-1] == "\n": > + stderr = stderr[:-1] > + raise Exception("Command failed: %s\n%s" % (self.proc.args, stderr)) > + > + > +def ip(args, json=None, ns=None): > + cmd_str = "ip " > + if json: > + cmd_str += '-j ' > + cmd_str += args > + cmd_obj = cmd(cmd_str, ns=ns) > + if json: > + return _json.loads(cmd_obj.stdout) > + return cmd_obj > diff --git a/tools/testing/selftests/net/lib/py/ynl.py b/tools/testing/selftests/net/lib/py/ynl.py > new file mode 100644 > index 000000000000..298bbc6b93c5 > --- /dev/null > +++ b/tools/testing/selftests/net/lib/py/ynl.py > @@ -0,0 +1,49 @@ > +# SPDX-License-Identifier: GPL-2.0 > + > +import sys > +from pathlib import Path > +from .consts import KSRC, KSFT_DIR > +from .ksft import ksft_pr, ktap_result > + > +# Resolve paths > +try: > + if (KSFT_DIR / "kselftest-list.txt").exists(): > + # Running in "installed" selftests > + tools_full_path = KSFT_DIR > + SPEC_PATH = KSFT_DIR / "net/lib/specs" > + > + sys.path.append(tools_full_path.as_posix()) > + from net.lib.ynl.lib import YnlFamily, NlError > + else: > + # Running in tree > + tools_full_path = Path(KSRC) / "tools" > + SPEC_PATH = Path(KSRC) / "Documentation/netlink/specs" I think KSRC is already Path? So it should be enough to just say KSRC / "tools", like above with KSFT_DIR? > + sys.path.append(tools_full_path.as_posix()) > + from net.ynl.lib import YnlFamily, NlError > +except ModuleNotFoundError as e: > + ksft_pr("Failed importing `ynl` library from kernel sources") > + ksft_pr(str(e)) > + ktap_result(True, comment="SKIP") > + sys.exit(4) > + > +# > +# Wrapper classes, loading the right specs > +# Set schema='' to avoid jsonschema validation, it's slow > +# > +class EthtoolFamily(YnlFamily): > + def __init__(self): > + super().__init__((SPEC_PATH / Path('ethtool.yaml')).as_posix(), > + schema='') > + > + > +class RtnlFamily(YnlFamily): > + def __init__(self): > + super().__init__((SPEC_PATH / Path('rt_link.yaml')).as_posix(), > + schema='') > + > + > +class NetdevFamily(YnlFamily): > + def __init__(self): > + super().__init__((SPEC_PATH / Path('netdev.yaml')).as_posix(), > + schema='')