Re: [PATCH net-next 3/7] selftests: net: add scaffolding for Netlink tests in Python

David Wei
2024-04-03T00:09:05Z

On 2024-04-01 18:05, Jakub Kicinski wrote:
> 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@kernel.org>
> ---
> CC: shuah@kernel.org
> CC: linux-kselftest@vger.kernel.org
> ---
> 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)),)

style nit: consistency in spacing

> + 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 *

style nit: sort alphabetically

> 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
> + 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"
> +
> + 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='')