DiffEngine
repository·main·Indexed 42 days ago
https://github.com/verifytests/diffengineA foundational component for Snapshot and Approval testing libraries that manages the launching and cleanup of various diff tools across Windows, macOS, and Linux. It supports a wide range of tools including AraxisMerge, BeyondCompare, Meld, KDiff3, and Visual Studio Code, and provides a native macOS renderer for visual diffing.
What's inside DiffEngine
- DiffEngine is a management layer for launching and cleaning up diff tools. It is designed as a foundational component for Snapshot or Approval testing libraries, providing a standardized way to trigger visual or text-based comparisons between expected and actual results.
Review moves and deletes via DiffEngineTray
mainIf
DiffEngineTrayis running, the viewer will also list pending file moves and deletes in the same queue as snapshots.- Moves: Shows the received file against the committed one. Use Accept move or Discard.
- Deletes: Shows the file's content against nothing. Use Accept delete or Discard.
- Bulk Actions:
Accept allwill sweep through snapshots, moves, and deletes, but will still skip conflicted snapshots.
Understand the default Diff Tool resolution order
mainDiffEngine attempts to resolve diff tools in a specific priority order. If multiple tools are installed on your system, DiffEngine will use the first one it finds in its internal priority list. The default order is as follows:
- MsWordDiff (Windows, requires Word)
- MsExcelDiff (Windows, requires Excel)
- BeyondCompare (Windows/OSX/Linux)
- P4Merge (Windows/OSX/Linux)
- Kaleidoscope (OSX)
- DeltaWalker (Windows/OSX)
- WinMerge (Windows)
- TortoiseMerge (Windows)
- TortoiseGitMerge (Windows)
- TortoiseGitIDiff (Windows)
- TortoiseIDiff (Windows)
- KDiff3 (Windows/OSX)
- TkDiff (OSX)
- Guiffy (Windows/OSX)
- ExamDiff (Windows)
- Diffinity (Windows)
- Rider (Windows/OSX/Linux)
- Vim (Windows/OSX)
- Neovim (Windows/OSX/Linux)
- AraxisMerge (Windows/OSX)
- Meld (Windows/OSX/Linux)
- SublimeMerge (Windows/OSX/Linux)
- VisualStudioCode (Windows/OSX/Linux)
- VisualStudio (Windows)
- Cursor (Windows/OSX/Linux)
- DiffEngineViewer (Windows/OSX/Linux)
Understand tool resolution order
mainWhen using
DiffRunner.LaunchAsync(tempFile, targetFile), the engine selects a tool based on the following priority:- Newest First: New tools are added to the top of the resolution list. The most recently added tool that supports the file types will be the one that resolves and launches.
- Explicit Launch: If you want to bypass the automatic resolution logic, you can use the specific
DiffToolinstance returned byAddTool*methods and pass it directly toDiffRunner.LaunchAsync.
// Automatic resolution (uses the most recently added compatible tool) await DiffRunner.LaunchAsync(tempFile, targetFile); // Explicit launch (uses the specific tool instance) var resolvedTool = DiffTools.AddToolBasedOn(...); await DiffRunner.LaunchAsync(resolvedTool!, "PathToTempFile", "PathToTargetFile");How images are compared in DiffEngineViewer
mainDiffEngineViewer compares image files (
.png,.jpg,.jpeg,.gif,.bmp,.webp, and.ico) as pictures rather than text.Key behaviors:
- Comparison Logic: The status line indicates if
images are identical,images differ, oronly <file> exists(for new snapshots). - Metadata Display: Each pane lists the format, pixel dimensions, and byte count. Properties are marked as unchanged or modified based on the comparison.
- Rendering: Images are drawn in the pane on a checkerboard background (to show transparency) and are fitted to the space without being enlarged beyond their original size.
- Format Support: Rendering depends on the platform's native toolkit:
- Windows (GDI+):
.png,.jpg,.jpeg,.gif,.bmp,.ico - macOS (ImageIO): All seven supported formats.
- Linux (raylib):
.png,.jpg,.jpeg,.gif,.bmp
- Windows (GDI+):
- Note: Comparison is based on file extension, not content. A file with a
.pngextension that contains non-PNG data will report its format as unrecognized rather than attempting to render bytes as text.
- Comparison Logic: The status line indicates if
How image rendering and caching work in the macOS renderer
mainThe macOS renderer uses ImageIO instead of
NSImageto ensure it retrieves the file's actual pixels rather than a screen-sized representation.Caching Logic:
- Images are cached and invalidated based on write time and file length. This matches the freshness test used by the managed queue poller.
- If ImageIO fails to read a file, the failure is remembered and nothing is drawn, though the property rows above the image will still display the comparison data.
Capturing Bitmaps:
deview_capturedraws into its own bitmap context rather than using the existing view. This ensures that scale, color space, and font smoothing/subpixel settings are pinned, preventing issues where the capture would otherwise inherit the window's backing scale. This also allows capture to function without a window server, making snapshot tests more stable.
How C# inline snapshots are written
mainWhen DiffEngine generates inline snapshots for C#, it follows specific rules for literal formatting based on the content and the source file's style:
- Form: Single-line content uses regular string literals (escaping
\,", control characters, and\uXXXX). Multi-line content uses raw string literals ("""). Empty content is rendered as"". - Delimiter: Uses three quotes (
""") or one more than the longest run of quotes in the content (e.g.,""""if the content contains"""). - Placement: Raw literals are placed on the line below the opening parenthesis, aligning the opening delimiter with the content. Regular literals stay within the argument list.
- Indentation: Uses the call site's leading whitespace plus one level. The indentation level is determined by the file's most common whitespace pattern (e.g., 2 spaces or 4 spaces).
- Line Endings: Uses the file's dominant line ending to ensure cross-platform compatibility.
To produce identical text in your own tools, use the public
CsStringLiteral.Rendermethod.// single line content await Verify(value).Snapshot("the value"); // multi-line content await Verify(value).Snapshot( """ line one line two """);- Form: Single-line content uses regular string literals (escaping
Using DiffEngineViewer with DiffEngineTray
mainWhen
DiffEngineTrayis running, it acts as the primary owner of the snapshot queue. The viewer interacts with the tray via a socket.Key Behaviors:
- Persistence: The tray holds the queue, meaning snapshots persist even if the viewer is closed, killed, or crashes. This prevents a large process from needing to stay resident just to hold a list.
- Attachment: Use the
--attachflag to connect a viewer to a running tray. The tray will start a viewer automatically whenever a new snapshot arrives if no window is currently open. - Ownership: If a viewer is already running when the tray starts, the viewer retains ownership of the queue and the tray drives it remotely. Ownership does not move between the two.
- Queue Management: The tray's Pending Snapshots group allows you to accept, discard, open a specific snapshot in the viewer, or close the viewer.
- Data Safety: Exiting the queue owner (closing an owning viewer without a tray, or exiting the tray) writes pending inline snapshots to the source project's
obj/VerifyInline/directory. Note that a crash or akillcommand will skip this step and result in the loss of pending file moves and deletes.
Understand initial and detected difference behavior
mainDiffEngine manages how external diff tools are launched based on whether a file is being verified for the first time or if a difference is detected during an ongoing verification.
Initial Verification
When an input is verified for the first time, behavior depends on whether an
EmptyFilesmatch for the current file extension exists:- If an
EmptyFilesmatch is found: The tool launches to compare the input against that empty file. - If no
EmptyFilesmatch is found: No tool is launched.
Detected Differences
When a difference is detected between the input and an existing verified file, the behavior depends on whether a tool is already running:
If no tool is running:
A new tool instance will be launched.
If a tool is already running:
The behavior depends on your
Auto RefreshandMdisettings:Auto Refresh Mdi Behavior truetrueNo action. Current instance will refresh truefalseNo action. Current instance will refresh falsetrueOpen new instance. Previous instance must be manually closed falsefalseKill current and open new instance Note: The bundled
DiffEngineVieweris an exception; it queues failing pairs into a single window and does not follow the logic above.- If an
How conflicting snapshots work
mainA conflict occurs when a test run under multiple target frameworks (e.g.,
net8.0andnet9.0) produces different content for the same call site.- Identification: Conflicted entries are marked with a
*in the pending list. The pane header will indicate the active framework (e.g.,received (net8.0)). - Resolution: Use the
vkey or the Variant button to cycle through the different framework contents. - Accepting: Accepting a variant applies only that specific variant to the source and resolves that call site.
- Accept All: The
Accept allcommand (andShift+A) will skip conflicted entries to avoid picking sides incorrectly. It will report how many conflicted items still require manual review.
- Identification: Conflicted entries are marked with a
Understand Diff Tool launch behavior
mainDiffEngine's behavior when launching tools depends on whether the input is being verified for the first time or if a difference has been detected during an ongoing verification.
Initial Verification
When an input is verified for the first time, DiffEngine looks for an EmptyFiles matching the file extension.
- If found: The tool launches to compare the input against that empty file.
- If not found: No tool is launched.
Detected Differences
If a difference is detected between the input and an existing verified file, behavior depends on whether a tool is already running for that specific test:
If no tool is currently running:
A new tool instance will be launched.
If a tool is already running:
The behavior depends on your
Auto RefreshandMdi(Multiple Document Interface) settings:Auto Refresh Mdi Behavior true true No action. Current instance will refresh true false No action. Current instance will refresh false true Open new instance. Previous instance must be manually closed false false Kill current and open new instance Note: The bundled DiffEngineViewer is an exception; it queues every failing pair into a single window regardless of these settings.
How DiffEngine renders new snapshot literals
mainWhen a patch is accepted, DiffEngine rewrites the source code. The rendering logic follows these rules:
- Form:
- Single-line content becomes a regular literal (with standard escaping).
- Multi-line content becomes a raw string literal (
"""). - Empty content becomes
"".
- Delimiter: Uses three quotes, or one more than the longest run of quotes in the content (e.g.,
""""if the content contains"""). - Placement:
- Raw literals are placed on the line below the opening parenthesis, with the delimiter aligned with the content.
- Regular literals stay within the argument list.
- Indentation: The call line's leading whitespace plus one level. The indentation width is detected from the file (e.g., spaces or tabs).
- Line Endings: Uses the file's dominant line ending. The rest of the file (encoding, BOM, etc.) is preserved byte-for-byte.
- Form: