]> tinc-vpn.org Git - tinc/commitdiff
Fix test suite running on Alpine edge
authorGuus Sliepen <guus@sliepen.org>
Fri, 3 Apr 2026 18:35:06 +0000 (20:35 +0200)
committerGuus Sliepen <guus@sliepen.org>
Fri, 3 Apr 2026 18:35:06 +0000 (20:35 +0200)
36 files changed:
test/integration/address_cache.py
test/integration/algorithms.py
test/integration/bind_address.py
test/integration/bind_port.py
test/integration/cmd_dump.py
test/integration/cmd_fsck.py
test/integration/cmd_import.py
test/integration/cmd_join.py
test/integration/cmd_keys.py
test/integration/cmd_misc.py
test/integration/cmd_sign_verify.py
test/integration/commandline.py
test/integration/compression.py
test/integration/device.py
test/integration/device_fd.py
test/integration/device_multicast.py
test/integration/device_raw_socket.py
test/integration/device_tap.py
test/integration/executables.py
test/integration/invite.py
test/integration/invite_tinc_up.py
test/integration/legacy_protocol.py
test/integration/ns_ping.py
test/integration/proxy.py
test/integration/sandbox.py
test/integration/scripts.py
test/integration/security.py
test/integration/splice.py
test/integration/sptps_basic.py
test/integration/systemd.py
test/integration/testlib/proc.py
test/integration/testlib/tinc_cmd.py [new file with mode: 0755]
test/integration/testlib/tinc_log.py [new file with mode: 0755]
test/integration/testlib/tinc_util.py [new file with mode: 0755]
test/integration/testlib/util.py
test/integration/variables.py

index e0b94e3fb2db180c45fe65abb261e11dc0165528..b9dceec77a88a3e896bd3c0b1c71b5a1bfc16eb1 100755 (executable)
@@ -2,15 +2,15 @@
 
 """Test recent address cache."""
 
-import os
-import typing as T
-import shutil
-
 from testlib import check
 from testlib.log import log
 from testlib.proc import Tinc, Script
 from testlib.test import Test
 
+import os
+import typing as T
+import shutil
+
 
 def init(ctx: Test) -> T.Tuple[Tinc, Tinc]:
     """Create test node."""
index b056c7d54974617997fd9ab41a343965806685c5..78876e3d652ef1a51a03099a9adf433ec9876b04 100755 (executable)
@@ -2,13 +2,13 @@
 
 """Check that legacy protocol works with different cryptographic algorithms."""
 
-import typing as T
-
 from testlib.test import Test
 from testlib.proc import Tinc
 from testlib.log import log
 from testlib import cmd, check
 
+import typing as T
+
 
 def init(ctx: Test, digest: str, cipher: str) -> T.Tuple[Tinc, Tinc]:
     """Initialize new test nodes."""
index 8db9998c992e2457efbf824a6573551fe2e8092f..eabc61875fda0f52e7654753d02f5abd8cd717ea 100755 (executable)
@@ -2,17 +2,17 @@
 
 """Test binding to interfaces and addresses."""
 
+from testlib.const import EXIT_SKIP
+from testlib.log import log
+from testlib.test import Test
+from testlib import check, util
+
 import json
 import socket
 import subprocess as subp
 import sys
 import typing as T
 
-from testlib import check, util
-from testlib.const import EXIT_SKIP
-from testlib.log import log
-from testlib.test import Test
-
 util.require_command("ss", "-nlup")
 util.require_command("ip", "--json", "addr")
 
index 5ebaefd0e0404b69d5256d88612357748d39f3e4..c003009e68da956921d803b084592293536654e6 100755 (executable)
@@ -3,15 +3,15 @@
 
 """Test binding to ports on localhost."""
 
-import socket
-import sys
-import typing as T
-
-from testlib import check, util
 from testlib.const import EXIT_SKIP
 from testlib.log import log
 from testlib.proc import Script
 from testlib.test import Test
+from testlib import check, util
+
+import socket
+import sys
+import typing as T
 
 # Call to close opened port
 Closer = T.Callable[[], None]
