TENT Testing#
TENT’s runtime does not inspect how a byte moved. It decides from a small
Transport contract: capabilities, a submit Status, and a
TransferStatus (PENDING / COMPLETED / FAILED). That is the
opening for FakeTransport: an in-memory backend that speaks the same
contract, moves no bytes, and lets tests drive the real
TransferEngineImpl with deterministic faults.
This page is about that mechanism. Individual test names and binaries change; the contract does not. Transport-layer tests (RDMA work-request errors, NVLink, GPU registration) stay with each real backend and need hardware.
Why a Fake Transport Works#
Correctness splits into two layers:
Transport-layer — how a backend moves bytes and reports hardware faults. Owned by each
Transport.Runtime-layer — failover, batch lifecycle, progress worker, shutdown. Owned by
TransferEngineImplandProxyManager. The runtime never looks at DMA; it only consumes the contract above.
A real transport produces those values as a side effect of moving data.
A fake that returns the same values at the same times drives
submitTransfer, getTransferStatus, resubmitTransferTask,
progressBatch, and shutdown identically. Failures that are hard to
stage on hardware — a completion that never arrives, the Nth submit
failing, a batch still PENDING when memory is freed — become a
controlled return value.
The production runtime is not stubbed. Tests wrap or replace the transport slots, then exercise the engine as an application would.
Test
|
v
TransferEngineImpl / ProxyManager (unmodified production code)
|
+-- swapTransportForTest(...)
|
v
[optional] fault-injecting decorator
|
v
FakeTransport (programmable Status, no DMA)
The Transport Contract Tests Actually Use#
FakeTransport implements Transport with the minimum surface the
runtime needs:
Capabilities. Advertise
dram_to_dramso the engine considers the slot available. Leave GPU capability bits off; FakeTransport tests register CPU buffers. The runtime will not route a CUDA request to a CPU-only fake.Buffer tagging.
addMemoryBufferrecords the fake’s slot onBufferDesc::transports. Without that tag,resolveTransport()will not pick the swapped-in backend.Submit. Record the request and stamp a
TransferStatus. Submit itself succeeds unless a decorator or subclass fails it.Poll. Return the stamped status, or override it from the poll count (for example
PENDINGfor N polls, thenCOMPLETED).Memory.
malloc/free. No pinning, no GPU registration, no DMA.warmupMemorydeclines so the engine uses its own warmup path.
Two optional hooks cover most status control without subclassing:
stamp a status at submit time
override the status on each
getTransferStatus, given the poll count
Call counts on install / submit / poll / register are how a test proves the runtime took a path (for example primary submitted once, secondary submitted after failover) without looking inside the engine.
Copies of FakeTransport live in the test files that need them, not in a shared header. Each test can extend its copy; divergence is expected. The point of the fake is the contract, not a single implementation.
Fault Injection Is a Decorator#
FakeTransport, by itself, completes successfully. Faults are layered on
top so the engine still sees an ordinary Transport:
Layer |
Role |
|---|---|
FakeTransport |
In-memory success path; programmable completions. |
|
Wraps any |
Test-local subclass |
When the fault is not a policy (poison only some requests; flip an atomic mid-loop). |
The engine’s failover and retry paths run unmodified. The decorator is
what makes a completion-stage FAILED look like a WC error or a dropped
peer, without a verbs layer.
FaultProxyTransport lives under
mooncake-transfer-engine/tent/transport/fault_proxy/. Its policy
fields are the source of truth; this page does not duplicate them.
Installing Fakes Without Bypassing the Runtime#
TransferEngineImpl::swapTransportForTest replaces one slot in
transport_list_ after construct(). That is the only test hook that
installs a fake (or a wrapped fake) while leaving resolveTransport()
and resubmitTransferTask() on the production path. Production code
never calls it.
A typical setup:
Build a config with
p2pmetadata on loopback and real transports disabled, soconstruct()does not require Redis, etcd, or an RDMA device.Construct the engine.
install()the fake (or proxy), thenswapTransportForTestinto the slots under test. Swap only replaces the slot; it does not re-run engine availability.Register memory after the swap, so
addMemoryBuffertags the buffers.Submit and poll through the public engine API.
Install two slots when the scenario needs a fallback (typically RDMA + TCP). Install one slot when the test wants no failover.
USE_CUDA=OFF is required on CPU-only hosts. With CUDA enabled and no
GPU, pointer queries fail, memory type becomes unknown, every transport
looks unavailable, and resolveTransport() returns UNSPEC before any
injected fault runs.
What the Mechanism Can Prove#
FakeTransport proves how the runtime reacts to the transport contract. It cannot prove that a backend moved the right bytes.
In scope. Cross-transport failover and budget exhaustion; poll vs
progressBatch recovery; progress-worker and freeBatch races; queue /
hint / metrics behavior that only depends on submit and poll results;
concurrency of runtime maps that FakeTransport can reach.
Out of scope. DMA integrity; verbs-level failures (WC error, QP
failure); GPU registration; NVLink link faults; true async RPC timing
(in-process callbacks fire synchronously). Those belong to each
transport’s tests, tebench --check_consistency=true on real peers, or
multi-node hardware CI.
Staging is decided before the request reaches a transport.
findStagingPolicy() inspects which hardware backends are installed and
what memory types the request uses, then ProxyManager runs the staging
loop. Swapping FakeTransport into RDMA or TCP does not change that
decision, and every current trigger is gated on NVLink, MNNVL, or TPU —
none of which a CPU-only fake replaces. A CPU-only host therefore cannot
exercise transferEventLoop with either real transports or fakes.
Covering that path is a hardware-runner problem, or a product change to
the staging policy, not a missing FakeTransport feature.
Running#
cmake -S . -B build-tent \
-DUSE_TENT=ON -DUSE_CUDA=OFF \
-DWITH_STORE=OFF -DWITH_STORE_RUST=OFF \
-DBUILD_UNIT_TESTS=ON -DCMAKE_BUILD_TYPE=Release
cmake --build build-tent --target mooncake_common -j
ctest --test-dir build-tent/mooncake-transfer-engine/tent/tests \
--output-on-failure
tent_link_group links mooncake_common by archive path, which does
not create a CMake build-order dependency — build mooncake_common
first. The tent-ci cuda-off legs in GitHub Actions run the same
ctest directory. The cuda-on leg compiles only: runners have no GPU,
and CUDA stubs would bypass the fakes.
Concurrency tests that touch shared runtime maps should also be run
under ThreadSanitizer (-fsanitize=thread). Which binaries those are
belongs in the test sources, not here.