The plumbing DiffEngine provides for reviewing inline snapshots: carrying a pending edit from the test process to a reviewer, showing it, and splicing it into the source file once accepted. Verify is the producing side; this page documents the DiffEngine half, for anyone integrating with it — a test library, an IDE plugin, or another review surface.
Five parties, three transports: two loopback ports, stdin for a cold launch, and staged files for when no viewer is available.
flowchart LR
subgraph Test["test process"]
Verify["Verify"] --> Engine["DiffEngine library"]
end
Tray["DiffEngineTray"]
Window["DiffEngineViewer window"]
Plugin["ReSharper / Rider plugin"]
Owner{{"inline queue owner: whoever bound 3493<br/>first — the tray at login, else a viewer"}}
Files[("source files and<br/>staged patch files")]
Engine -->|"3492 moves, deletes (one way),<br/>when a tray is running"| Tray
Engine -->|"3493 inline, settle, and<br/>moves and deletes with no tray"| Owner
Engine -.->|"launch with patch on stdin, or with<br/>a delete, when nothing owns 3493"| Window
Tray <-->|"3493 list, accept, focus"| Owner
Window <-->|"3493 listfull (with the owner's moves<br/>and deletes), accept, discard"| Owner
Plugin -->|"3493 settle, after accepting"| Owner
Owner -->|"InlineApplier"| Files
Plugin -->|"InlineApplier"| Files
The queue of pending snapshots has exactly one owner per session: whichever process bound port
3493 first, decided once and never transferred. When the tray owns it, its edges to the owner
above are in-process calls; when a viewer owns it, the tray drives that viewer over the same
verbs. Either way both hosts run the same InlineQueue implementation, so they cannot disagree
on what accepting or settling means. DiffEngineViewer and
DiffEngineTray cover the two arrangements in detail.
Pending file moves and deletes follow the same rule. They go to the tray when one is running, over the port they have always used, and to the queue owner when one is not — so with no tray installed they are reviewed in the viewer rather than going nowhere. A delete starts a viewer if nothing owns the queue, because it has no second file to compare against and so no diff tool ever opens for it. A move does not: DiffEngine has already opened a diff tool for that file pair.
sequenceDiagram
participant Test as test process
participant Owner as queue owner (3493)
participant Window as viewer window
Test->>Test: literal differs: build InlinePatch
alt something owns 3493 (usually the tray)
Test->>Owner: inline, over the socket
Owner->>Window: focus, launching --attach if no window
else nothing bound, bundled viewer resolves
Test->>Window: launch, patch on stdin
Window->>Window: bind 3493, own the queue
else no viewer, or DiffEngine_InlineViewer=false
Test->>Test: stage received / expected / patch files
end
Window->>Owner: accept
Owner->>Owner: InlineApplier rewrites the literal
Test->>Owner: re-run passes: settle
Owner->>Owner: drop the entry
Nothing touches disk on the happy path: a running owner receives the patch over the socket, a
newly launched viewer receives it on stdin. Staging only happens in the fallback, where the test
library writes three files (Verify: *.received.txt, *.expected.txt, *.inlinepatch) so the
snapshot can still be reviewed by an IDE plugin, a plain text diff tool, or by hand:
DiffEngineViewer --inline --source <file.cs> --line <number> < the.inlinepatch
For the producing side — a test library with a failing inline snapshot:
DiffRunner.AddInlineAsync(patch)queues a patch with whatever owns the port, launching the bundled viewer when nothing does. ReturnsQueued,Disabled(build servers, continuous testing and AI CLIs included), orNoViewerFound— the caller's cue to stage files and fall back to a text diff.DiffRunner.SettleInline(sourceFile, line)drops the pending entry for a call site, for when a previously failing test passes. Unknown entries and an absent owner are no-ops, so call it freely. The settle carries the running framework, so a multi-targeted run only settles its own variant of a conflicted entry.AddInlineAsyncstampspatch.Frameworkwith the running process's target framework ("net9.0", "net48") unless the caller already set it, which is what lets the owner tell a re-run from another framework disagreeing. Callers may also setpatch.TestName, which the viewer uses to group and label the queue; without it, items are labeled by call site.- Setting
DiffEngine_InlineViewertofalsereportsNoViewerFoundwithout probing, which is how a user opts into reviewing in their IDE instead of a window.
InlinePatchFile reads and writes the wire and staging format for a patch. Plain text, content
fields base64 encoded so snapshot text needs no escaping and no JSON dependency:
version: 2
sourceFile: C:\code\project\tests\SampleTests.cs
lineHint: 42
mode: Set
originalExpression: {base64}
newContent: {base64}
testName: {base64}
framework: net9.0
lineHint is a hint: locating the call is content anchored, so a file that shifted since the
test run still patches, and one whose call site changed reports rather than corrupts. mode is
Set (replace or insert the expected argument), Append (add a Snapshot call where none exists
yet), or Remove (delete the call, used when migrating a snapshot back to a file). testName
and framework are optional provenance — who produced the patch and under which target
framework — parsed tolerantly: absent means unknown, and unknown trailing lines are ignored.
The contract for a review surface of its own, which is what the ReSharper / Rider plugin is:
read the staged patch with InlinePatchFile.TryRead, apply it with InlineApplier.Apply, and
honour two rules.
- InlineApplier owns all locking. A per file cross process mutex (up to a ten second wait) plus an in process gate serialise every writer, so applying beside a concurrently accepting tray or viewer is safe, and callers must not add locking of their own. The file's encoding, BOM and line endings are preserved.
- Settle what was applied. The same test run that staged the files may also have queued the
patch with the port owner, and that queue outlives both the window and the run. After
AppliedorAlreadyApplied, callDiffRunner.SettleInline(patch.SourceFile, patch.LineHint)— otherwise the tray keeps offering a snapshot that is already in the source.
Apply returns Applied, AlreadyApplied (the literal already matches), NotFound (the
source changed since the test run — tell the user to re-run rather than retrying), or a failure
with a message (locked file, unreadable source), which is retryable.
Remove mode patches are configuration changes with nothing to review: apply them directly;
AddInlineAsync refuses them.
| Port | Meaning | Protocol |
|---|---|---|
| 3492 | a tray is here | one way payloads: moves and deletes (tray) |
| 3493 | the inline queue owner is here | request/response verbs, internal |
Two ports because they answer different questions: the owner of 3493 is sometimes a viewer, and
a late starting tray still receives every move on 3492 while it is. DiffEngine_ViewerPort
overrides 3493, which test suites use to keep out of the way of a live tray. The 3493 protocol
is internal and versioned; integrate through DiffRunner and InlineApplier rather than
speaking it directly.