Skip to content

Table-drive the parse_port_mappings and iptables argv tests #45

Closed

Description

@FAZuH

Part of the whole-repo test-suite audit (2026-09-30). This changes assertion quality, not just test count.

What to build

Two table-driven suites replacing 44 near-identical tests, and assertions strong enough that a wrong rule fails.

Findings

  • 17 parse_port_mappings tests, ~27 lines each at crates/lab-lib/src/docker.rs:512-971. Each builds a 10-line JSON ContainerInspectResponse, calls the parser, and asserts one to three fields. A single table of (name, json, expected: Vec<DockerPortMapRequest>) is about 90 lines. Per the guidelines: use table-driven tests instead of a body full of near-identical setup; prefer DAMP over DRY in tests, but a shared JSON table here is readable rather than clever.
  • 27 args.contains(&token) argv assertions at crates/natmap/src/iptables.rs:784-1122. dnat_args_unspecified_ip_omits_d_flag (:784) asserts only that the tokens -t, DNAT, 8080, 10.0.0.2:80 appear somewhere in the vector; forward_accept_args_includes_ctn_ip_and_port (:839) is four contains calls. A rule built in the wrong order, with the wrong -p, or with the port on the wrong side still passes. Replace with assert_eq!(args, vec![...]), one table per builder (build_dnat_rule_args, build_forward_accept_args, build_masquerade_args, build_output_dnat_args, the hairpin_* builders, cmd_for).

Acceptance criteria

  • Each suite is one table-driven test (or one per builder), not a body containing a loop
  • Every iptables argv assertion is an exact-vector comparison, not a substring check
  • Deliberately corrupting a builder (swap an argument order, drop a flag) makes the new tests fail. Demonstrate this.
  • Per the guidelines: no if, for, while or match inside a test body
  • Test names follow standards.md §3.6: <module_or_function>_<scenario>
  • cargo test -p lab-ops_lab-lib and cargo test -p lab-ops_natmap --lib pass

Blocked by

Estimated: -44 tests, -550 lines.

Activity

  1. added 2 commits that reference this issue on Oct 1, 2026
    de1a891
    0db8006
  2. FAZuH commented on Oct 1, 2026

    @FAZuH
    OwnerAuthor

    Landed on v0.2 as de1a891, merged 0db8006. Closed by hand: a closing keyword only fires from commits reaching the default branch.

    The defect, stated plainly

    dnat_args_unspecified_ip_omits_d_flag asserted only that -t, DNAT, 8080 and 10.0.0.2:80 appear somewhere in the argument vector. forward_accept_args_includes_ctn_ip_and_port was four contains calls. A rule built in the wrong order, with the wrong -p, or with the port on the wrong side passed both. Every argv assertion is now an exact-vector comparison against a literal.

    Eleven corruptions, each shown red and reverted

    build_dnat_rule_args (swap --dport/-j) · build_forward_accept_args (drop --dport) · build_masquerade_args (--dport before -p) · build_output_dnat_args (--dport takes the container port) · build_loopback_masq_args (ipv6 loopback src) · build_static_dnat_prerouting_args (-i after -d) · build_static_dnat_forward_args (FORWARD → DOCKER-USER) · build_snat_args (-s takes ext_ip) · build_hairpin_prerouting_args (--to-destination takes ext_ip) · build_hairpin_postrouting_args (lan_cidr ignored) · cmd_for (families swapped) · plus four variants of parse_port_mappings.

    A real gap the work found

    The output_dnat case inherited 9090/9090 from the original — host port and container port identical — so swapping them was invisible and the corruption passed. The fixture is now 9090/9443 and the corruption fails correctly. The old test could not have caught that either.

    The line count is -187, not the -550 the ticket estimated

    group tests cases lines
    parse_port_mappings 21 → 1 21 → 21 485 → 263, −222
    iptables argv 27 → 11 26 → 25 rows + 2 cmd_for asserts 349 → 384, +35
    total 48 → 12 47 → 47 834 → 647, −187

    The iptables file grew 35 lines and that is the correct outcome. A contains check is about six lines; an exact 22-token vector is the case plus a literal that rustfmt wraps. Reaching −550 would have meant going back to substring assertions, which is the defect this ticket exists to remove.

    The ticket's own counts were also off: it said 17 parse_port_mappings tests where there are 21, and estimated −44 tests where the real merge is −36.

    Coverage held

    47 cases before, 47 after, enumerated name by name. The 21 docker rows map 1:1. The iptables 25 rows cover 25 builder tests, and the two cmd_for tests merged into one test with two assert_eq!, losing no assertion.

    Shape, and why

    A table plus a helper that owns the loop, not a macro_rules! macro: the macro route puts the case name nowhere in the failure message without a #[test] per row, while the helper prints case: <name>. Verified mechanically — no if, for, while or match token survives in any test body once string literals are stripped.

    Split, not tabled, across the 11 argv tests. They do not share a row shape: five builders take a DockerPortMap, three a DnatConfig, two a HairpinConfig, one a SnatConfig, and two return Option where eight return Vec. One table would need a per-builder input constructor column and a sentinel for the Option builders — the same failure mode #46 refused earlier in this batch.

    Verification

    Every hunk sits inside a #[cfg(test)] module: docker.rs hunks run 378..767 against #[cfg(test)] at 288, iptables.rs 730..1101 against one at 720. The only two surviving contains(& are in production delete_by_comment matching a rule's comment string, which is not an argv assertion.

    ./dev.sh test: 427 passed, 0 failed, 10 targets, exit 0, at loadavg 1.61/1.58 before and 2.66/1.85 after. Down 36 from 463, which is exactly 48 tests merged into 12.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-agentFully specified, ready for an AFK agent

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions