From 7bc1936eded0367aab1a4b9a393ccfd74ab110f7 Mon Sep 17 00:00:00 2001 From: Kirill Isakov Date: Tue, 24 May 2022 22:29:46 +0600 Subject: [PATCH] Add tests for dump commands --- test/integration/cmd_dump.py | 194 ++++++++++++++++++++++++++++++ test/integration/meson.build | 1 + test/integration/testlib/check.py | 7 ++ 3 files changed, 202 insertions(+) create mode 100755 test/integration/cmd_dump.py diff --git a/test/integration/cmd_dump.py b/test/integration/cmd_dump.py new file mode 100755 index 00000000..76186ee6 --- /dev/null +++ b/test/integration/cmd_dump.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 + +"""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 + +SUBNETS_FOO = ("10.0.0.0/16", "10.1.2.0/24") +SUBNETS_BAR = ("10.3.2.0/27", "fe80::/64") +SUBNETS_BROADCAST = len( + ( + "ff:ff:ff:ff:ff:ff owner (broadcast)", + "255.255.255.255 owner (broadcast)", + "224.0.0.0/4 owner (broadcast)", + "ff00::/8 owner (broadcast)", + ) +) +ONLINE_REQUESTS = ( + ("connections",), + ("digraph",), + ("edges",), + ("foobar",), + ("graph",), + ("nodes",), + ("reachable", "nodes"), + ("subnets",), +) + + +def init(ctx: Test) -> Tinc: + """Initialize a node.""" + + node = ctx.node() + stdin = f""" + init {node} + set Port 0 + set Address localhost + set DeviceType dummy + """ + node.cmd(stdin=stdin) + return node + + +def try_dot(src: str) -> None: + """Try passing graph source through the dot binary, if it's present.""" + try: + res = subp.run("dot", input=src, stdout=subp.PIPE, check=True, encoding="utf-8") + check.true(res.stdout) + except FileNotFoundError: + pass + + +def run_offline_tests(command: str, foo: Tinc) -> None: + """Run offline tests.""" + + log.info("dump empty invitations") + out, err = foo.cmd(command, "invitations") + check.false(out) + check.is_in("No outstanding invitations", err) + + for request in ONLINE_REQUESTS: + log.info("dump online type %s", request) + _, err = foo.cmd(command, *request, code=1) + check.is_in("Could not open pid file", err) + + +def dump_pending_invitation(foo: Tinc, bar: Tinc) -> None: + """Test dumping of pending invitations.""" + + log.info("dump %s invitation", bar) + out, _ = foo.cmd("dump", "invitations") + check.lines(out, 1) + file, node = out.strip().split(" ") + check.true(file) + check.equals(node, bar.name) + + +def run_unconnected_tests(foo: Tinc) -> None: + """Run online tests with unconnected nodes.""" + + log.info("dump invalid type") + _, err = foo.cmd("dump", "foobar42", code=1) + check.is_in("Unknown dump type", err) + + log.info("use 'reachable' with wrong command") + _, err = foo.cmd("dump", "reachable", "edges", code=1) + check.is_in("reachable' only supported for nodes", err) + + log.info("check for too many arguments") + _, err = foo.cmd("dump", "edges", "please", code=1) + check.is_in("Invalid number of arguments", err) + + log.info("dump unconnected edges") + out, _ = foo.cmd("dump", "edges") + check.lines(out, 0) + + log.info("dump unconnected subnets") + out, _ = foo.cmd("dump", "subnets") + check.lines(out, SUBNETS_BROADCAST + len(SUBNETS_FOO)) + for sub in SUBNETS_FOO: + check.is_in(sub, out) + + log.info("dump unconnected connections") + out, _ = foo.cmd("dump", "connections") + check.lines(out, 1) + check.is_in("", out) + + log.info("dump unconnected nodes") + for arg in (("nodes",), ("reachable", "nodes")): + out, _ = foo.cmd("dump", *arg) + check.lines(out, 1) + check.is_in(f"{foo} id ", out) + + +def run_connected_tests(foo: Tinc, bar: Tinc) -> None: + """Run online tests with connected nodes.""" + + log.info("dump connected edges") + out, _ = foo.cmd("dump", "edges") + check.lines(out, 2) + check.is_in(f"{foo} to {bar}", out) + check.is_in(f"{bar} to {foo}", out) + + log.info("dump connected connections") + out, _ = foo.cmd("dump", "connections") + check.lines(out, 2) + check.is_in(" at ", out) + check.is_in(f"{bar} at ", out) + + log.info("dump connected subnets") + out, _ = foo.cmd("dump", "subnets") + check.lines(out, SUBNETS_BROADCAST + len(SUBNETS_FOO) + len(SUBNETS_BAR)) + for sub in (*SUBNETS_FOO, *SUBNETS_BAR): + check.is_in(sub, out) + + for kind in "graph", "digraph": + log.info("dump %s", kind) + out, _ = foo.cmd("dump", kind) + check.is_in(f"{kind} {{", out) + try_dot(out) + + log.info("dump connected nodes") + for arg in (("nodes",), ("reachable", "nodes")): + out, _ = foo.cmd("dump", *arg) + check.lines(out, 2) + check.is_in(f"{foo} id ", out) + check.is_in(f"{bar} id ", out) + + +def run_tests(ctx: Test) -> None: + """Run all tests.""" + + foo = init(ctx) + bar = ctx.node() + + log.info("set %s subnets", foo) + for sub in SUBNETS_FOO: + foo.cmd("add", "Subnet", sub) + + for command in "dump", "list": + run_offline_tests(command, foo) + + log.info("start %s", foo) + foo.start() + + log.info("invite %s", bar) + url, _ = foo.cmd("invite", bar.name) + url = url.strip() + + dump_pending_invitation(foo, bar) + + log.info("join %s and set subnets", bar) + bar.cmd("join", url) + bar.cmd("set", "DeviceType", "dummy") + bar.cmd("set", "Port", "0") + for sub in SUBNETS_BAR: + bar.cmd("add", "Subnet", sub) + + run_unconnected_tests(foo) + + log.info("start %s", bar) + foo.add_script(bar.script_up) + bar.cmd("start") + foo[bar.script_up].wait() + + run_connected_tests(foo, bar) + + +with Test("run dump tests") as context: + run_tests(context) diff --git a/test/integration/meson.build b/test/integration/meson.build index 4d2bf99b..2b9f4030 100644 --- a/test/integration/meson.build +++ b/test/integration/meson.build @@ -1,5 +1,6 @@ tests = [ 'basic.py', + 'cmd_dump.py', 'cmd_fsck.py', 'cmd_sign_verify.py', 'commandline.py', diff --git a/test/integration/testlib/check.py b/test/integration/testlib/check.py index 524ca623..a82e0e43 100755 --- a/test/integration/testlib/check.py +++ b/test/integration/testlib/check.py @@ -52,6 +52,13 @@ def in_range(value: Num, gte: Num, lte: Num) -> None: raise ValueError(f"value {value} must be between {gte} and {lte}") +def lines(text: T.AnyStr, num: int) -> None: + """Check that text splits into `num` lines.""" + rows = text.splitlines() + if len(rows) != num: + raise ValueError(f"expected {num} lines, got {len(rows)}: {rows}") + + def is_in(needle: Val, *haystacks: T.Container[Val]) -> None: """Check that at least one haystack includes needle.""" for haystack in haystacks: -- 2.20.1