Skip to main content

feature-unit-testing

Use when writing, planning, or improving unit tests for low-level transport or systems code — especially when reasoning about branch coverage, test gaps, identifying which uncovered paths are worth pursuing, or deciding when a feature's test suite is ready to merge.

Quellinformationen

Repository
ROCm/rocm-systems
Letzte Quellaktivität
2. September 2026 um 09:38
Erkannte Sprache von SKILL.md
Englisch
Sterne
516
Forks
423

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

Datei-Explorer
2 Dateien

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
feature-unit-testing
description
Use when writing, planning, or improving unit tests for low-level transport or systems code — especially when reasoning about branch coverage, test gaps, identifying which uncovered paths are worth pursuing, or deciding when a feature's test suite is ready to merge.
# Feature-Centric Unit Testing ## Overview One test = one transport feature. Tests live at the internal API boundary, not at the system level (`ncclAllReduce`). This localizes failures immediately: broken test → broken feature, no stack archaeology. The suite is the acceptance criterion for every PR. **Central discipline:** measure coverage first, plan second. Verify assumptions against real data before writing a single test. ## Modules - [multithread-validation.md](multithread-validation.md) — covering concurrent use of a net plugin: which production threading to reproduce, which concurrency is not worth testing, worker-option parsing, and how to make overlap an assertion. --- ## Feature Testing Lifecycle ``` Feature spec / integration plan │ ▼ DOMAIN 1 — Requirements & Planning • Write test plan at spec time (before any code) • Hardware scope: which cluster, NIC model, GDR, AINIC, QP count • Agree on coverage tier as merge acceptance criterion │ ▼ DOMAIN 2 — Basic Scenarios • Happy path, functional correctness, data integrity • Whitebox: direct internal API calls, scheduler inspection • Typically reaches ~50% line coverage │ ▼ DOMAIN 3 — Bottleneck & Edge Case Discovery • Fault injection, stress, boundary sizes, concurrent connections • Parametric sweeps, async data mutation, multidirectional patterns • Pushes coverage from 50% toward 70–90%+ │ ▼ ACCEPTANCE — Coverage Measurement • Measure → classify gap → add tests → re-measure • below target ──► identify gap ──► add tests ──► re-measure • at target ──► merge ``` **TDD analogy:** test plan = "red" phase written before code exists; Domain 2+3 = "green"; coverage measurement = objective acceptance signal. --- ## Coverage Acceptance Tiers Prefer **branch coverage** as the primary merge criterion. Line coverage is a secondary signal; function coverage is informational only. See the *Why Branch Coverage* section. | Tier | Line coverage | Branch coverage | When to target | |------|:------------:|:---------------:|----------------| | **Basic** | ≥ 50% | ≥ 35% | New feature, first PR — core path exercised | | **Standard** | ≥ 70% | ≥ 50% | Feature complete — error paths and main branches covered | | **Thorough** | ≥ 90% | ≥ 65% | Stable, high-impact — fault injection required | | **Critical** | ≥ 95% | ≥ 80% | Safety-critical: fatal-error handling, data integrity | **Cost of moving between tiers:** Standard→Thorough (branch) requires fault injection and parametric sweeps. Thorough→Critical is expensive — hardware-specific paths and rare races; calculate the realistic branch ceiling for your cluster before committing to this tier. --- ## Domain 1: Requirements & Planning Before writing any test code, create a test plan that defines: 1. **Feature scope** — which internal API surface is being tested, what is explicitly out of scope 2. **Scenario inventory** — happy path, error paths, edge cases, concurrency patterns 3. **Hardware scope** — which NIC model, how many ports, GDR/DMA-buf availability, single-node vs cross-host. Discovering that a test requires unavailable hardware after writing it wastes effort. 4. **Coverage tier** — agree on a branch coverage target (Basic/Standard/Thorough/Critical) as the merge acceptance criterion. This is the "red" phase in TDD: the bar is set before code exists. The plan becomes the PR description's Test Plan section — reviewable alongside code. It is a planning artifact — do not commit it as a file; keep it in the PR body. --- ## Commit Convention **One commit per test.** Each test gets its own atomic commit so reviewers can evaluate test intent and scope individually, and bisect targets a single test on regression. **Title format:** `<subsystem>: add <test name> test` (lowercase, no brackets) ``` net-ib: add test infrastructure (StressTests harness) ← CMakeLists + base fixture net-ib: add InvalidRecvCount test ← repeated ×N (test name verbatim) net-ib: update feature-unit-testing skill ← skill update last ``` **Body (required):** one short paragraph — what the test does, what path it covers, BRDA ref: ``` Call ncclIbIrecv with n=9 (> NCCL_NET_IB_MAX_RECVS=8). Verifies the early-return branch returns ncclInternalError without crashing (net_ib.cc:2731, BRDA:2731,0,1). ``` **What to NOT commit:** - Test plans (`TEST_PLAN.md`) — planning artifact, not part of the test suite - Coverage shell scripts (`run_*.sh`, `merge_coverage.sh`) — operational, not source - Profraw / profdata files — build artifacts --- ## Domain 2: Basic Scenario Patterns ### Whitebox testing — bypass the public API Call internal APIs directly. No `ncclCommInit` overhead. Example: ```cpp // Direct internal call — not through ncclAllReduce IbCastIsend(sendComm, buf, size, tag, mh, nullptr, &req); // Read scheduler state directly to verify internal invariants ncclIbCastGetSchedState(sendComm, &state); EXPECT_GT(state.activeQpTokens[0], 0); ``` Whitebox lets a test assert that WRR correctly redistributed tokens when one link is slow — something a blackbox allreduce test cannot verify. ### Structural patterns (MPI transport tests) **Double-barrier around every send/recv iteration:** ```cpp // rank 0: post recv | rank 1: post send MPI_Barrier(MPI_COMM_WORLD); // after both sides post // both: wait for completion MPI_Barrier(MPI_COMM_WORLD); // after both sides complete ``` Without the second barrier, one rank reuses request slots before the other finishes. **Retry loop for NULL send request (FIFO backpressure is normal):** ```cpp do { ASSERT_EQ(net_->isend(sendComm, data, size, tag, mh, nullptr, &req), ncclSuccess); if (req) break; usleep(10000); } while (true); // Never ASSERT_NE(req, nullptr) immediately after isend. ``` **RAII guard declaration order:** ```cpp NetConnectionGuard connGuard(net_); // 1st — connection auto bufGuard = makeHostBufferAutoGuard(…); // 2nd — buffers NetMHandleGuard mhGuard(mh, …); // 3rd — memory registration // Destruction runs in reverse: MR deregistered before connection closed. ``` **Non-fatal checks in multi-rank helpers** (fan-in, fan-out, all-to-all): ```cpp // Use EXPECT_ (non-fatal), not ASSERT_ (fatal), before any MPI_Barrier. // A fatal assert in rank 0 leaves all other ranks hanging at the barrier. EXPECT_EQ(PostRecv(…), ncclSuccess); // … MPI_Barrier(MPI_COMM_WORLD); // unconditional — always reached ``` **Timeout in poll loops (never spin forever):** ```cpp // Poll loops that wait for async completion MUST have a timeout. // An infinite loop masks the real bug (e.g. unroutable NIC) as "test hung". auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30); while (!comm && std::chrono::steady_clock::now() < deadline) { ncclResult_t r = AcceptConnection(listenComm, &comm); if (r != ncclSuccess) break; } if (!comm) { /* handle timeout — GTEST_SKIP or fail with diagnostic */ } ``` Track the return value of each poll iteration separately from the NULL-comm check. A loop that only checks `!comm` cannot distinguish "still in progress" from "API returned an error 10 iterations ago and will never succeed". **GTEST_SKIP() synchronization across ranks:** ```cpp // When one rank detects a condition requiring SKIP (e.g. QP connect failed), // it MUST broadcast this to all ranks BEFORE calling GTEST_SKIP(). // GTEST_SKIP() does a return — if rank 0 returns, rank 1 hangs on MPI_Recv. int skipFlag = (connectFailed ? 1 : 0); MPI_Allreduce(MPI_IN_PLACE, &skipFlag, 1, MPI_INT, MPI_MAX, MPI_COMM_WORLD); if (skipFlag) { GTEST_SKIP() << "QP connect failed on at least one rank"; } ``` Without the Allreduce, a unilateral `GTEST_SKIP()` in one rank leaves the other rank blocked forever at its next MPI call or poll loop. --- ## Domain 3: Fault Injection **Deliberately break one component — verify the system response.** Compile-guarded (`-DENABLE_FAULT_INJECTION=ON`), zero cost in production: ```cpp ncclIbCastFaultSetQpDelay(comm, qpIdx, delayUs); // artificial latency on one QP ncclIbCastFaultSetQpError(comm, qpIdx, true); // force ncclSystemError ncclIbCastFaultClear(comm); // reset for recovery test ``` **Test categories fault injection enables:** - Fatal event propagation — all QPs faulted simultaneously - Single link failure — one QP faulted, WRR steered away deterministically - Scheduler rebalancing — artificial delay → WRR redistributes tokens - Data integrity under delay — sends complete correctly despite latency - Recovery — fault → clear → fresh connection completes cleanly **When fault injection is the only option:** `wc.status != IBV_WC_SUCCESS` paths in `ncclIbTest`, `fatalErrorCount` threshold branches, async error processing — all show zero hits in coverage until a fault injection hook exists. **Without a compile-time hook:** use LD_PRELOAD to intercept at the dynamic linker level: ```c // libibverbs_mock.so — intercepts ibv_poll_cq, injects bad WC after N calls int ibv_poll_cq(struct ibv_cq *cq, int num, struct ibv_wc *wc) { static int calls = 0; if (++calls == INJECT_AT) { wc->status = IBV_WC_REM_ACCESS_ERR; return 1; } return real_ibv_poll_cq(cq, num, wc); } ``` LD_PRELOAD only works if the target calls the symbol through the PLT. It does **not** work when the code uses `dlopen`/`dlsym` to resolve symbols into private function pointers, or dispatches through a driver ops-struct (e.g. `cq->context->ops.poll_cq`) — both bypass interposition. Check the binary for `dlopen`/`dlsym` usage before planning a shim; if it's there, fall back to a compile-time fault hook instead. --- ## Domain 3: Stress Testing **Resource leak detection:** ``` 100 × (listen → connect → transfer → close) Assert QP / PD / CQ / MR counts return to baseline after every cycle. ``` **Transfer size coverage:** - Minimum: 1-byte messages — exposes framing bugs invisible at larger sizes - Non-powers-of-two: 3, 7, 1023, 4097, 65537 — alignment and boundary conditions - Maximum: up to 64 MB — exercises large MR registration and buffer management **Multidirectional patterns:** allgather, alltoall, hypercube topologies; bidirectional simultaneous send/recv on the same connection; many concurrent connections open simultaneously (stress QP allocation limits). **Async data mutation:** mutate the send buffer *after* posting the send. Verifies the NIC captured data before mutation — detects use-after-post bugs in buffer registration. --- ## Domain 3: Parametric Configuration Matrix One test body swept over a grid of env-var combinations: ``` WRR on/off × QPS count × split threshold × adaptive routing threshold ``` Surfaces interactions between features that per-feature tests miss. Implement with `GTEST_SKIP()` when a required env var is absent: ```cpp const char* v = getenv("NCCL_IB_QPS_PER_CONNECTION"); if (!v) GTEST_SKIP() << "Set NCCL_IB_QPS_PER_CONNECTION to run this test"; ``` --- ## Why Branch Coverage, Not Lines or Functions **Line coverage lies. Branch coverage tells the truth.** A line is "covered" if it executed at least once — regardless of which path through it was taken. A function is "covered" if it was called — regardless of what happened inside. A branch is covered only when **both sides** of a conditional have been exercised. ``` // Line coverage: 100% (line executes in every test) // Branch coverage: 50% (error path never taken) if (result != ncclSuccess) goto fail; // ← "fail" branch: count = 0 ``` **Typical pattern in transport code:** adding a batch of stress tests can leave line coverage essentially flat while moving branch coverage only marginally — because the new tests re-exercise already-covered happy-path lines, while the uncovered side of each decision (the error or hardware-specific path) stays untaken. A healthy-looking line-coverage figure can hide that a large share of decision points are only half-tested. For transport code that gap maps directly to bugs tests cannot catch. **Functions coverage is the least informative metric for systems code.** A function called once with the happy-path arguments appears fully covered even if it contains 20 conditional branches that were never exercised. ### Branch coverage in practice **Use `--branch-coverage` in genhtml:** ```bash genhtml --branch-coverage merged.lcov.info -o html/ # Without this flag, the HTML report does not show per-branch hit counts. ``` **lcov vs llvm-cov branch counts differ:** the two tools report different branch denominators for the same file (llvm-cov includes C++ exception pseudo-branches that lcov's BRDA parsing excludes). Use one tool consistently within a comparison — mixing denominators makes deltas meaningless. lcov/genhtml is preferred for branch-level HTML drill-down. **Read BRDA lines, not DA lines, when hunting gaps:** ``` DA:1234,5 ← line 1234 executed 5 times (line coverage) BRDA:1234,0,0,5 ← block 0, branch 0: taken 5 times (covered) BRDA:1234,0,1,0 ← block 0, branch 1: taken 0 times ← THIS IS THE GAP ``` A line with `DA` count > 0 but a `BRDA` count of 0 on one arm is the exact pattern of "line covered, branch not covered" — the most common source of false confidence. Agree on a branch coverage tier at planning time (see *Coverage Acceptance Tiers* above). Branch coverage ceilings are hardware-dependent — establish a **realistic ceiling** (branches reachable on your hardware) before setting a target. --- ## Coverage Analysis Workflow > **STOP — mandatory before writing any test to cover a gap:** > 1. Export BRDA data and find the exact branch at the target line. > 2. Confirm the count field is `0`. If it is `> 0`, the branch is already covered — do not write a test. > 3. Read ±10 lines of source context to classify the branch (Trivial / Structural / Hardware / Fault injection / Dead). > 4. Only then write or run a test. > > Skipping this check is the single most common cause of zero-delta test additions. ### 1. Get the raw BRDA data ```bash llvm-profdata merge -sparse *.profraw -o merged.profdata llvm-cov export -format=lcov -instr-profile=merged.profdata ./binary \ -sources src/your_file.cc > merged.lcov.info genhtml --branch-coverage merged.lcov.info -o html/ # Extract uncovered branches (count == 0) awk -F: '/^SF:.*your_file/{found=1} found && /^BRDA:/ && $NF==0 \ {print} /^end_of_record/{found=0}' merged.lcov.info | sort -t: -k2 -n ``` ### 2. Read source context around every uncovered line Read ±10 lines. A branch at a given line may be a trivial env-var check or a hardware-only fault path — the line number tells you nothing without context. ### 3. Verify existing tests before claiming a gap **Anti-pattern:** "All tests call `PostRecv(n=1)`, so the `nreqs > 1` path is uncovered." **What actually happened:** `MultiRecv` and `MultiRecvShuffled` already called `PostRecv(n=8)` with shuffled tag order, covering the FIFO slot scan for r > 0. The BRDA count showed > 0. The assumption was wrong. **Rule:** Before adding a test to cover branch X, check the BRDA count for that exact line in the merged profile. If count > 0, it is already covered. --- ## Branch Classification Classify every uncovered branch before deciding whether to test it: | Class | Description | Technique | |-------|-------------|-----------| | **Trivial** | One env var or API param change | Set `NCCL_IB_X=1`, call with `n=9` | | **Structural** | State the harness can't easily create | Cross-host run, `tc qdisc netem delay` | | **Hardware** | Needs specific HW (GDR, >1 NIC, non-loopback) | Different node, or `GTEST_SKIP()` | | **Fault injection** | Needs a mock/shim returning errors | LD_PRELOAD, compile-time hook | | **Dead/unreachable** | Kernel or driver guarantees make it impossible | Document and skip | **Ceiling calculation:** count only Trivial + Structural (with infra) + Fault injection. Never include Dead branches in projected delta. **What moves coverage in net_ib.cc (measured on one cluster — verify on yours):** - Multi-QP split (`NCCL_IB_SPLIT_DATA_ON_QPS=1`) — split alignment branches, QP distribution
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen