diff mbox series

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

Message ID 20240402010520.1209517-4-kuba@kernel.org (mailing list archive)
State New
Headers show
Series selftests: net: groundwork for YNL-based tests | expand

Commit Message

Jakub Kicinski April 2, 2024, 1:05 a.m. UTC
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

Comments

Petr Machata April 2, 2024, 3:53 p.m. UTC | #1
Jakub Kicinski <kuba@kernel.org> 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@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)),)
> +	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='')
Jakub Kicinski April 2, 2024, 5:26 p.m. UTC | #2
On Tue, 2 Apr 2024 17:53:41 +0200 Petr Machata wrote:
> > +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.

I wanted to support the semantics which the C kselftest harness has,
by which I mean EXPECT and ASSERT. The helpers don't raise, just record
the failure and keep going. ASSERT semantics are provided by the
exceptions.

I thought it may be easier to follow and write correct code if we raise
ASSERTS explicitly in the test, rather than in the helpers. I mean - if 
the programmer has to type in "raise" they are more likely to realize
they need to also add cleanup.

But TBH I'm happy to be persuaded otherwise, I couldn't find a strong
reason to do it one way or the other. I have tried to integrate with
unittest and that wasn't great (I have one huge test I need to port
back now). I don't know if pytest is better, but I decided that we
should probably roll our own. What "our own" exactly is I don't have
strong opinion.
David Wei April 3, 2024, 12:09 a.m. UTC | #3
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='')
diff mbox series

Patch

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