42f9d36edf
One file per source area, exercising the pure/isolated parsing and CIDR- arithmetic functions already exposed via headers with no side effects -- parse_port_forward_spec() (protocol suffix parsing/validation, network resolution left to add_port_forward()), resolve_env_specs() (literal and --env-file parsing, ordering, error cases -- a small local RAII ScratchFile helper writes the --env-file fixtures under /tmp), network_subnet.h's CIDR validation/overlap/allocation/address-arithmetic functions, and parse_args() itself against synthetic argv's. Two real bugs found running parse_args() repeatedly in one process (never possible before -- a real invocation only ever calls it once), not assumed: 1. getopt_long's scanning position (`optind`) is process-global and never reset, so a second parse_args() call would silently resume scanning wherever the first one left off. Fixing this alone (optind = 1) wasn't enough on its own, either -- 2. -h/-V return out of the getopt_long loop early (their own `return 0` case), before a call ever completes its scan and lets getopt_long null out its own private `nextchar` pointer -- the *next* parse_args() call then resumed scanning through that stale pointer into the *previous* call's already-destroyed argv strings, misparsing its own fresh argv. glibc documents `optind = 0` (not 1) as the "fully reinitialize private state before rescanning a new argv" signal; switching to it fixed this for good, confirmed by 3 repeated runs each in both random and deterministic (--order lex) Catch2 ordering with zero flakiness either way. Neither bug could ever have surfaced in real usage (parse_args() is only ever called once per process from main()) -- purely a testability gap the new unit tests exposed, now fixed at the source rather than worked around in the test file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
87 lines
3.4 KiB
C++
87 lines
3.4 KiB
C++
// Copyright (C) 2026 Viorel Munteanu
|
|
//
|
|
// This program is free software; you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation; either version 2 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License along
|
|
// with this program; if not, write to the Free Software Foundation, Inc.,
|
|
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
|
|
// [unit] tests for parse_port_forward_spec() (port_forward.h) -- pure
|
|
// syntax/range parsing, no networks/processes involved.
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
|
|
#include "port_forward.h"
|
|
|
|
TEST_CASE("parse_port_forward_spec: bare host:container defaults to tcp, no network", "[unit]") {
|
|
auto spec = parse_port_forward_spec("8080:80");
|
|
REQUIRE(spec.has_value());
|
|
CHECK_FALSE(spec->network.has_value());
|
|
CHECK(spec->host_port == 8080);
|
|
CHECK(spec->container_port == 80);
|
|
CHECK(spec->protocol == PortForwardProtocol::tcp);
|
|
}
|
|
|
|
TEST_CASE("parse_port_forward_spec: network:host:container", "[unit]") {
|
|
auto spec = parse_port_forward_spec("mynet:8080:80");
|
|
REQUIRE(spec.has_value());
|
|
REQUIRE(spec->network.has_value());
|
|
CHECK(*spec->network == "mynet");
|
|
CHECK(spec->host_port == 8080);
|
|
CHECK(spec->container_port == 80);
|
|
}
|
|
|
|
TEST_CASE("parse_port_forward_spec: explicit /tcp suffix", "[unit]") {
|
|
auto spec = parse_port_forward_spec("8080:80/tcp");
|
|
REQUIRE(spec.has_value());
|
|
CHECK(spec->container_port == 80);
|
|
CHECK(spec->protocol == PortForwardProtocol::tcp);
|
|
}
|
|
|
|
TEST_CASE("parse_port_forward_spec: /udp suffix", "[unit]") {
|
|
auto spec = parse_port_forward_spec("8080:80/udp");
|
|
REQUIRE(spec.has_value());
|
|
CHECK(spec->host_port == 8080);
|
|
CHECK(spec->container_port == 80);
|
|
CHECK(spec->protocol == PortForwardProtocol::udp);
|
|
}
|
|
|
|
TEST_CASE("parse_port_forward_spec: network + /udp suffix combined", "[unit]") {
|
|
auto spec = parse_port_forward_spec("mynet:8080:80/udp");
|
|
REQUIRE(spec.has_value());
|
|
REQUIRE(spec->network.has_value());
|
|
CHECK(*spec->network == "mynet");
|
|
CHECK(spec->protocol == PortForwardProtocol::udp);
|
|
}
|
|
|
|
TEST_CASE("parse_port_forward_spec: rejects an unrecognized protocol suffix", "[unit]") {
|
|
CHECK_FALSE(parse_port_forward_spec("8080:80/xyz").has_value());
|
|
}
|
|
|
|
TEST_CASE("parse_port_forward_spec: rejects out-of-range ports", "[unit]") {
|
|
CHECK_FALSE(parse_port_forward_spec("0:80").has_value());
|
|
CHECK_FALSE(parse_port_forward_spec("8080:0").has_value());
|
|
CHECK_FALSE(parse_port_forward_spec("65536:80").has_value());
|
|
CHECK_FALSE(parse_port_forward_spec("8080:65536").has_value());
|
|
}
|
|
|
|
TEST_CASE("parse_port_forward_spec: rejects non-numeric ports", "[unit]") {
|
|
CHECK_FALSE(parse_port_forward_spec("abc:80").has_value());
|
|
CHECK_FALSE(parse_port_forward_spec("8080:abc").has_value());
|
|
CHECK_FALSE(parse_port_forward_spec("80.5:80").has_value());
|
|
}
|
|
|
|
TEST_CASE("parse_port_forward_spec: rejects malformed field counts", "[unit]") {
|
|
CHECK_FALSE(parse_port_forward_spec("8080").has_value());
|
|
CHECK_FALSE(parse_port_forward_spec("").has_value());
|
|
CHECK_FALSE(parse_port_forward_spec("a:b:c:d").has_value());
|
|
}
|