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:
- Ensure all snapshot test pass
- Delete the snapshot directory
- Run all snapshot tests with
DVUI_SNAPSHOT_WRITEset 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;
}
}