index 89560166366528970a918981570a1500aafb2ebe..c2519fc84ac5645d42c3e7bc7c82c8786a410cea 100755 (executable)
@@ -2,13 +2,13 @@
 
 """Test dump commands."""
 
-import subprocess as subp
-
 from testlib import check
 from testlib.log import log
 from testlib.proc import Tinc
 from testlib.test import Test
 
+import subprocess as subp
+
 SUBNETS_FOO = ("10.0.0.0/16", "10.1.2.0/24")
 SUBNETS_BAR = ("10.3.2.0/27", "fe80::/64")
 SUBNETS_BROADCAST = len(
index fc8171f2afddaa9bd91cf7a1a48e5a576f6ef2d6..b948da0667a9495b1c2f06017e459dc069553dab 100755 (executable)
@@ -2,16 +2,16 @@
 
 """Test 'tinc fsck' command."""
 
-import os
-import sys
-import typing as T
-
 from testlib import check
 from testlib.const import RUN_ACCESS_CHECKS
 from testlib.log import log
 from testlib.proc import Tinc, Feature
 from testlib.util import read_text, read_lines, write_lines, append_line, write_text
 
+import os
+import sys
+import typing as T
+
 RUN_LEGACY_CHECKS = Feature.LEGACY_PROTOCOL in Tinc().features
 RUN_EXECUTABILITY_CHECKS = os.name != "nt"
 RUN_PERMISSION_CHECKS = RUN_EXECUTABILITY_CHECKS
index 40c258c9f42d6634c104a30b3249212d03e5f9a8..1a440a7cbee72f04727d168878a56bcc8bb4d154 100755 (executable)
@@ -2,14 +2,14 @@
 
 """Test import/export error conditions."""
 
-import os
-
 from testlib import check, cmd, util
 from testlib.log import log
 from testlib.const import RUN_ACCESS_CHECKS
 from testlib.proc import Tinc
 from testlib.test import Test
 
+import os
+
 SEPARATOR = f"#{'-' * 63}#"
 
 MULTI_HOST = f"""
index 2a9f5581486d0b53ca61858d8ba69ebbf7d2e59b..74749f3e4dd9ac98e0df155248a72abfd73e54cc 100755 (executable)
@@ -2,15 +2,15 @@
 
 """Test invite/join error conditions."""
 
-import os
-import shutil
-import socket
-
-from testlib import check, util
 from testlib.log import log
 from testlib.const import RUN_ACCESS_CHECKS
 from testlib.proc import Tinc, Script
 from testlib.test import Test
+from testlib import check, util
+
+import os
+import shutil
+import socket
 
 FAKE_INVITE = "localhost:65535/pVOZMJGm3MqTvTu0UnhMGb2cfuqygiu79MdnERnGYdga5v8C"
 
index 717a84a15c56661275a835108cb60148b553929f..1d6ab988926362d2476a754b75614b4a9ffcb959 100755 (executable)
@@ -3,15 +3,15 @@
 
 """Test key management commands."""
 
-import os
-import typing
-
-from testlib import check, util
 from testlib.log import log
 from testlib.const import RUN_ACCESS_CHECKS
 from testlib.feature import Feature
 from testlib.proc import Tinc
 from testlib.test import Test
+from testlib import check, util
+
+import os
+import typing
 
 TEST_DATA = b"foo bar baz"
 
index 3b7b7295f0422d4a75382220360ac2c2f1a5177c..3a2e723cbbbe7e98cad8c95a2956aacfd878269d 100755 (executable)
@@ -2,14 +2,14 @@
 
 """Test miscellaneous commands."""
 
-import os
-import typing as T
-
 from testlib import check, cmd
 from testlib.log import log
 from testlib.proc import Tinc, Script
 from testlib.test import Test
 
+import os
+import typing as T
+
 SUBNETS_BAR = ("10.20.30.40", "fe80::")
 
 
index e9677b78fb650ac967ff64b5dbef6ce3c3971457..d6e3fc388996dd23a3457d95ecd1fb79e6d50396 100755 (executable)
@@ -2,12 +2,12 @@
 
 """Test sign/verify commands."""
 
-import os
-import tempfile
-
-from testlib import util, cmd, check
 from testlib.proc import Tinc
 from testlib.test import Test
+from testlib import util, cmd, check
+
+import os
+import tempfile
 
 PRIV_KEY = """
 -----BEGIN ED25519 PRIVATE KEY-----
@@ -26,7 +26,9 @@ SIGNED_BYTES = """Signature = foo 1653397516 \
 T8Bjg7dc7IjsCrZQC/20qLRsWPlrbthnjyDHQM0BMLoTeAHbLt0fxP5CbTy7Cifgg7P0K179GeahBFsnaIr4MA\n\
 fake testing data\n\
 hello there\n\
-""".encode("utf-8")
+""".encode(
+    "utf-8"
+)
 
 raw_fd, raw_path = tempfile.mkstemp()
 
index 474a9d36dacaf9e157490b0a8c122f1926b34b85..21972eafd3ea6ba21e707fc3a1c2f8b118d2e760 100755 (executable)
@@ -2,6 +2,12 @@
 
 """Test supported and unsupported commandline flags."""
 
+from testlib.log import log
+from testlib.proc import Tinc, Script
+from testlib.test import Test
+from testlib.feature import SANDBOX_LEVEL
+from testlib import check, util, path
+
 import os
 import shutil
 import signal
@@ -9,12 +15,6 @@ import subprocess as subp
 import tempfile
 import time
 
-from testlib import check, util, path
-from testlib.log import log
-from testlib.proc import Tinc, Script
-from testlib.test import Test
-from testlib.feature import SANDBOX_LEVEL
-
 
 def init(ctx: Test) -> Tinc:
     """Initialize new test nodes."""
index 82667c7895555750d078cb55e9762642987dfdb4..06505f8c8149ed265fc75b93e47161d2a83a2a63 100755 (executable)
@@ -2,6 +2,13 @@
 
 """Test supported and unsupported compression levels."""
 
+from testlib import cmd, path, check, util
+from testlib.log import log
+from testlib.proc import Script, Tinc, Feature
+from testlib.test import Test
+from testlib.template import make_netns_config
+from testlib import external as ext
+
 import os
 import signal
 import sys
@@ -10,12 +17,6 @@ import subprocess as subp
 import time
 import typing as T
 
-from testlib import external as ext, cmd, path, check, util
-from testlib.log import log
-from testlib.proc import Script, Tinc, Feature
-from testlib.test import Test
-from testlib.template import make_netns_config
-
 IP_FOO = "192.168.1.1"
 IP_BAR = "192.168.1.2"
 MASK = 24
index 6c50000f782ceaa1ebbe80a2ba306adfd855b0a1..9375242f717c5dc6a5d5ea679ad0c67e158c5f9a 100755 (executable)
@@ -2,16 +2,16 @@
 
 """Test device configuration variables."""
 
-import os
-import platform
-import typing as T
-
 from testlib import check
 from testlib.feature import Feature
 from testlib.log import log
 from testlib.proc import Script
 from testlib.test import Test
 
+import os
+import platform
+import typing as T
+
 system = platform.system()
 
 
index 1788b0d9f7de24e2aa98127c15941863ed1a5bf7..237e18ec74c377a468e990aaf746a0eb2a685e08 100755 (executable)
@@ -2,17 +2,17 @@
 
 """Test FD device support."""
 
+from testlib import check
+from testlib.log import log
+from testlib.test import Test
+from testlib.proc import Script
+
 import array
 import socket
 import tempfile
 import threading
 import time
 
-from testlib import check
-from testlib.log import log
-from testlib.test import Test
-from testlib.proc import Script
-
 JUNK_FRAME = b"\xff" * 80
 
 
index 97dd168137a585df85f3bc9f07b01c53c46d47e8..7bea1244bbf91fe57a3a34485e50745ef1ed951b 100755 (executable)
@@ -2,6 +2,11 @@
 
 """Test multicast device."""
 
+from testlib.log import log
+from testlib.proc import Tinc, Script
+from testlib.test import Test
+from testlib import check
+
 import enum
 import os
 import select
@@ -9,11 +14,6 @@ import socket
 import struct
 import time
 
-from testlib import check
-from testlib.log import log
-from testlib.proc import Tinc, Script
-from testlib.test import Test
-
 MCAST_ADDR = "224.15.98.12"
 PORT = 38245
 
@@ -33,6 +33,7 @@ def multicast_works() -> MulticastSupport:
 
     try:
         with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as server:
+            server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
             server.bind((MCAST_ADDR, PORT))
 
             req = struct.pack("=4sl", socket.inet_aton(MCAST_ADDR), socket.INADDR_ANY)
@@ -108,7 +109,7 @@ def test_device_multicast(ctx: Test) -> None:
         log.info("multicast blocked")
     else:
         log.info("multicast not supported")
-        test_no_mcast_support(foo)
+        test_no_mcast_support(foo)
 
 
 with Test("test DeviceType = multicast") as context:
index b2bbf1140b67c5d3bed22194ff38ef1067c2e6f9..15b16e058fb408ff16e8308f8b98dd4b5b272ccb 100755 (executable)
@@ -2,23 +2,22 @@
 
 """Test raw socket device support."""
 
-import sys
-import subprocess as subp
-import socket
-
-from testlib import check, util
 from testlib.log import log
 from testlib.const import EXIT_SKIP
 from testlib.proc import Script
 from testlib.test import Test
 from testlib.external import veth_add, move_dev, ping
+from testlib import check, util
+
+import sys
+import subprocess as subp
+import socket
 
 util.require_root()
 util.require_command("ip", "link")
 
 try:
     s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.IPPROTO_IP)
-    print("Success!")
 except PermissionError:
     log.info("This test requires raw socket privileges")
     sys.exit(77)
index 9449ae957c9a08cbacc41865ae5a6f41d66be52b..f42391c86c0834e780bfbe4880f1b2e139600122 100755 (executable)
@@ -2,13 +2,13 @@
 
 """Test TAP device support."""
 
-import typing as T
-
-from testlib import check, util, cmd
 from testlib.log import log
 from testlib.proc import Script, Tinc
 from testlib.test import Test
 from testlib.external import netns_add, netns_exec, ping
+from testlib import check, util, cmd
+
+import typing as T
 
 util.require_root()
 util.require_command("ip", "netns", "list")
index eb78784e2931a5fd9880b20240b1b335bb0c84ce..765b1aa441b7d121c7b58eb9a89b7e6263654ba8 100755 (executable)
@@ -2,11 +2,11 @@
 
 """Basic sanity checks on compiled executables."""
 
-from subprocess import run, PIPE
-
 from testlib import path, check
 from testlib.log import log
 
+from subprocess import run, PIPE
+
 for exe in (
     path.TINC_PATH,
     path.TINCD_PATH,
index 743b4249c28785225ece05b8aba71497d3135627..baba00244016386c152c36fa81f0135aa384fdfd 100755 (executable)
@@ -3,13 +3,13 @@
 
 """Test tinc peer invitations."""
 
-import time
-import subprocess as subp
-
-from testlib import check, util
 from testlib.proc import Tinc
 from testlib.log import log
 from testlib.test import Test
+from testlib import check, util
+
+import time
+import subprocess as subp
 
 
 def run_port0_test(ctx: Test) -> None:
index 0d6db85883a49385dac3ce3d3e3dce00316b6300..0bca72da18d22fc4a0a2c54ba617a012fcab76e7 100755 (executable)
@@ -2,13 +2,13 @@
 
 """Test inviting tinc nodes through tinc-up script."""
 
-import os
-import typing as T
-
-from testlib import check, util
 from testlib.log import log
 from testlib.proc import Tinc, Script
 from testlib.test import Test
+from testlib import check, util
+
+import os
+import typing as T
 
 IFCONFIG = "93.184.216.34/24"
 ROUTES_IPV6 = ("2606:2800:220:1::/64", "2606:2800:220:1:248:1893:25c8:1946")
index 845ac3458288bef15624dc422062b0427691879e..2dcdf6b2ea19f69dbf6539fff1fd0a0db678c094 100755 (executable)
@@ -2,13 +2,13 @@
 
 """Test legacy protocol support (tinc 1.0)."""
 
-import typing as T
-
 from testlib import check, cmd
 from testlib.log import log
 from testlib.proc import Tinc, Script
 from testlib.test import Test
 
+import typing as T
+
 TIMEOUT = 2
 
 
index 758bfeb26b2d5bc8ccfa0120db0422a32f509473..7411b0ed5a4c9e9e165190e774773fed41ef389d 100755 (executable)
@@ -2,13 +2,13 @@
 
 """Create two network namespaces and run ping between them."""
 
-import typing as T
-
-from testlib import external as ext, util, template, cmd
 from testlib.log import log
 from testlib.proc import Tinc, Script
 from testlib.test import Test
 from testlib.external import ping
+from testlib import external as ext, util, template, cmd
+
+import typing as T
 
 util.require_root()
 util.require_command("ip", "netns", "list")
index 3b22704749c8acb9b2994bc5b116f06ad0ee43a7..171892f307e5f03ba9c2235ece778e8c269ce36e 100755 (executable)
@@ -2,6 +2,15 @@
 
 """Test that tincd works through proxies."""
 
+from threading import Thread
+from socketserver import ThreadingMixIn, TCPServer, StreamRequestHandler
+from testlib import check, cmd, path, util
+from testlib.proc import Tinc, Script
+from testlib.test import Test
+from testlib.util import random_string
+from testlib.log import log
+from testlib.feature import HAVE_SANDBOX
+
 import os
 import re
 import time
@@ -12,15 +21,6 @@ import select
 import socket
 import struct
 
-from threading import Thread
-from socketserver import ThreadingMixIn, TCPServer, StreamRequestHandler
-from testlib import check, cmd, path, util
-from testlib.proc import Tinc, Script
-from testlib.test import Test
-from testlib.util import random_string
-from testlib.log import log
-from testlib.feature import HAVE_SANDBOX
-
 USERNAME = random_string(8)
 PASSWORD = random_string(8)
 
index 26f6928684d2f7a4dcb698510a0ad8bbe760e433..09203a631ff03886e432d6c04ab78b70ef876904 100755 (executable)
@@ -2,15 +2,15 @@
 
 """Test that tincd works through proxies."""
 
-import os
-import time
-
 from testlib import check, cmd, path, util
 from testlib.proc import Tinc, Script
 from testlib.test import Test
 from testlib.log import log
 from testlib.feature import HAVE_SANDBOX
 
+import os
+import time
+
 
 def init(ctx: Test, level: str) -> Tinc:
     """Create a new tinc node."""
index 7b1307e2a05b97ba52193574f3e55332cec2da24..8b61b0b8d3c3ed3be001d5673cc7fdb96fca6118 100755 (executable)
@@ -2,15 +2,15 @@
 
 """Test that all tincd scripts execute in correct order and contain expected env vars."""
 
-import os
-import typing as T
-
 from testlib import check, path
 from testlib.log import log
 from testlib.proc import Tinc, Script, ScriptType, TincScript
 from testlib.test import Test
 from testlib.util import random_string
 
+import os
+import typing as T
+
 SUBNET_SERVER = ("10.0.0.1", "fec0::/64")
 SUBNET_CLIENT = ("10.0.0.2", "fec0::/64#5")
 NETNAMES = {
index bfaaa733207d8a93b32d882612b5df9aa0930135..e57e4a3a120c7859919c00cda37094743a1e7bcd 100755 (executable)
@@ -2,15 +2,15 @@
 
 """Test tinc protocol security."""
 
-import asyncio
-import typing as T
-
 from testlib import check
 from testlib.log import log
 from testlib.proc import Tinc, Script
 from testlib.test import Test
 from testlib.feature import SANDBOX_LEVEL
 
+import asyncio
+import typing as T
+
 TIMEOUT = 2
 
 
@@ -124,7 +124,7 @@ async def run_tests(ctx: Test) -> None:
     )
 
 
-loop = asyncio.get_event_loop()
+loop = asyncio.new_event_loop()
 
 with Test("security") as context:
     loop.run_until_complete(run_tests(context))
index ce8136aa5efe1ac91789151c360bf054f1973cc3..8310ea90fe919ece975ff7c81d514bdd34813e97 100755 (executable)
@@ -2,16 +2,16 @@
 
 """Test splicing connection between tinc peers."""
 
-import os
-import subprocess as subp
-import typing as T
-
 from testlib import check, cmd, path
 from testlib.log import log
 from testlib.proc import Tinc, Script
 from testlib.test import Test
 from testlib.feature import SANDBOX_LEVEL
 
+import os
+import subprocess as subp
+import typing as T
+
 
 def init(ctx: Test, *options: str) -> T.Tuple[Tinc, Tinc]:
     """Initialize new test nodes."""
index c5c42cc3db5b2cababc92638bbe4c4b934495281..baddf868fbc5160092279d6e3d0fa1cfb1c7364f 100755 (executable)
@@ -2,13 +2,15 @@
 
 """Test basic SPTPS features."""
 
+from testlib.log import log
+from testlib.proc import Tinc, Script
+from testlib.test import Test
+from testlib import check, util, path
+
 import os
 import subprocess as subp
 import re
 
-from testlib import path, util, check
-from testlib.log import log
-
 port_re = re.compile(r"Listening on (\d+)\.\.\.")
 
 
index 9993a0da0248dcad8fddc9372f4dcc05d0fe1fbf..07349c16f9ce192b704efda37c60290d6df4870e 100755 (executable)
@@ -2,11 +2,6 @@
 
 """Test systemd integration."""
 
-import os
-import socket
-import tempfile
-import time
-
 from testlib import check, path
 from testlib.log import log
 from testlib.feature import Feature
@@ -14,6 +9,11 @@ from testlib.const import MAXSOCKETS
 from testlib.proc import Tinc, Script
 from testlib.test import Test
 
+import os
+import socket
+import tempfile
+import time
+
 
 def tincd_start_socket(foo: Tinc, pass_pid: bool) -> int:
     """Start tincd as systemd socket activation does it."""
index aadafb8bf0099765bcd75612b2cff23664c2d70b..96ec518d414e2ffb8742cc7f5b86071e71f06689 100755 (executable)
@@ -1,5 +1,11 @@
 """Classes for working with compiled instances of tinc and tincd binaries."""
 
+from . import check, path
+from .log import log
+from .script import TincScript, Script, ScriptType
+from .template import make_script, make_cmd_wrap
+from .util import random_string, random_port
+
 import os
 import random
 import tempfile
@@ -8,12 +14,6 @@ import subprocess as subp
 from enum import Enum
 from platform import system
 
-from . import check, path
-from .log import log
-from .script import TincScript, Script, ScriptType
-from .template import make_script, make_cmd_wrap
-from .util import random_string, random_port
-
 # Does the OS support all addresses in 127.0.0.0/8 without additional configuration?
 _FULL_LOCALHOST_SUBNET = system() in ("Linux", "Windows")
 
diff --git a/test/integration/testlib/tinc_cmd.py b/test/integration/testlib/tinc_cmd.py
new file mode 100755 (executable)
index 0000000..3a97fc9
--- /dev/null
@@ -0,0 +1,64 @@
+"""Wrappers for more complicated tinc/tincd commands."""
+
+import typing as T
+
+from . import check
+from .log import log
+from .proc import Tinc
+
+ExchangeIO = T.Tuple[
+    T.Tuple[str, str],
+    T.Tuple[str, str],
+    T.Tuple[str, str],
+]
+
+
+def connect(node0: Tinc, node1: Tinc) -> ExchangeIO:
+    """Exchange configuration between nodes and start
+    them in such an order that `Port 0` works on both sides.
+    """
+    node0.add_script(node1.script_up)
+    node0.start()
+    result = exchange(node0, node1)
+    node1.add_script(node0.script_up)
+    node1.cmd("add", "ConnectTo", node0.name)
+    node1.start()
+    node0[node1.script_up].wait()
+    node1[node0.script_up].wait()
+    return result
+
+
+def exchange(node0: Tinc, node1: Tinc, export_all: bool = False) -> ExchangeIO:
+    """Run `export(-all) | exchange | import` between the passed nodes.
+    `export-all` is used if export_all is set to True.
+    """
+    export_cmd = "export-all" if export_all else "export"
+    log.debug("%s between %s and %s", export_cmd, node0.name, node1.name)
+
+    exp_out, exp_err = node0.cmd(export_cmd)
+    log.debug(
+        'exchange: %s %s returned ("%s", "%s")', export_cmd, node0, exp_out, exp_err
+    )
+    check.is_in("Name =", exp_out)
+
+    xch_out, xch_err = node1.cmd("exchange", stdin=exp_out)
+    log.debug('exchange: exchange %s returned ("%s", "%s")', node1, xch_out, xch_err)
+    check.is_in("Name =", xch_out)
+    check.is_in("Imported ", xch_err)
+
+    imp_out, imp_err = node0.cmd("import", stdin=xch_out)
+    log.debug('exchange: import %s returned ("%s", "%s")', node0, imp_out, imp_err)
+    check.is_in("Imported ", imp_err)
+
+    return (
+        (exp_out, exp_err),
+        (xch_out, xch_err),
+        (imp_out, imp_err),
+    )
+
+
+def get(tinc: Tinc, var: str) -> str:
+    """Get the value of the variable, stripped of whitespace."""
+    assert var
+    stdout, _ = tinc.cmd("get", var)
+    return stdout.strip()
diff --git a/test/integration/testlib/tinc_log.py b/test/integration/testlib/tinc_log.py
new file mode 100755 (executable)
index 0000000..0c39475
--- /dev/null
@@ -0,0 +1,50 @@
+"""Global logger for using in test and tincd scripts."""
+
+import logging
+import os
+import sys
+import typing as T
+from types import TracebackType
+
+from .path import TEST_WD, TEST_NAME
+
+logging.basicConfig(level=logging.DEBUG)
+
+_fmt = logging.Formatter(
+    "%(asctime)s %(name)s %(filename)s:%(lineno)d %(levelname)s %(message)s"
+)
+
+# Where to put log files for this test and nodes started by it
+_log_dir = os.path.join(TEST_WD, "logs")
+
+
+def new_logger(name: str) -> logging.Logger:
+    """Create a new named logger with common logging format.
+    Log entries will go into a separate logfile named 'name.log'.
+    """
+    os.makedirs(_log_dir, exist_ok=True)
+
+    logger = logging.getLogger(name)
+    logger.setLevel(logging.DEBUG)
+
+    file = logging.FileHandler(os.path.join(_log_dir, name + ".log"))
+    file.setFormatter(_fmt)
+    logger.addHandler(file)
+
+    return logger
+
+
+# Main logger used by most tests
+log = new_logger(TEST_NAME)
+
+
+def _exc_hook(
+    ex_type: T.Type[BaseException],
+    base: BaseException,
+    tb_type: T.Optional[TracebackType],
+) -> None:
+    """Logging handler for uncaught exceptions."""
+    log.error("Uncaught exception", exc_info=(ex_type, base, tb_type))
+
+
+sys.excepthook = _exc_hook
diff --git a/test/integration/testlib/tinc_util.py b/test/integration/testlib/tinc_util.py
new file mode 100755 (executable)
index 0000000..aead5b8
--- /dev/null
@@ -0,0 +1,136 @@
+"""Miscellaneous utility functions."""
+
+from . import check
+from .log import log
+from .const import EXIT_SKIP
+
+import os
+import sys
+import subprocess as subp
+import random
+import string
+import socket
+import typing as T
+import tempfile
+from pathlib import Path
+
+_ALPHA_NUMERIC = string.ascii_lowercase + string.digits
+
+
+def random_port() -> int:
+    """Return an unused TCP port in the unprivileged range.
+    Note that this function releases the port before returning, and it can be
+    overtaken by something else before you use it.
+    """
+    while True:
+        port = random.randint(1024, 65535)
+        try:
+            with socket.socket() as sock:
+                sock.bind(("0.0.0.0", port))
+                sock.listen()
+            return port
+        except OSError as ex:
+            log.debug("could not bind to random port %d", port, exc_info=ex)
+
+
+def temp_file(content: str) -> str:
+    """Create a temporary file and write text content into it."""
+    fd, path = tempfile.mkstemp()
+    with os.fdopen(fd, "w", encoding="utf-8") as f:
+        f.write(content)
+    return path
+
+
+def remove_file(path: T.Union[str, Path]) -> bool:
+    """Try to remove file without failing if it does not exist."""
+    try:
+        os.remove(path)
+        return True
+    except FileNotFoundError:
+        return False
+
+
+def random_string(k: int) -> str:
+    """Generate a random alphanumeric string of length k."""
+    return "".join(random.choices(_ALPHA_NUMERIC, k=k))
+
+
+def find_line(filename: str, prefix: str) -> str:
+    """Find a line with the prefix in a text file.
+    Check that only one line matches.
+    """
+    with open(filename, "r", encoding="utf-8") as f:
+        keylines = [line for line in f.readlines() if line.startswith(prefix)]
+    check.equals(1, len(keylines))
+    return keylines[0].rstrip()
+
+
+def require_root() -> None:
+    """Check that test is running with root privileges.
+    Exit with code 77 otherwise.
+    """
+    euid = os.geteuid()
+    if euid:
+        log.info("this test requires root (but running under UID %d)", euid)
+        sys.exit(EXIT_SKIP)
+
+
+def require_command(*args: str) -> None:
+    """Check that command args runs with exit code 0.
+    Exit with code 77 otherwise.
+    """
+    try:
+        if subp.run(args, check=False).returncode == 0:
+            return
+    except FileNotFoundError:
+        pass
+    log.info('this test requires command "%s" to work', " ".join(args))
+    sys.exit(EXIT_SKIP)
+
+
+def require_path(path: str) -> None:
+    """Check that path exists in your file system.
+    Exit with code 77 otherwise.
+    """
+    if not os.path.exists(path):
+        log.warning("this test requires path %s to be present", path)
+        sys.exit(EXIT_SKIP)
+
+
+# Thin wrappers around `with open(...) as f: f.do_something()`
+# Don't do much, besides saving quite a bit of space because of how frequently they're needed.
+
+
+def read_text(path: str) -> str:
+    """Return the text contents of a file."""
+    with open(path, encoding="utf-8") as f:
+        return f.read()
+
+
+def write_text(path: str, text: str) -> str:
+    """Write text to a file, replacing its content. Return the text added."""
+    with open(path, "w", encoding="utf-8") as f:
+        f.write(text)
+    return text
+
+
+def read_lines(path: str) -> T.List[str]:
+    """Read file as a list of lines."""
+    with open(path, encoding="utf-8") as f:
+        return f.read().splitlines()
+
+
+def write_lines(path: str, lines: T.List[str]) -> T.List[str]:
+    """Write text lines to a file, replacing it content. Return the line added."""
+    with open(path, "w", encoding="utf-8") as f:
+        f.write(os.linesep.join(lines))
+        f.write(os.linesep)
+    return lines
+
+
+def append_line(path: str, line: str) -> str:
+    """Append a line to the end of the file. Return the line added."""
+    line = f"{os.linesep}{line}{os.linesep}"
+    with open(path, "a", encoding="utf-8") as f:
+        f.write(line)
+    return line
index 38aaffff63e1d200687a969875d989f23324fe22..aead5b85f49b674b9df8a2c3ee54e2e6d21848ac 100755 (executable)
@@ -1,5 +1,9 @@
 """Miscellaneous utility functions."""
 
+from . import check
+from .log import log
+from .const import EXIT_SKIP
+
 import os
 import sys
 import subprocess as subp
@@ -10,10 +14,6 @@ import typing as T
 import tempfile
 from pathlib import Path
 
-from . import check
-from .log import log
-from .const import EXIT_SKIP
-
 _ALPHA_NUMERIC = string.ascii_lowercase + string.digits
 
 
index 9ecca0666b113dd00e47b1ed7e67e3c05a4ad755..41495b0776f301231a8dd096810656d44aef9e89 100755 (executable)
@@ -3,14 +3,14 @@
 
 """Test tinc and tincd configuration variables."""
 
-import typing as T
-from pathlib import Path
-
 from testlib import check, cmd
 from testlib.log import log
 from testlib.proc import Tinc
 from testlib.test import Test
 
+import typing as T
+from pathlib import Path
+
 bad_subnets = (
     "1.1.1",
     "1:2:3:4:5:",