IPVarch/tests/cases.py
2026-11-04 13:53:35 -05:00

282 lines
14 KiB
Python

# SPDX-License-Identifier: GPL-3.0-or-later
"""IPVarch test cases.
Each case is a dict:
name : id shown on pass/fail
args : argv passed after the binary name
exit : expected exit code (default 0)
must : substrings required in stdout ('stderr:' prefix targets stderr)
must_not : substrings forbidden in stdout
validator : key of a structural validator run on stdout (see runner.py)
repeat : run the case this many times (for random/statistical checks)
"""
def case(name, args, exit=0, must=None, must_not=None, validator=None,
repeat=1, ban_network=None):
return {
"name": name,
"args": args,
"exit": exit,
"must": must or [],
"must_not": must_not or [],
"validator": validator,
"repeat": repeat,
"ban_network": ban_network,
}
GROUPS = [
("info: IPv4", [
case("public class A", ["8.8.8.8"],
must=["Address: 8.8.8.8", "Type: IPv4", "Class: A",
"Scope: Public", "Octets: 8, 8, 8, 8"]),
case("public class C", ["198.140.10.1"],
must=["Class: C", "Scope: Public"]),
case("private class C", ["192.168.1.1"],
must=["Class: C", "Scope: Private (RFC 1918)",
"Octets: 192, 168, 1, 1"]),
case("private class A", ["10.0.0.1"],
must=["Class: A", "Scope: Private (RFC 1918)"]),
case("private class B", ["172.16.254.1"],
must=["Class: B", "Scope: Private (RFC 1918)"]),
case("class D multicast", ["224.0.0.1"],
must=["Class: D", "Scope: Multicast"]),
case("class E reserved", ["240.0.0.1"],
must=["Class: E", "Scope: Reserved"]),
case("loopback", ["127.0.0.1"],
must=["Scope: Loopback"]),
case("this-network zero", ["0.0.0.0"],
must=["Class: 0 (reserved)", "Scope: Reserved (this network)"]),
case("class B public", ["172.32.0.1"],
must=["Class: B", "Scope: Public"]),
case("link-local APIPA", ["169.254.9.9"],
must=["Scope: Link-local (APIPA)"]),
]),
("info: IPv6", [
case("documentation 2001:db8", ["2001:db8::1"],
must=["Scope: Documentation (2001:db8::/32)"]),
case("documentation 3fff", ["3fff:ffff::1"],
must=["Scope: Public (global unicast)"]),
case("documentation 2620:4f", ["2620:4f:0:0:0:0:0:1"],
must=["Scope: Public (global unicast)"]),
case("plain global unicast", ["2610:0:0:0:0:0:0:1"],
must=["Address: 2610::1", "Type: IPv6",
"Groups: 2610 0000 0000 0000 0000 0000 0000 0001"]),
case("link-local", ["fe80::1"],
must=["Address: fe80::1", "Scope: Link-local (fe80::/10)"]),
case("loopback", ["::1"],
must=["Address: ::1", "Scope: Loopback (::1)",
"Groups: 0000 0000 0000 0000 0000 0000 0000 0001"]),
case("unspecified", ["::"],
must=["Address: ::", "Scope: Unspecified (::)",
"Groups: 0000 0000 0000 0000 0000 0000 0000 0000"]),
case("multicast", ["ff02::1"],
must=["Scope: Multicast (ff00::/8)"]),
case("unique local", ["fd12::1"],
must=["Scope: Private (unique local, fc00::/7)"]),
case("embedded IPv4 tail", ["2001:db8::192.168.1.1"],
must=["Address: 2001:db8::c0a8:101",
"Groups: 2001 0db8 0000 0000 0000 0000 c0a8 0101"]),
case("IPv4-mapped", ["::ffff:192.168.1.1"],
must=["Address: ::ffff:c0a8:101",
"Scope: IPv4-mapped (::ffff:0:0/96)"]),
case("no classes in IPv6", ["::1"],
must=["Class: n/a (IPv6 has no address classes)"]),
]),
("canonicalization round-trips", [
case("expanded form compresses", ["2001:0db8:0000:0000:0000:0000:0000:0001"],
must=["Address: 2001:db8::1"]),
case("expanded octets normalize", ["2001:0db8:0000:0000:0000:0000:0000:0001"],
must=["Groups: 2001 0db8 0000 0000 0000 0000 0000 0001"]),
case("run of zeros compressed", ["2001:db8:0:0:0:0:2:1"],
must=["Address: 2001:db8::2:1"]),
case("longest run wins", ["2001:0:0:1:0:0:0:1"],
must=["Address: 2001:0:0:1::1"]),
case("run of one not compressed", ["1:2:3:4:5:6:0:8"],
must=["Address: 1:2:3:4:5:6:0:8"]),
case("full uncompressed round-trip", ["1:2:3:4:5:6:7:8"],
must=["Address: 1:2:3:4:5:6:7:8"]),
case("embedded IPv4 expanded head", ["0:0:0:0:0:ffff:1.2.3.4"],
must=["Address: ::ffff:102:304"]),
case("leading zero group", ["0db8:0000:0000:0000:0000:0000:0000:0001"],
must=["Address: db8::1"]),
]),
("network plan", [
case("/24 plan", ["--plan-network", "192.168.1.0/24"],
must=["IPVarch Network Plan - 192.168.1.0 /24",
"255.255.255.0 /24", "192.168.1.255",
"192.168.1.1 - 192.168.1.254", "254",
"192.168.1.1", "192.168.1.2",
"192.168.1.100 - 192.168.1.254"],
validator="plan"),
case("/8 plan", ["--plan-network", "10.0.0.0/8"],
must=["IPVarch Network Plan - 10.0.0.0 /8",
"255.0.0.0 /8", "10.255.255.255",
"10.0.0.1 - 10.255.255.254", "16777214",
"10.0.0.100 - 10.0.0.254"],
validator="plan"),
case("/16 plan", ["--plan-network", "172.18.0.0/16"],
must=["IPVarch Network Plan - 172.18.0.0 /16",
"255.255.0.0 /16", "172.18.255.255",
"172.18.0.1 - 172.18.255.254", "65534",
"172.18.0.100 - 172.18.0.254"],
validator="plan"),
case("/32 single-host plan", ["--plan-network", "192.168.1.7/32"],
must=["IPVarch Network Plan - 192.168.1.7 /32",
"255.255.255.255 /32", "n/a (single host)",
"192.168.1.7", "1",
"disabled (single-host network)"],
validator="plan"),
case("gateway is first usable", ["--plan-network", "192.168.1.0/24"],
validator="plan-gateway"),
]),
("random networks", [
case("random private /8", ["--random-ipv4network", "private", "/8"],
must=["Network address", "DHCP pool"],
validator="random-private", repeat=3),
case("random private /16", ["--random-ipv4network", "private", "/16"],
validator="random-private", repeat=10),
case("random private /24", ["--random-ipv4network", "private", "/24"],
validator="random-private", repeat=20),
case("random private /32", ["--random-ipv4network", "private", "/32"],
validator="random-private", repeat=5),
case("random public /24", ["--random-ipv4network", "public", "/24"],
validator="random-public", repeat=20),
case("random public /32", ["--random-ipv4network", "public", "/32"],
validator="random-public", repeat=5),
]),
("used networks", [
case("list empty database", ["--list-networks"],
must=["No networks registered yet", "Add one with"]),
case("register private /16", ["--use-network", "10.164.0.0/16",
"Main Homelab Bridge"],
must=["Registered 10.164.0.0/16 as", "Main Homelab Bridge"]),
case("list shows entry", ["--list-networks"],
must=["CIDR", "Label", "Registered", "10.164.0.0/16",
"Main Homelab Bridge"],
validator="table3"),
case("duplicate registration", ["--use-network", "10.164.0.0/16",
"Other Bridge"],
exit=2, must=["stderr:already registered as",
"stderr:Main Homelab Bridge"]),
case("register rejects host bits",
["--use-network", "10.5.0.182/8", "x"], exit=1,
must=["stderr:10.0.0.0/8"]),
case("label too long", ["--use-network", "10.0.0.0/8", "x" * 65],
exit=1, must=["stderr:label too long"]),
case("empty label", ["--use-network", "10.0.0.0/8", ""],
exit=1, must=["stderr:empty label"]),
case("use hidden bad prefix", ["--use-network", "10.0.0.0/40", "x"],
exit=1, must=["stderr:prefix"]),
case("forget registered", ["--forget-network", "10.164.0.0/16"],
must=["Removed 10.164.0.0/16 (was \"Main Homelab Bridge\")"]),
case("plan over registered /20",
["--use-network", "172.16.0.0/20", "Core Office"]),
case("plan shows Used by",
["--plan-network", "172.16.0.0/16"],
must=["Used by", "172.16.0.0/20 (\"Core Office\")"]),
case("plan clean unrelated network",
["--plan-network", "10.0.0.0/24"],
must=["Network address"],
must_not=["Used by"]),
case("forget non-registered", ["--forget-network", "192.0.2.0/24"],
exit=2, must=["stderr:not registered"]),
case("prefix mismatch forget",
["--forget-network", "172.16.0.0/12"],
exit=2, must=["stderr:not registered"]),
case("forget registered /20", ["--forget-network", "172.16.0.0/20"],
must=["Removed 172.16.0.0/20 (was \"Core Office\")"]),
case("plan clean after forget",
["--plan-network", "172.16.0.0/16"],
must=["Network address"],
must_not=["Used by"]),
case("register exclusion /24",
["--use-network", "192.168.55.0/24", "Excluded Net"]),
case("random avoids registered /24",
["--random-ipv4network", "private", "/24"],
validator="random-private", repeat=10,
ban_network="192.168.55.0/24"),
case("register exhaustion block",
["--use-network", "10.0.0.0/8", "All of 10"]),
case("random private /8 exhausted",
["--random-ipv4network", "private", "/8"],
exit=1, must=["stderr:no free network available for private /8"]),
case("forget exhaustion block",
["--forget-network", "10.0.0.0/8"],
must=["Removed 10.0.0.0/8 (was \"All of 10\")"]),
case("random private /8 freed",
["--random-ipv4network", "private", "/8"],
validator="random-private", repeat=2),
]),
("invalid inputs", [
case("use-network missing args", ["--use-network", "10.0.0.0/8"],
exit=1),
case("use-network too many args",
["--use-network", "10.0.0.0/8", "x", "y"], exit=1),
case("forget missing argument", ["--forget-network"], exit=1),
case("list with extra argument", ["--list-networks", "extra"],
exit=1),
case("octet too big", ["256.1.1.1"], exit=1,
must=["stderr:ipvarch: error:"]),
case("too few octets", ["1.2.3"], exit=1,
must=["stderr:ipvarch: error:"]),
case("trailing dot", ["1.2.3.4."], exit=1),
case("empty octet", ["1..2.3"], exit=1),
case("four-digit octet", ["1.2.3.0001"], exit=1),
case("too many groups", ["1:2:3:4:5:6:7:8:9"], exit=1),
case("double ::", ["::1::2"], exit=1),
case("trailing colon", ["1:2:3:4::5:"], exit=1),
case("colon then garbage", [":::g:1"], exit=1),
case("hex overflow group", ["::fffff"], exit=1),
case("embedded IPv4 in head", ["1.2.3.4::"], exit=1),
case("embedded IPv4 not last", ["::1.2.3.4:5"], exit=1),
case("garbage text", ["not an ip at all"], exit=1,
must_not=["Address:"]),
case("info mode rejects mask", ["192.168.1.1/24"], exit=1,
must=["stderr:--plan-network"]),
case("plan missing argument", ["--plan-network"], exit=1),
case("plan unsupported prefix", ["--plan-network", "192.168.1.0/12"], exit=1,
must=["stderr:/8 /16 /24 /32"]),
case("plan host bits set", ["--plan-network", "10.5.0.182/8"], exit=1,
must=["stderr:10.0.0.0/8"]),
case("plan host bits set /16", ["--plan-network", "192.168.1.0/16"], exit=1,
must=["stderr:192.168.0.0/16"]),
case("plan zero network", ["--plan-network", "0.0.0.0/8"], exit=1),
case("plan multicast network", ["--plan-network", "224.0.0.0/24"], exit=1),
case("plan reserved network", ["--plan-network", "250.0.0.0/8"], exit=1),
case("random bad scope", ["--random-ipv4network", "residential", "/24"], exit=1),
case("random bad prefix", ["--random-ipv4network", "private", "/12"], exit=1),
case("random missing argument", ["--random-ipv4network", "private"], exit=1),
]),
("edge & boundaries", [
case("limited broadcast", ["255.255.255.255"],
must=["Class: E", "Scope: Reserved"]),
case("leading zeros accepted", ["010.000.000.001"],
must=["Address: 10.0.0.1", "Octets: 10, 0, 0, 1"]),
case(":: with tail-only IPv4", ["::1.2.3.4"],
must=["Address: ::102:304"]),
case("all-zero long form", ["0:0:0:0:0:0:0:0"], must=["Address: ::"]),
case("full hex range", ["fedc:ba98:7654:3210:fedc:ba98:7654:3210"],
must=["Address: fedc:ba98:7654:3210:fedc:ba98:7654:3210"]),
case("79-char garbage (buffer boundary)", ["a" * 79], exit=1),
case("63-hex-group garbage", ["1:2:3:4:5:6:7:8:9:10:11:12:13:14:15:16"],
exit=1),
case("upper-case IPv6 accepted", ["2001:0DB8::1"],
must=["Address: 2001:db8::1"]),
]),
("usage", [
case("-h", ["-h"], must=["Usage:", "IPVarch"]),
case("--help", ["--help"], must=["Usage:"]),
case("help", ["help"], must=["Usage:"]),
case("no arguments", [], exit=1, must=["Usage:"]),
case("usage examples are plannable", ["--help"],
validator="usage-examples"),
]),
]