DVUI

snapshot

Runs exactly one frame, creating a hash of the state of that frame and compares to an earilier saved hash, returning an error if they are not the same.

IMPORTANT: Snapshots are unstable and both backend and platform dependent. Changing any of these might fail the test.

All snapshot tests can be ignored (without skipping the whole test) by setting the environment variable DVUI_SNAPSHOT_IGNORE.

Set the environment variable DVUI_SNAPSHOT_WRITE to create/overwrite the snapshot files

To generate and image of the snapshot for debugging pass -Dsnapshot-images with a suffix like "before" or "after". The images will be places in a images directory next to the snapshot files in question

Dvui does not clear out old or unused snapshot files. To clean the snapshot directory follow these steps:

  1. Ensure all snapshot test pass
  2. Delete the snapshot directory
  3. Run all snapshot tests with DVUI_SNAPSHOT_WRITE set to recreate only the used files

Parameters

#
self:*Self
*Self
src:std.builtin.SourceLocation
std.builtin.SourceLocation
frame:dvui.App.frameFunction
dvui.App.frameFunction

Source

Implementation

#
pub fn snapshot(self: *Self, src: std.builtin.SourceLocation, frame: dvui.App.frameFunction) !void {
    if (should_ignore_snapshots()) {
        _ = try step(frame);
        return;
    }

    defer self.snapshot_index += 1;
    const filename = try std.fmt.allocPrint(self.allocator, "{s}-{s}-{d}", .{ src.file, src.fn_name, self.snapshot_index });
    defer self.allocator.free(filename);
    // NOTE: do fs operation through cwd to handle relative and absolute paths
    var dir = std.Io.Dir.cwd().openDir(self.snapshot_dir, .{}) catch |err| switch (err) {
        error.FileNotFound => {
            std.debug.print("{s}:{d}:{d}: Snapshot directory did not exist! Run the test with DVUI_SNAPSHOT_WRITE to create all snapshot files\n", .{ src.file, src.line, src.column });
            return error.MissingSnapshotFile;
        },
        else => return err,
    };
    defer dir.close();

    widget_hasher = .init();
    defer widget_hasher = null;

    if (@import("build_options").snapshot_image_suffix) |image_suffix| {
        const image_name = try std.fmt.allocPrint(self.allocator, "images/{s}-{s}.png", .{ filename, image_suffix });
        defer self.allocator.free(image_name);
        if (std.Io.Dir.path.dirname(image_name)) |sub| try dir.makePath(sub);
        var file = try dir.createFile(image_name, .{});
        defer file.close();

        var buf: [512]u8 = undefined;
        var writer = file.writer(&buf);
        try capturePng(frame, null, &writer.interface);
        try writer.end();
        // Do not continue with checking hashes as it is not deterministic across content_scales because
        // fonts render in integer steps and scaling changes the step used and the size of the test
        return; // Do not skip test because other snapshots might run after this one
    } else {
        _ = try step(frame);
    }

    const HashInt = u64;
    const hash: HashInt = widget_hasher.?.final();
    // used both for reading or writing the hash
    var hash_buf: [@sizeOf(HashInt) * 2]u8 = undefined;

    const file = dir.openFile(filename, .{ .mode = .read_write }) catch |err| switch (err) {
        std.fs.File.OpenError.FileNotFound => {
            if (should_write_snapshots()) {
                if (std.Io.Dir.path.dirname(filename)) |sub| try dir.makePath(sub);
                const file = try dir.createFile(filename, .{});
                var writer = file.writer(&hash_buf);
                try writer.interface.print("{X}", .{hash});
                try writer.end();
                std.debug.print("Snapshot: Created file \"{s}\"\n", .{filename});
                return;
            }
            std.debug.print("{s}:{d}:{d}: Snapshot file did not exist! Run the test with `DVUI_SNAPSHOT_WRITE` to create all snapshot files\n", .{ src.file, src.line, src.column });
            return error.MissingSnapshotFile;
        },
        else => return err,
    };
    defer file.close();

    const len = try file.readAll(&hash_buf);
    const prev_hash = try std.fmt.parseUnsigned(HashInt, hash_buf[0..len], 16);

    if (prev_hash != hash) {
        if (should_write_snapshots()) {
            var writer = file.writer(&hash_buf);
            try writer.seekTo(0);
            try writer.interface.print("{X}", .{hash});
            try writer.end();
            std.debug.print("Snapshot: Overwrote file \"{s}\"\n", .{filename});
            return;
        }
        return SnapshotError.SnapshotsDidNotMatch;
    }
}