diff -Nru ucspi-tcp-0.88/debian/changelog ucspi-tcp-0.88/debian/changelog --- ucspi-tcp-0.88/debian/changelog 2019-01-17 06:30:43.000000000 +0000 +++ ucspi-tcp-0.88/debian/changelog 2023-01-05 02:05:08.000000000 +0000 @@ -1,3 +1,24 @@ +ucspi-tcp (1:0.88-7) unstable; urgency=medium + + [ Dmitry Bogatov ] + * Add Gitlab CI config file + * Do not run tests under Gitlab CI + + [ Peter Pentchev ] + * New maintainer. Closes: #983804 + * Declare compliance with Policy 4.6.2 with no changes. + * Set the debhelper compat level to 13 with no changes. + * Add an EditorConfig definitions file. + * Run a custom test suite at build-time and as an autopkgtest. + * Clean up after a build. + * Show the compiler and linker invocations unless "terse" is requested. + * Do not use "tee" to generate the config files. + * Add Rules-Requires-Root: no to the source control stanza. + * Remove leading slashes from debian/*.install. + * No longer mention ucspi-tcp-doc, no such package for a long time. + + -- Peter Pentchev Thu, 05 Jan 2023 04:05:08 +0200 + ucspi-tcp (1:0.88-6) unstable; urgency=medium * Remove references to patched-out `rbl.maps.vix.com' host from manpage diff -Nru ucspi-tcp-0.88/debian/clean ucspi-tcp-0.88/debian/clean --- ucspi-tcp-0.88/debian/clean 1970-01-01 00:00:00.000000000 +0000 +++ ucspi-tcp-0.88/debian/clean 2023-01-05 02:05:08.000000000 +0000 @@ -0,0 +1,4 @@ +conf-cc +conf-home +conf-ld +ipv6/ diff -Nru ucspi-tcp-0.88/debian/control ucspi-tcp-0.88/debian/control --- ucspi-tcp-0.88/debian/control 2018-12-01 21:52:55.000000000 +0000 +++ ucspi-tcp-0.88/debian/control 2023-01-05 02:05:08.000000000 +0000 @@ -1,18 +1,21 @@ Source: ucspi-tcp Section: net Priority: optional -Maintainer: Dmitry Bogatov +Maintainer: Peter Pentchev Build-Depends: - debhelper-compat (= 11), -Standards-Version: 4.2.1 + debhelper-compat (= 13), + python3 , + python3-netifaces , + python3-utf8-locale , +Standards-Version: 4.6.2 Homepage: http://cr.yp.to/ucspi-tcp Vcs-Git: https://salsa.debian.org/debian/ucspi-tcp.git Vcs-Browser: https://salsa.debian.org/debian/ucspi-tcp +Rules-Requires-Root: no Package: ucspi-tcp Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends} -Replaces: ucspi-tcp-doc Description: command-line tools for building TCP client-server applications tcpserver waits for incoming connections and, for each connection, runs a program of your choice. Your program receives environment variables showing @@ -43,7 +46,6 @@ Package: ucspi-tcp-ipv6 Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends} -Replaces: ucspi-tcp-doc Conflicts: ucspi-tcp Provides: ucspi-tcp Description: command-line tools for building TCP client-server applications (IPv6) diff -Nru ucspi-tcp-0.88/debian/.editorconfig ucspi-tcp-0.88/debian/.editorconfig --- ucspi-tcp-0.88/debian/.editorconfig 1970-01-01 00:00:00.000000000 +0000 +++ ucspi-tcp-0.88/debian/.editorconfig 2023-01-05 02:05:08.000000000 +0000 @@ -0,0 +1,36 @@ +# https://editorconfig.org/ + +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +[*.1] +indent_style = space +indent_size = 4 + +[*.py] +indent_style = space +indent_size = 4 + +[*.pyi] +indent_style = space +indent_size = 4 + +[{changelog,control,copyright,watch}] +indent_style = space +indent_size = 4 + +[rules] +indent_style = tab +tab_size = 8 + +[setup.cfg] +indent_style = space +indent_size = 4 + +[tox.ini] +indent_style = space +indent_size = 2 diff -Nru ucspi-tcp-0.88/debian/.gitlab-ci.yml ucspi-tcp-0.88/debian/.gitlab-ci.yml --- ucspi-tcp-0.88/debian/.gitlab-ci.yml 1970-01-01 00:00:00.000000000 +0000 +++ ucspi-tcp-0.88/debian/.gitlab-ci.yml 2023-01-05 02:05:08.000000000 +0000 @@ -0,0 +1,5 @@ +include: + - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/salsa-ci.yml + - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/pipeline-jobs.yml +variables: + RELEASE: experimental diff -Nru ucspi-tcp-0.88/debian/patches/0005-build-verbose.patch ucspi-tcp-0.88/debian/patches/0005-build-verbose.patch --- ucspi-tcp-0.88/debian/patches/0005-build-verbose.patch 1970-01-01 00:00:00.000000000 +0000 +++ ucspi-tcp-0.88/debian/patches/0005-build-verbose.patch 2023-01-05 02:05:08.000000000 +0000 @@ -0,0 +1,26 @@ +Description: Show the compiler and linker invocations during the build + If the "V" environment variable is set to the value "1", show + the compiler and linker commands before running them. +Forwarded: not-needed +Author: Peter Pentchev +Last-Update: 2023-01-05 + +--- a/Makefile ++++ b/Makefile +@@ -158,6 +158,7 @@ + compile: \ + warn-auto.sh conf-cc + ( cat warn-auto.sh; \ ++ [ '$V' != '1' ] || echo echo "`head -1 conf-cc`" '-c $${1+"$$@"}'; \ + echo exec "`head -1 conf-cc`" '-c $${1+"$$@"}' \ + ) > compile + chmod 755 compile +@@ -384,6 +385,8 @@ + warn-auto.sh conf-ld + ( cat warn-auto.sh; \ + echo 'main="$$1"; shift'; \ ++ [ '$V' != '1' ] || echo echo "`head -1 conf-ld`" \ ++ '-o "$$main" "$$main".o $${1+"$$@"}'; \ + echo exec "`head -1 conf-ld`" \ + '-o "$$main" "$$main".o $${1+"$$@"}' \ + ) > load diff -Nru ucspi-tcp-0.88/debian/patches/series ucspi-tcp-0.88/debian/patches/series --- ucspi-tcp-0.88/debian/patches/series 2018-12-05 15:20:56.000000000 +0000 +++ ucspi-tcp-0.88/debian/patches/series 2023-01-05 02:05:08.000000000 +0000 @@ -2,3 +2,4 @@ 0002-rblsmtpd.c-don-t-use-a-the-default-rbl.maps.vix.com.diff 0003-Makefile-target-choose-do-not-depend-on-conf-home.diff 0004-respect-DESTDIR-variable.patch +0005-build-verbose.patch diff -Nru ucspi-tcp-0.88/debian/rules ucspi-tcp-0.88/debian/rules --- ucspi-tcp-0.88/debian/rules 2018-12-05 15:39:48.000000000 +0000 +++ ucspi-tcp-0.88/debian/rules 2023-01-05 02:05:08.000000000 +0000 @@ -8,16 +8,27 @@ PREFIX_IPV4 = $(CURDIR)/debian/ucspi-tcp PREFIX_IPV6 = $(CURDIR)/debian/ucspi-tcp-ipv6 +ifeq (,$(filter terse,$(DEB_BUILD_OPTIONS))) +export V=1 +endif + %: dh $@ +# The upstream Makefile does not have a 'clean' target, so simulate one. +override_dh_auto_clean: + sed -nre '/^[a-z0-9@_.-]+:/ { s/:.*//; p; }' Makefile | grep -Fxve it -e default | xargs -r rm -f + override_dh_auto_configure: + echo '/usr' > conf-home + echo '$(CC) $(CFLAGS) $(CPPFLAGS)' > conf-cc + echo '$(CC) $(LDFLAGS)' > conf-ld +ifeq ($(DEBEMAIL),) + echo 'int main () { return 0; }' > chkshsgr.c +endif mkdir ipv6 xargs install -t ipv6 < FILES cd ipv6 && patch -p1 < ../debian/ipv6-support.patch - echo '/usr' | tee conf-home ipv6/conf-home - echo '$(CC) $(CFLAGS) $(CPPFLAGS)' | tee ipv6/conf-cc conf-cc - echo '$(CC) $(LDFLAGS)' | tee ipv6/conf-ld conf-ld override_dh_auto_build: $(MAKE) DESTDIR=$(PREFIX_IPV4) @@ -37,5 +48,11 @@ # and `install'. # # So automatic invocation of tests is inhibited, and they are invoked -# manuall at `install' stage. +# manually at `install' stage. +# +# We run our own test suite instead, one that will also be used in +# the autopkgtest later. override_dh_auto_test: + env PYTHONPATH='$(CURDIR)/debian/tests/python' python3 -B -u -m ucspi_tcp_test -d '$(CURDIR)' -p tcp + env PYTHONPATH='$(CURDIR)/debian/tests/python' python3 -B -u -m ucspi_tcp_test -d '$(CURDIR)/ipv6' -p tcp -i 4 + env PYTHONPATH='$(CURDIR)/debian/tests/python' python3 -B -u -m ucspi_tcp_test -d '$(CURDIR)/ipv6' -p tcp -i 6 diff -Nru ucspi-tcp-0.88/debian/tests/control ucspi-tcp-0.88/debian/tests/control --- ucspi-tcp-0.88/debian/tests/control 1970-01-01 00:00:00.000000000 +0000 +++ ucspi-tcp-0.88/debian/tests/control 2023-01-05 02:05:08.000000000 +0000 @@ -0,0 +1,11 @@ +Test-Command: set -e; for py in $(py3versions -i); do printf -- '\n\n====== %s\n\n' "$py"; env PYTHONPATH="$(pwd)/debian/tests/python" "$py" -B -u -m ucspi_tcp_test -d /usr/bin -p tcp; done +Depends: ucspi-tcp, python3-all, python3-netifaces, python3-utf8-locale +Features: test-name=debian-python-v4 + +Test-Command: set -e; for py in $(py3versions -i); do printf -- '\n\n====== %s\n\n' "$py"; env PYTHONPATH="$(pwd)/debian/tests/python" "$py" -B -u -m ucspi_tcp_test -d /usr/bin -p tcp -i 4; done +Depends: ucspi-tcp-ipv6, python3-all, python3-netifaces, python3-utf8-locale +Features: test-name=debian-python-v6-4 + +Test-Command: set -e; for py in $(py3versions -i); do printf -- '\n\n====== %s\n\n' "$py"; env PYTHONPATH="$(pwd)/debian/tests/python" "$py" -B -u -m ucspi_tcp_test -d /usr/bin -p tcp -i 6; done +Depends: ucspi-tcp-ipv6, python3-all, python3-netifaces, python3-utf8-locale +Features: test-name=debian-python-v6 diff -Nru ucspi-tcp-0.88/debian/tests/pyproject.toml ucspi-tcp-0.88/debian/tests/pyproject.toml --- ucspi-tcp-0.88/debian/tests/pyproject.toml 1970-01-01 00:00:00.000000000 +0000 +++ ucspi-tcp-0.88/debian/tests/pyproject.toml 2023-01-05 02:05:08.000000000 +0000 @@ -0,0 +1,38 @@ +[build-system] +requires = ["setuptools >= 61", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "uctest" +version = "0.1.0" +description = "Run some tests for the UCSPI client and server tools" +requires-python = ">= 3.10" +license = {"text" = "public domain"} +urls = {"Source" = "https://salsa.debian.org/debian/ucspi-tcp"} +dynamic = ["dependencies"] + +[[project.authors]] +name = "Peter Pentchev" +email = "roam@debian.org" + +[project.scripts] +uctest_tcp = "ucspi_tcp_test.__main__:main" + +[tool.setuptools] +zip-safe = true +package-dir = {"" = "python"} +packages = ["ucspi_test", "ucspi_tcp_test"] + +[tool.setuptools.dynamic] +dependencies = {file = "requirements/install.txt"} + +[tool.setuptools.package-data] +ucspi_test = ["py.typed"] +ucspi_tcp_test = ["py.typed"] + +[tool.black] +line-length = 100 + +[tool.mypy] +strict = true +python_version = "3.10" diff -Nru ucspi-tcp-0.88/debian/tests/python/ucspi_tcp_test/__main__.py ucspi-tcp-0.88/debian/tests/python/ucspi_tcp_test/__main__.py --- ucspi-tcp-0.88/debian/tests/python/ucspi_tcp_test/__main__.py 1970-01-01 00:00:00.000000000 +0000 +++ ucspi-tcp-0.88/debian/tests/python/ucspi_tcp_test/__main__.py 2023-01-05 02:05:08.000000000 +0000 @@ -0,0 +1,228 @@ +"""Test the TCP implementation of UCSPI.""" + +import argparse +import dataclasses +import enum +import pathlib +import socket + +from typing import Any + +import netifaces +import utf8_locale + +import ucspi_test + + +@dataclasses.dataclass(frozen=True) +class Config(ucspi_test.Config): + """Runtime configuration for the TCP test runner.""" + + listen_addr: str + listen_addr_len: set[int] + # pylint: disable-next=no-member + listen_family: socket.AddressFamily + + +class TcpRunner(ucspi_test.Runner): + """Run ucspi-tcp tests.""" + + def find_listening_address(self) -> list[str]: + """Find a local address/port combination.""" + print(f"{self.proto}.find_listening_address() starting") + for port in range(6502, 8086): + assert isinstance(self.cfg, Config), repr(self.cfg) + addr = self.cfg.listen_addr + sock = socket.socket(self.cfg.listen_family, socket.SOCK_STREAM, socket.IPPROTO_TCP) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + sock.bind((addr, port)) + print(f"- got {port}") + sock.close() + return [addr, str(port)] + except OSError: + pass + + raise ucspi_test.RunnerError(f"Could not find a suitable port on {addr}") + + def get_listening_socket(self, addr: list[str]) -> socket.socket: + assert isinstance(self.cfg, Config), repr(self.cfg) + if len(addr) not in self.cfg.listen_addr_len: + raise ucspi_test.RunnerError( + f"{self.proto}.get_listening_socket(): unexpected address length for {addr!r}" + ) + laddr = addr[0] + try: + lport = int(addr[1]) + except ValueError as err: + raise ucspi_test.RunnerError( + f"{self.proto}.get_listening_socket(): could not convert " + f"{addr[1]!r} to a number: {err}" + ) from err + + try: + sock = socket.socket(self.cfg.listen_family, socket.SOCK_STREAM, socket.IPPROTO_TCP) + except OSError as err: + raise ucspi_test.RunnerError(f"Could not create a TCP socket: {err}") from err + try: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + except OSError as err: + raise ucspi_test.RunnerError( + f"Could not set the 'reuse address' option on a TCP socket: {err}" + ) from err + try: + sock.bind((laddr, lport)) + except OSError as err: + raise ucspi_test.RunnerError( + f"Could not get a TCP socket to bind to {laddr}:{lport}: {err}" + ) from err + try: + sock.listen(5) + except OSError as err: + raise ucspi_test.RunnerError( + f"Could not get a TCP socket to listen on {laddr}:{lport}: {err}" + ) from err + + return sock + + def get_connected_socket(self, addr: list[str]) -> socket.socket: + assert isinstance(self.cfg, Config), repr(self.cfg) + if len(addr) not in self.cfg.listen_addr_len: + raise ucspi_test.RunnerError( + f"{self.proto}.get_connected_socket(): unexpected address length for {addr!r}" + ) + laddr = addr[0] + try: + lport = int(addr[1]) + except ValueError as err: + raise ucspi_test.RunnerError( + f"{self.proto}.get_connected_socket(): could not convert " + f"{addr[1]!r} to a number: {err}" + ) from err + + try: + sock = socket.socket(self.cfg.listen_family, socket.SOCK_STREAM, socket.IPPROTO_TCP) + except OSError as err: + raise ucspi_test.RunnerError(f"Could not create a TCP socket: {err}") from err + try: + sock.connect((laddr, lport)) + except OSError as err: + raise ucspi_test.RunnerError( + f"Could not connect a TCP socket to {laddr}:{lport}: {err}" + ) from err + + return sock + + def format_local_addr(self, addr: list[str]) -> str: + assert isinstance(self.cfg, Config), repr(self.cfg) + assert len(addr) in self.cfg.listen_addr_len, repr(addr) + return f"{addr[0]}:{addr[1]}" + + def format_remote_addr(self, addr: Any) -> str: + assert isinstance(self.cfg, Config), repr(self.cfg) + assert ( + isinstance(addr, tuple) + and len(addr) in self.cfg.listen_addr_len + and isinstance(addr[0], str) + and isinstance(addr[1], int) + ), repr(addr) + return f"{addr[0]}:{addr[1]}" + + +class IPVersion(str, enum.Enum): + """The IP address family for the listening socket.""" + + IPV4 = "4" + IPV6 = "6" + + def __str__(self) -> str: + """Return the string value itself.""" + return self.value + + def addr_len(self) -> set[int]: + """Obtain the expected length of an address/port tuple.""" + match self: + case IPVersion.IPV4: + return {2} + + case IPVersion.IPV6: + return {2, 4} + + # pylint: disable-next=no-member + def family(self) -> socket.AddressFamily: + """Obtain the address family corresponding to this value.""" + match self: + case IPVersion.IPV4: + return socket.AF_INET + + case IPVersion.IPV6: + return socket.AF_INET6 + + +# pylint: disable-next=no-member +def get_listen_address(ip_version: IPVersion) -> tuple[str, set[int], socket.AddressFamily] | None: + """Get a loopback address for the specified address family, if any are configured.""" + # pylint: disable=c-extension-no-member + + ifaces = netifaces.interfaces() + if "lo" not in ifaces: + print("No 'lo' interface at all?!") + return None + + family = ip_version.family() + addrs = netifaces.ifaddresses("lo") + candidates = addrs.get(family) + if not candidates: + print("No addresses for the specified family on the 'lo' interface") + return None + + return candidates[0]["addr"], ip_version.addr_len(), family + + +def parse_args() -> Config | None: + """Parse the command-line arguments.""" + parser = argparse.ArgumentParser(prog="uctest") + + parser.add_argument( + "-d", "--bindir", type=pathlib.Path, required=True, help="the path to the UCSPI utilities" + ) + parser.add_argument( + "-i", + "--ip-version", + type=IPVersion, + default=IPVersion.IPV4, + help="the address family to listen on ('4' for IPv4, '6' for IPv6)", + choices=["4", "6"], + ) + parser.add_argument( + "-p", "--proto", type=str, required=True, help="the UCSPI protocol ('tcp', 'unix', etc)" + ) + args = parser.parse_args() + + listen_data = get_listen_address(args.ip_version) + if listen_data is None: + return None + + return Config( + bindir=args.bindir.absolute(), + listen_addr=listen_data[0], + listen_addr_len=listen_data[1], + listen_family=listen_data[2], + proto=args.proto, + utf8_env=utf8_locale.UTF8Detect().detect().env, + ) + + +def main() -> None: + """Parse command-line arguments, run the tests.""" + cfg = parse_args() + if cfg is None: + print("No loopback interface addresses for the requested family") + return + + ucspi_test.add_handler("tcp", TcpRunner) + ucspi_test.run_test_handler(cfg) + + +if __name__ == "__main__": + main() diff -Nru ucspi-tcp-0.88/debian/tests/python/ucspi_test/__init__.py ucspi-tcp-0.88/debian/tests/python/ucspi_test/__init__.py --- ucspi-tcp-0.88/debian/tests/python/ucspi_test/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ ucspi-tcp-0.88/debian/tests/python/ucspi_test/__init__.py 2023-01-05 02:05:08.000000000 +0000 @@ -0,0 +1,422 @@ +"""Run a couple of UCSPI client and server tests.""" + +import abc +import dataclasses +import pathlib +import shlex +import socket +import subprocess +import sys + +from typing import Any, Callable + + +MSG_RESP_HELLO = "a01 hello\n" +MSG_RESP_BYE = "a02 bye\n" + + +@dataclasses.dataclass(frozen=True) +class Config: + """Runtime configuration for the UCSPI test runner.""" + + bindir: pathlib.Path + proto: str + utf8_env: dict[str, str] + + +class RunnerError(Exception): + """An error that occurred while preparing for or running the tests.""" + + +class Runner(metaclass=abc.ABCMeta): + """A helper class for running tests for a single UCSPI protocol.""" + + _cfg: Config + _proto: str + + def __init__(self, cfg: Config, proto: str) -> None: + """Store the configuration object.""" + self._cfg = cfg + self._proto = proto + + @property + def cfg(self) -> Config: + """Get the configuration for this runner.""" + return self._cfg + + @property + def proto(self) -> str: + """Get the name of the UCSPI protocol to test.""" + return self._proto + + @abc.abstractmethod + def find_listening_address(self) -> list[str]: + """Find an available protocol-specific address to listen on.""" + raise NotImplementedError(f"{type(self).__name__}.find_listening_address() is abstract") + + @abc.abstractmethod + def get_listening_socket(self, addr: list[str]) -> socket.socket: + """Start listening on the specified address.""" + raise NotImplementedError(f"{type(self).__name__}.get_listening_socket() is abstract") + + @abc.abstractmethod + def get_connected_socket(self, addr: list[str]) -> socket.socket: + """Connect to the specified address.""" + raise NotImplementedError(f"{type(self).__name__}.get_connected_socket() is abstract") + + @abc.abstractmethod + def format_local_addr(self, addr: list[str]) -> str: + """Format an address returned by accept(), etc.""" + raise NotImplementedError(f"{type(self).__name__}.format_local_addr() is abstract") + + @abc.abstractmethod + def format_remote_addr(self, addr: Any) -> str: + """Format an address returned by accept(), etc.""" + raise NotImplementedError(f"{type(self).__name__}.format_remote_addr() is abstract") + + def __repr__(self) -> str: + """Provide a Python-esque representation of the helper.""" + return f"{type(self).__name__}(cfg={self._cfg!r}, proto={self._proto!r})" + + +HANDLERS: dict[str, type[Runner]] = {} + + +def with_listener( + runner: Runner, addr: list[str], tag: str, callback: Callable[[socket.socket], None] +) -> None: + """Get a listener socket from the runner, invoke the callback.""" + proto = runner.proto + saddr = runner.format_local_addr(addr) + print(f"- {tag}: setting up a listening socket at {saddr}") + try: + listener = runner.get_listening_socket(addr) + except RunnerError as err: + sys.exit(f"{tag}: could not get a {proto} listening socket: {err}") + + try: + callback(listener) + finally: + try: + listener.close() + except OSError as err: + print(f"{tag}: could not close the listening socket at {saddr}: {err}", file=sys.stderr) + + +def with_connect( + runner: Runner, addr: list[str], tag: str, callback: Callable[[socket.socket], None] +) -> None: + """Ask the runner to connect to the specified address, invoke the callback.""" + proto = runner.proto + saddr = runner.format_local_addr(addr) + print(f"- {tag}: setting up a socket connected to {saddr}") + try: + conn = runner.get_connected_socket(addr) + except RunnerError as err: + sys.exit(f"{tag}: could not get a {proto} connected socket: {err}") + + try: + callback(conn) + finally: + try: + conn.close() + except OSError as err: + print(f"{tag}: could not close the socket connected to {saddr}: {err}", file=sys.stderr) + + +def with_child( + runner: Runner, + tag: str, + cmd: list[str], + callback: Callable[[subprocess.Popen[str]], None], + stderr: int | None = None, +) -> None: + """Spawn a child process, invoke the callback.""" + print(f"- {tag}: starting {shlex.join(cmd)}") + try: + with subprocess.Popen( + cmd, + bufsize=0, + encoding="UTF-8", + env=runner.cfg.utf8_env, + stdout=subprocess.PIPE, + stderr=stderr, + ) as child: + try: + callback(child) + finally: + if child.poll() is None: + ptag = f"the {child.pid} ({cmd[0]}) process" + print(f"- {tag}: {ptag} is still active, killing it") + try: + child.kill() + except OSError as err: + print( + f"{tag}: could not send a kill signal to {ptag}: {err}", file=sys.stderr + ) + print(f"- {tag}: waiting for {ptag} to go away") + try: + child.wait() + except OSError as err: + print(f"{tag}: could not wait for {ptag} to end: {err}", file=sys.stderr) + except (OSError, subprocess.CalledProcessError) as err: + sys.exit(f"Could not spawn {shlex.join(cmd)}: {err}") + + +def test_local_spew(runner: Runner, addr: list[str], tag: str, cmd: list[str]) -> None: + """Test a client program against our listening socket, unidirectional transfer.""" + + def run(listener: socket.socket, catproc: subprocess.Popen[str]) -> None: + """Run the spew test itself.""" + saddr = runner.format_local_addr(addr) + print(f"- started the client as pid {catproc.pid}") + print(f"- waiting for a connection at {saddr}") + try: + conn, rem_addr = listener.accept() + except OSError as err: + sys.exit(f"Could not accept an incoming connection at {saddr}: {err}") + print( + f"- accepted a connection at fd {conn.fileno()} from " + f"{runner.format_remote_addr(rem_addr)}" + ) + + msg = MSG_RESP_HELLO + MSG_RESP_BYE + try: + conn.sendall(msg.encode("UTF-8")) + except OSError as err: + sys.exit(f"Could not send a message on the {conn} connection at {saddr}: {err}") + + print("- closing the incoming connection") + try: + conn.close() + except OSError as err: + sys.exit(f"Could not close the {conn} connection at {saddr}: {err}") + + try: + output, _ = catproc.communicate() + except OSError as err: + sys.exit(f"Could not read the output of the {cmd[0]} process: {err}") + assert isinstance(output, str), repr(output) + + try: + res = catproc.wait() + except OSError as err: + sys.exit(f"Could not wait for the {cmd[0]} process to end: {err}") + print(f"- client exit code: {res}; output: {output!r}") + if res != 0: + sys.exit(f"The {cmd[0]} program exited with non-zero code {res}") + + if output != msg: + sys.exit(f"Expected {msg!r} as {cmd[0]} output, got {catproc.stdout!r}") + + with_listener( + runner, + addr, + tag, + lambda listener: with_child(runner, tag, cmd, lambda catproc: run(listener, catproc)), + ) + print(f"- {cmd[0]} seems fine") + + +def test_local_cat(runner: Runner, addr: list[str]) -> None: + """Test the {proto}cat program.""" + proto = runner.proto + print(f"\n=== Testing {proto}cat") + if runner.cfg.bindir != pathlib.Path("/usr/bin"): + print( + f"- test skipped, {proto}cat will probably not find {runner.cfg.bindir}/{proto}client" + ) + return + + catpath = runner.cfg.bindir / f"{proto}cat" + print(f"- will spawn {proto}cat at {catpath} in a while") + test_local_spew(runner, addr, "test_local_cat", [str(catpath)] + addr) + + +def test_local_client_spew(runner: Runner, addr: list[str]) -> None: + """Test {proto}client against our own listening socket.""" + proto = runner.proto + print(f"\n=== Testing {proto}client against our own listening socket") + + clipath = runner.cfg.bindir / f"{proto}client" + print(f"- will spawn {proto}client at {clipath} in a while") + test_local_spew( + runner, + addr, + "test_local_client_spew", + [str(clipath), "-R", "--"] + addr + ["sh", "-c", "set -e; exec <&6; exec cat"], + ) + + +def test_server_local(runner: Runner, addr: list[str]) -> None: + """Test {proto}server against our own client socket.""" + + def run(srvproc: subprocess.Popen[str], conn: socket.socket) -> None: + """Run the test itself.""" + try: + rem_addr = conn.getpeername() + except OSError as err: + sys.exit(f"getpeername() failed for {conn!r}: {err}") + raddr = runner.format_remote_addr(rem_addr) + print(f"- got a connection at fd {conn.fileno()} to {raddr}") + print("- reading all the data we can") + try: + data = conn.recv(4096) + except OSError as err: + sys.exit(f"Could not read from the {conn!r} socket: {err}") + print(f"- read {len(data)} bytes from the socket") + try: + output = data.decode("UTF-8") + except ValueError as err: + sys.exit(f"Could not decode {data!r} as valid UTF-8: {err}") + if output != message: + sys.exit(f"Expected {message!r}, got {output!r}") + + if srvproc.poll() is not None: + sys.exit( + f"Did not expect the {proto}server process to have exited: " + f"code {srvproc.returncode}" + ) + print(f"- sending a SIGTERM signal to the {proto}server process") + try: + srvproc.terminate() + except OSError as err: + sys.exit( + f"Could not send a SIGTERM signal to the {proto}server process at " + f"{srvproc.pid}: {err}" + ) + print(f"- waiting for the {proto}server process to exit") + res = srvproc.wait() + if res != 0: + sys.exit(f"The {proto}server process exited with a non-zero code {res}") + + def wait_and_connect(srvproc: subprocess.Popen[str]) -> None: + """Wait for the "ready" message from tcpserver.""" + assert srvproc.stderr is not None, repr(srvproc) + print(f"- awaiting the first 'status' line from {proto}server") + line = srvproc.stderr.readline() + if "server: status: 0/" not in line: + sys.exit( + f"Unexpected first line from {proto}server: expected 'status: 0/N', got {line!r}" + ) + with_connect(runner, addr, "test_server_local", lambda conn: run(srvproc, conn)) + + proto = runner.proto + print(f"\n=== Testing {proto}server against our own listening socket") + + srvpath = runner.cfg.bindir / f"{proto}server" + print(f"- will spawn {proto}client at {srvpath} in a while") + message = MSG_RESP_HELLO + MSG_RESP_BYE + with_child( + runner, + "test_server_local", + [str(srvpath), "-vR", "--"] + addr + ["printf", "--", message.replace("\n", "\\n")], + wait_and_connect, + stderr=subprocess.PIPE, + ) + + print(f"- test_server_local: {srvpath} seems fine") + + +def test_server_client_spew(runner: Runner, addr: list[str]) -> None: + """Test {proto}server against {proto}client, unidirectional data transfer.""" + + def run(srvproc: subprocess.Popen[str], cliproc: subprocess.Popen[str]) -> None: + """Read the data received by the client.""" + try: + output, _ = cliproc.communicate() + except OSError as err: + sys.exit(f"Could not read the output of the {proto}client process: {err}") + res = cliproc.poll() + print(f"- client exit code {res}; output {output!r}") + if res is None: + sys.exit(f"Expected the {proto}client process to be done by now") + if res != 0: + sys.exit(f"The {proto}client process exited with a non-zero code {res}") + if output != message: + sys.exit(f"Expected {message!r}, got {output!r}") + + if srvproc.poll() is not None: + sys.exit( + f"Did not expect the {proto}server process to have exited: " + f"code {srvproc.returncode}" + ) + print(f"- sending a SIGTERM signal to the {proto}server process") + try: + srvproc.terminate() + except OSError as err: + sys.exit( + f"Could not send a SIGTERM signal to the {proto}server process at " + f"{srvproc.pid}: {err}" + ) + print(f"- waiting for the {proto}server process to exit") + res = srvproc.wait() + if res != 0: + sys.exit(f"The {proto}server process exited with a non-zero code {res}") + + def wait_and_connect(srvproc: subprocess.Popen[str]) -> None: + """Wait for the "ready" message from tcpserver.""" + assert srvproc.stderr is not None, repr(srvproc) + print(f"- awaiting the first 'status' line from {proto}server") + line = srvproc.stderr.readline() + if "server: status: 0/" not in line: + sys.exit( + f"Unexpected first line from {proto}server: expected 'status: 0/N', got {line!r}" + ) + with_child( + runner, + "test_server_client_spew", + [str(clipath), "-R", "--"] + addr + ["sh", "-c", "set -e; exec <&6; exec cat"], + lambda cliproc: run(srvproc, cliproc), + ) + + proto = runner.proto + print(f"\n=== Testing {proto}server against {proto}client") + + srvpath = runner.cfg.bindir / f"{proto}server" + print(f"- will spawn {proto}server at {srvpath} in a while") + clipath = runner.cfg.bindir / f"{proto}client" + print(f"- will spawn {proto}client at {clipath} in a while") + message = MSG_RESP_HELLO + MSG_RESP_BYE + with_child( + runner, + "test_server_client_spew", + [str(srvpath), "-vR", "--"] + addr + ["printf", "--", message.replace("\n", "\\n")], + wait_and_connect, + stderr=subprocess.PIPE, + ) + + print(f"- {srvpath} seems fine") + + +def run_test(runner: Runner) -> None: + """Run a couple of UCSPI tests.""" + addr = runner.find_listening_address() + + test_local_cat(runner, addr) + test_local_client_spew(runner, addr) + + test_server_local(runner, addr) + test_server_client_spew(runner, addr) + + print(f"\n=== The tests for {runner.cfg.proto} passed") + + +def add_handler(proto: str, runner: type[Runner]) -> None: + """Add a UCSPI protocol test runner.""" + current = HANDLERS.get(proto) + if current is None: + HANDLERS[proto] = runner + elif current != runner: + raise RunnerError( + f"Handler mismatch for the {proto!r} protocol: had {current!r}, now {runner!r}" + ) + + +def run_test_handler(cfg: Config) -> None: + """Parse command-line arguments, run the tests.""" + hprot = HANDLERS.get(cfg.proto) + if hprot is None: + sys.exit(f"Don't know how to test the {cfg.proto!r} UCSPI protocol") + + run_test(hprot(cfg, cfg.proto)) diff -Nru ucspi-tcp-0.88/debian/tests/requirements/install.txt ucspi-tcp-0.88/debian/tests/requirements/install.txt --- ucspi-tcp-0.88/debian/tests/requirements/install.txt 1970-01-01 00:00:00.000000000 +0000 +++ ucspi-tcp-0.88/debian/tests/requirements/install.txt 2023-01-05 02:05:08.000000000 +0000 @@ -0,0 +1,2 @@ +netifaces >= 0.10, < 1 +utf8-locale >= 1, < 2 diff -Nru ucspi-tcp-0.88/debian/tests/setup.cfg ucspi-tcp-0.88/debian/tests/setup.cfg --- ucspi-tcp-0.88/debian/tests/setup.cfg 1970-01-01 00:00:00.000000000 +0000 +++ ucspi-tcp-0.88/debian/tests/setup.cfg 2023-01-05 02:05:08.000000000 +0000 @@ -0,0 +1,2 @@ +[flake8] +max_line_length = 100 diff -Nru ucspi-tcp-0.88/debian/tests/stubs/netifaces.pyi ucspi-tcp-0.88/debian/tests/stubs/netifaces.pyi --- ucspi-tcp-0.88/debian/tests/stubs/netifaces.pyi 1970-01-01 00:00:00.000000000 +0000 +++ ucspi-tcp-0.88/debian/tests/stubs/netifaces.pyi 2023-01-05 02:05:08.000000000 +0000 @@ -0,0 +1,15 @@ +import socket + +from typing import TypedDict + +# Ah well, we can't use NotRequired with Python 3.10, can we now... +class AddressDict(TypedDict): + addr: str + broadcast: str + netmask: str + peer: str + + +def interfaces() -> list[str]: ... + +def ifaddresses(iface: str) -> dict[socket.AddressFamily, list[AddressDict]]: ... diff -Nru ucspi-tcp-0.88/debian/tests/tox.ini ucspi-tcp-0.88/debian/tests/tox.ini --- ucspi-tcp-0.88/debian/tests/tox.ini 1970-01-01 00:00:00.000000000 +0000 +++ ucspi-tcp-0.88/debian/tests/tox.ini 2023-01-05 02:05:08.000000000 +0000 @@ -0,0 +1,60 @@ +[tox] +envlist = + black + pep8 + mypy + pylint + functional +isolated_build = True + +[defs] +pyfiles = + python/ucspi_test + python/ucspi_tcp_test + +[testenv:black] +skip_install = True +deps = + black >= 22, < 23 +commands = + black --check {[defs]pyfiles} + +[testenv:black-reformat] +skip_install = True +deps = + black >= 22, < 23 +commands = + black {[defs]pyfiles} + +[testenv:pep8] +skip_install = True +deps = + flake8 >= 6, < 7 +commands = + flake8 {[defs]pyfiles} + +[testenv:mypy] +skip_install = True +deps = + -r {toxinidir}/requirements/install.txt + mypy >= 0.981 +setenv = + MYPYPATH = {toxinidir}/stubs +commands = + mypy {[defs]pyfiles} + +[testenv:pylint] +skip_install = True +deps = + -r {toxinidir}/requirements/install.txt + pylint >= 2.14, < 2.16 +commands = + pylint {[defs]pyfiles} + +[testenv:functional] +deps = + -r {toxinidir}/requirements/install.txt +setenv = + UCBINDIR = {env:UCBINDIR:{toxinidir}/../..} +commands = + uctest_tcp -d '{env:UCBINDIR}' -p tcp diff -Nru ucspi-tcp-0.88/debian/ucspi-tcp.install ucspi-tcp-0.88/debian/ucspi-tcp.install --- ucspi-tcp-0.88/debian/ucspi-tcp.install 2019-01-17 06:16:28.000000000 +0000 +++ ucspi-tcp-0.88/debian/ucspi-tcp.install 2023-01-05 02:05:08.000000000 +0000 @@ -1 +1 @@ -debian/contrib/httptaild /usr/share/doc/ucspi-tcp/contrib +debian/contrib/httptaild usr/share/doc/ucspi-tcp/contrib diff -Nru ucspi-tcp-0.88/debian/ucspi-tcp-ipv6.install ucspi-tcp-0.88/debian/ucspi-tcp-ipv6.install --- ucspi-tcp-0.88/debian/ucspi-tcp-ipv6.install 2019-01-17 06:16:48.000000000 +0000 +++ ucspi-tcp-0.88/debian/ucspi-tcp-ipv6.install 2023-01-05 02:05:08.000000000 +0000 @@ -1 +1 @@ -debian/contrib/httptaild /usr/share/doc/ucspi-tcp-ipv6/contrib +debian/contrib/httptaild usr/share/doc/ucspi-tcp-ipv6/contrib