Skip to documentation
SLOP

tiny.smg.watch

Reference tiny.smg watch

Defined in tiny.smg.

API (14)

Actions

Public operations.

Types and contracts

Public types and contracts.

No direct callersNo direct callstiny.smgwatch
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallsNo direct callsprivate; no linktools.smg.src.watch.Snapshotreplacewatch.Snapshotdeinit
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsprivate; no linktools.smg.src.watchexpectChangedFixturewatchruntest; no linktools.smg.src.watchtest: watch detects changed and delet...private; no linktools.smg.src.watchscanChangedFileswatchchangedFiles
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallswatchrunprivate; no linktools.smg.src.watchfileListTextwatcheventText
Static calls · unresolved targets: 1 · external targets: 5.
Called byCallsprivate; no linktools.smg.src.watchcollecttest; no linktools.smg.src.watchtest: watch filters supported and exc...private; no linktools.smg.src.watchexcludedPartprivate; no linktools.smg.src.watchsupportedExtensionwatchisSupported
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsprivate; no linktools.smg.src.help.click.top.mutatewritewatchrundiffgraphsscanscanPathsWithOptionsstorage.graphloadSnapshotwatchrescan
Static calls · unresolved targets: 1 · external targets: 1.
Called byCallsNo direct callerswatchchangedFileswatcheventTextwatchrescanprivate; no linktools.smg.src.watchresetArenaprivate; no linktools.smg.src.watchsleepDebounce+4 morewatchrun
Static calls · unresolved targets: 2 · external targets: 11.
Called byCallswatchruntest; no linktools.smg.src.watchtest: watch snapshot replacements ret...private; no linktools.smg.src.watchcollectwatchsnapshot
Static calls · unresolved targets: 2 · external targets: 1.
Called byCallsNo direct callswatchrunwatchstartText
Static calls · unresolved targets: 0 · external targets: 3.
Called byCallsNo direct callswatchruntest; no linktools.smg.src.watchtest: watch stop text matches Python ...watchstoppedText
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callswatchrunwatchwatchPaths
Static calls · unresolved targets: 1 · external targets: 2.

Source: tools/smg/src/root.zig:38

zig
pub const watch = @import("watch.zig");

Source: tools/smg/src/watch.zig

zig
const std = @import("std");const alloc = @import("alloc");const pretty = @import("pretty");const sql = @import("sql");const sys = @import("sys");const diff = @import("diff.zig");const graph_mod = @import("graph.zig");const limits_mod = @import("limits/root.zig");const scan = @import("scan/root.zig");const storage = @import("storage/root.zig");const text = @import("text/root.zig");var interrupted: std.atomic.Value(bool) = .init(false);pub const FileState = struct {    path: []const u8,    size: u64,    mtime: i96,};pub const Snapshot = struct {    states: []FileState,    pub fn deinit(self: *Snapshot, allocator: std.mem.Allocator) void {        for (self.states) |state| allocator.free(state.path);        allocator.free(self.states);        self.* = undefined;    }    fn replace(self: *Snapshot, allocator: std.mem.Allocator, next: Snapshot) void {        self.deinit(allocator);        self.* = next;    }    fn retainedBytes(self: Snapshot) usize {        std.debug.assert(self.states.len <= std.math.maxInt(usize) / @sizeOf(FileState));        var bytes = self.states.len * @sizeOf(FileState);        for (self.states) |state| {            std.debug.assert(bytes <= std.math.maxInt(usize) - state.path.len);            bytes += state.path.len;        }        return bytes;    }};pub const Result = struct {    files: []const []const u8,    stats: scan.Stats,    changes: diff.Result,};pub const Save = *const fn (    std.mem.Allocator,    []const u8,    graph_mod.Graph,    sql.Hash,    limits_mod.Limits,) anyerror!void;pub fn run(    allocator: std.mem.Allocator,    phases: std.mem.Allocator,    root: []const u8,    raw_paths: []const []const u8,    debounce_seconds: f64,    limits: limits_mod.Limits,    save: Save,) !void {    interrupted.store(false, .release);    sys.signal.installInterruptHandler(handleInterrupt);    defer sys.signal.restoreInterruptDefault();    const paths = try watchPaths(allocator, raw_paths);    var stdout_buffer: [4096]u8 = undefined;    var stdout = std.Io.File.stdout().writer(std.Options.debug_io, &stdout_buffer);    var iteration_arena = std.heap.ArenaAllocator.init(phases);    defer iteration_arena.deinit();    try pretty.write(&stdout.interface, .{ .text = try startText(iteration_arena.allocator(), paths) }, .{ .width = 88 });    try stdout.interface.flush();    resetArena(&iteration_arena);    var previous = try snapshot(phases, paths);    defer previous.deinit(phases);    while (!interrupted.load(.acquire)) {        defer resetArena(&iteration_arena);        const scratch = iteration_arena.allocator();        sys.time.sleepMilliseconds(100);        if (interrupted.load(.acquire)) break;        var current = try snapshot(phases, paths);        var current_live = true;        defer if (current_live) current.deinit(phases);        const first_changed = try changedFiles(scratch, previous.states, current.states);        if (first_changed.len == 0) {            previous.replace(phases, current);            current_live = false;            continue;        }        current.deinit(phases);        current_live = false;        sleepDebounce(debounce_seconds);        if (interrupted.load(.acquire)) break;        var after_debounce = try snapshot(phases, paths);        var after_debounce_live = true;        defer if (after_debounce_live) after_debounce.deinit(phases);        const files = try changedFiles(scratch, previous.states, after_debounce.states);        const result = rescan(scratch, phases, root, files, limits, save) catch |err| switch (err) {            error.StaleSourceHead => continue,            else => return err,        };        try pretty.write(&stdout.interface, .{ .text = try eventText(scratch, root, result) }, .{ .width = 88 });        try stdout.interface.flush();        previous.replace(phases, after_debounce);        after_debounce_live = false;    }    try pretty.write(&stdout.interface, .{ .text = stoppedText() }, .{ .width = 88 });    try stdout.interface.flush();}pub fn startText(allocator: std.mem.Allocator, paths: []const []const u8) ![]const u8 {    var out: std.Io.Writer.Allocating = .init(allocator);    errdefer out.deinit();    try out.writer.writeAll("Watching ");    for (paths, 0..) |path, index| {        if (index != 0) try out.writer.writeAll(", ");        try out.writer.writeAll(path);    }    try out.writer.writeAll("\nPress Ctrl+C to stop.\n\n");    return try out.toOwnedSlice();}pub fn eventText(allocator: std.mem.Allocator, root: []const u8, result: Result) ![]const u8 {    var out: std.Io.Writer.Allocating = .init(allocator);    errdefer out.deinit();    try out.writer.writeAll(try fileListText(allocator, root, result.files));    if (result.changes.empty()) {        try out.writer.writeAll(" → no structural changes\n");        return try out.toOwnedSlice();    }    try out.writer.writeAll(" → ");    var wrote = false;    if (result.changes.added_nodes.len != 0) {        try out.writer.print("+{d} nodes", .{result.changes.added_nodes.len});        wrote = true;    }    if (result.changes.removed_nodes.len != 0) {        if (wrote) try out.writer.writeAll(", ");        try out.writer.print("-{d} nodes", .{result.changes.removed_nodes.len});        wrote = true;    }    if (result.changes.changed_nodes.len != 0) {        if (wrote) try out.writer.writeAll(", ");        try out.writer.print("~{d} changed", .{result.changes.changed_nodes.len});        wrote = true;    }    if (result.changes.added_edges.len != 0) {        if (wrote) try out.writer.writeAll(", ");        try out.writer.print("+{d} edges", .{result.changes.added_edges.len});        wrote = true;    }    if (result.changes.removed_edges.len != 0) {        if (wrote) try out.writer.writeAll(", ");        try out.writer.print("-{d} edges", .{result.changes.removed_edges.len});    }    try out.writer.writeByte('\n');    for (result.changes.added_nodes[0..@min(result.changes.added_nodes.len, 3)]) |node| try out.writer.print("  + [{s}] {s}\n", .{ node.type, node.name });    for (result.changes.removed_nodes[0..@min(result.changes.removed_nodes.len, 3)]) |node| try out.writer.print("  - [{s}] {s}\n", .{ node.type, node.name });    for (result.changes.changed_nodes[0..@min(result.changes.changed_nodes.len, 3)]) |changed| {        for (changed.changes) |change| try out.writer.print("  ~ {s} {s}: {s} → {s}\n", .{ changed.node.name, change.field, change.old orelse "None", change.new orelse "None" });    }    for (result.stats.orphaned_manual_edges.items) |edge| try out.writer.print("  orphaned: {s} --{s}--> {s}\n", .{ edge.source, edge.rel, edge.target });    return try out.toOwnedSlice();}pub fn stoppedText() []const u8 {    return "\nStopped.\n";}pub fn rescan(    allocator: std.mem.Allocator,    phases: std.mem.Allocator,    root: []const u8,    files: []const []const u8,    limits: limits_mod.Limits,    save: Save,) !Result {    const snapshot_value = try storage.graph.loadSnapshot(        allocator,        phases,        root,        limits.storage,    );    const old = snapshot_value.graph;    var graph = try graph_mod.clone(allocator, phases, old);    const stats = try scan.scanPathsWithOptions(allocator, &graph, root, files, true, .{ .limits = limits, .phases = phases });    try save(phases, root, graph, snapshot_value.head, limits);    return .{ .files = files, .stats = stats, .changes = try diff.graphs(allocator, old, graph, true) };}pub fn watchPaths(allocator: std.mem.Allocator, raw_paths: []const []const u8) ![]const []const u8 {    var out: std.ArrayList([]const u8) = .empty;    if (raw_paths.len == 0) {        try out.append(allocator, try sys.fs.realPathAlloc(allocator, "."));    } else {        for (raw_paths) |path| try out.append(allocator, try sys.fs.realPathAlloc(allocator, path));    }    return try out.toOwnedSlice(allocator);}pub fn snapshot(allocator: std.mem.Allocator, paths: []const []const u8) !Snapshot {    var states: std.ArrayList(FileState) = .empty;    errdefer {        for (states.items) |state| allocator.free(state.path);        states.deinit(allocator);    }    for (paths) |path| try collect(allocator, path, &states);    std.mem.sort(FileState, states.items, {}, stateLess);    return .{ .states = try states.toOwnedSlice(allocator) };}pub fn changedFiles(allocator: std.mem.Allocator, previous: []const FileState, current: []const FileState) ![]const []const u8 {    return (try scanChangedFiles(allocator, previous, current)).files;}const ChangedFileScan = struct {    files: []const []const u8,    path_comparisons: usize,};fn scanChangedFiles(allocator: std.mem.Allocator, previous: []const FileState, current: []const FileState) !ChangedFileScan {    var out: std.ArrayList([]const u8) = .empty;    errdefer out.deinit(allocator);    var previous_index: usize = 0;    var current_index: usize = 0;    var path_comparisons: usize = 0;    const entry_max = std.math.add(usize, previous.len, current.len) catch        return error.CapacityOverflow;    for (0..entry_max) |_| {        if (previous_index == previous.len or current_index == current.len) break;        const old = previous[previous_index];        const new = current[current_index];        switch (pathOrder(old.path, new.path, &path_comparisons)) {            .lt => {                try out.append(allocator, old.path);                previous_index = stateGroupEnd(previous, previous_index, &path_comparisons);            },            .gt => {                try out.append(allocator, new.path);                current_index = stateGroupEnd(current, current_index, &path_comparisons);            },            .eq => {                previous_index = stateGroupEnd(previous, previous_index, &path_comparisons);                const current_end = stateGroupEnd(current, current_index, &path_comparisons);                var changed_path: ?[]const u8 = null;                for (current[current_index..current_end]) |state| {                    if (old.size != state.size or old.mtime != state.mtime) {                        if (changed_path == null) changed_path = state.path;                    }                }                if (changed_path) |path| try out.append(allocator, path);                current_index = current_end;            },        }    }    const previous_remaining_max = previous.len - previous_index;    for (0..previous_remaining_max) |_| {        if (previous_index == previous.len) break;        try out.append(allocator, previous[previous_index].path);        previous_index = stateGroupEnd(previous, previous_index, &path_comparisons);    }    const current_remaining_max = current.len - current_index;    for (0..current_remaining_max) |_| {        if (current_index == current.len) break;        try out.append(allocator, current[current_index].path);        current_index = stateGroupEnd(current, current_index, &path_comparisons);    }    std.debug.assert(previous_index == previous.len);    std.debug.assert(current_index == current.len);    return .{        .files = try out.toOwnedSlice(allocator),        .path_comparisons = path_comparisons,    };}pub fn isSupported(path: []const u8) bool {    var parts = std.mem.splitAny(u8, path, "/\\");    while (parts.next()) |part| {        if (part.len == 0) continue;        if (part[0] == '.') return false;        if (excludedPart(part)) return false;    }    return supportedExtension(path);}fn collect(allocator: std.mem.Allocator, path: []const u8, states: *std.ArrayList(FileState)) !void {    const stat = sys.fs.statFile(path) catch |err| switch (err) {        error.FileNotFound => return,        else => return err,    };    if (stat.kind == .directory) {        if (!directoryAllowed(path)) return;        const entries = sys.fs.listDirAlloc(allocator, path) catch return;        defer sys.fs.freeEntries(allocator, entries);        for (entries) |entry| try collect(allocator, entry.path, states);        return;    }    if (stat.kind != .file or !isSupported(path)) return;    const owned_path = try allocator.dupe(u8, path);    errdefer allocator.free(owned_path);    try states.append(allocator, .{        .path = owned_path,        .size = stat.size,        .mtime = stat.mtime.nanoseconds,    });}fn directoryAllowed(path: []const u8) bool {    var parts = std.mem.splitAny(u8, path, "/\\");    while (parts.next()) |part| {        if (part.len == 0) continue;        if (part[0] == '.') return false;        if (excludedPart(part)) return false;    }    return true;}fn excludedPart(part: []const u8) bool {    const exact = [_][]const u8{ ".git", "__pycache__", ".venv", "venv", ".env", "node_modules", ".mypy_cache", ".pytest_cache", ".ruff_cache", ".hypothesis", ".tox", "dist", "build", ".smg", "site-packages", "vendor", "third_party", "zig-cache", ".zig-cache", "zig-out" };    for (exact) |item| if (std.mem.eql(u8, part, item)) return true;    return std.mem.endsWith(u8, part, ".egg-info");}fn supportedExtension(path: []const u8) bool {    const extensions = [_][]const u8{ ".py", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".zig", ".chic", ".c", ".h", ".cpp", ".hpp", ".cc", ".cxx", ".hh", ".hxx", ".cu", ".cuh", ".metal" };    for (extensions) |extension| if (std.mem.endsWith(u8, path, extension)) return true;    return false;}fn fileListText(allocator: std.mem.Allocator, root: []const u8, files: []const []const u8) ![]const u8 {    var out: std.Io.Writer.Allocating = .init(allocator);    errdefer out.deinit();    const count = @min(files.len, 3);    for (files[0..count], 0..) |file, index| {        if (index != 0) try out.writer.writeAll(", ");        try out.writer.writeAll(try relativeName(allocator, root, file));    }    if (files.len > 3) try out.writer.print(" (+{d} more)", .{files.len - 3});    return try out.toOwnedSlice();}fn relativeName(allocator: std.mem.Allocator, root: []const u8, path: []const u8) ![]const u8 {    var rel = path;    if (std.fs.path.isAbsolute(path) and std.mem.startsWith(u8, path, root)) {        rel = path[root.len..];        while (rel.len != 0 and (rel[0] == '/' or rel[0] == '\\')) rel = rel[1..];    }    return try text.replaceSeparators(allocator, rel, '/');}fn stateGroupEnd(states: []const FileState, start: usize, path_comparisons: *usize) usize {    std.debug.assert(start < states.len);    const path = states[start].path;    var end = start + 1;    const remaining_max = states.len - end;    for (0..remaining_max) |_| {        if (!pathsEqual(path, states[end].path, path_comparisons)) break;        end += 1;    }    return end;}fn pathOrder(a: []const u8, b: []const u8, path_comparisons: *usize) std.math.Order {    std.debug.assert(path_comparisons.* < std.math.maxInt(usize));    path_comparisons.* += 1;    return std.mem.order(u8, a, b);}fn pathsEqual(a: []const u8, b: []const u8, path_comparisons: *usize) bool {    std.debug.assert(path_comparisons.* < std.math.maxInt(usize));    path_comparisons.* += 1;    return std.mem.eql(u8, a, b);}fn sleepDebounce(debounce_seconds: f64) void {    if (!std.math.isFinite(debounce_seconds) or debounce_seconds <= 0) return;    var remaining_ms = secondsToMilliseconds(debounce_seconds);    while (remaining_ms != 0 and !interrupted.load(.acquire)) {        const step = @min(remaining_ms, 100);        sys.time.sleepMilliseconds(step);        remaining_ms -= step;    }}fn resetArena(arena: *std.heap.ArenaAllocator) void {    _ = arena.reset(.free_all);}fn secondsToMilliseconds(seconds: f64) u64 {    const max_seconds = @as(f64, @floatFromInt(std.math.maxInt(u64))) / 1000.0;    if (seconds >= max_seconds) return std.math.maxInt(u64);    return @intFromFloat(@ceil(seconds * 1000.0));}fn handleInterrupt(_: sys.signal.RawSignal) callconv(.c) void {    interrupted.store(true, .release);}fn stateLess(_: void, a: FileState, b: FileState) bool {    return std.mem.lessThan(u8, a.path, b.path);}fn stringLess(_: void, a: []const u8, b: []const u8) bool {    return std.mem.lessThan(u8, a, b);}fn changedFilesReference(allocator: std.mem.Allocator, previous: []const FileState, current: []const FileState) ![]const []const u8 {    var out: std.ArrayList([]const u8) = .empty;    errdefer out.deinit(allocator);    for (current) |state| {        const old = referenceFindState(previous, state.path);        if (old == null or old.?.size != state.size or old.?.mtime != state.mtime) {            try referenceAppendUnique(allocator, &out, state.path);        }    }    for (previous) |state| {        if (referenceFindState(current, state.path) == null) {            try referenceAppendUnique(allocator, &out, state.path);        }    }    std.mem.sort([]const u8, out.items, {}, stringLess);    return try out.toOwnedSlice(allocator);}fn referenceFindState(states: []const FileState, path: []const u8) ?FileState {    for (states) |state| if (std.mem.eql(u8, state.path, path)) return state;    return null;}fn referenceAppendUnique(allocator: std.mem.Allocator, out: *std.ArrayList([]const u8), path: []const u8) !void {    for (out.items) |item| if (std.mem.eql(u8, item, path)) return;    try out.append(allocator, path);}fn expectChangedFixture(previous: []const FileState, current: []const FileState, expected: []const []const u8) !void {    const allocator = std.testing.allocator;    const merged = try changedFiles(allocator, previous, current);    defer allocator.free(merged);    const reference = try changedFilesReference(allocator, previous, current);    defer allocator.free(reference);    try std.testing.expectEqual(reference.len, merged.len);    try std.testing.expectEqual(expected.len, merged.len);    for (merged, reference, expected) |actual, old, wanted| {        try std.testing.expectEqualStrings(old, actual);        try std.testing.expectEqualStrings(wanted, actual);    }}test "watch filters supported and excluded paths" {    try std.testing.expect(isSupported("src/app/core.py"));    try std.testing.expect(isSupported("lib/server.ts"));    try std.testing.expect(isSupported("lib/chic/kernel.chic"));    try std.testing.expect(!isSupported("__pycache__/core.cpython-311.pyc"));    try std.testing.expect(!isSupported(".git/hooks/pre-commit"));    try std.testing.expect(!isSupported("node_modules/foo/index.js"));    try std.testing.expect(!isSupported(".venv/lib/site.py"));    try std.testing.expect(!isSupported("README.md"));    try std.testing.expect(!isSupported("data.csv"));}test "watch detects changed and deleted files" {    const allocator = std.testing.allocator;    const previous = [_]FileState{        .{ .path = "a.py", .size = 1, .mtime = 1 },        .{ .path = "b.py", .size = 2, .mtime = 2 },    };    const current = [_]FileState{        .{ .path = "a.py", .size = 3, .mtime = 1 },        .{ .path = "c.py", .size = 1, .mtime = 1 },    };    const changed = try changedFiles(allocator, previous[0..], current[0..]);    defer allocator.free(changed);    try std.testing.expectEqual(@as(usize, 3), changed.len);    try std.testing.expectEqualStrings("a.py", changed[0]);    try std.testing.expectEqualStrings("b.py", changed[1]);    try std.testing.expectEqualStrings("c.py", changed[2]);}test "watch merge walk matches reference change classifications" {    const one = [_]FileState{        .{ .path = "a.py", .size = 1, .mtime = 1 },    };    const two = [_]FileState{        .{ .path = "a.py", .size = 1, .mtime = 1 },        .{ .path = "b.py", .size = 2, .mtime = 2 },    };    const modified = [_]FileState{        .{ .path = "a.py", .size = 2, .mtime = 1 },    };    try expectChangedFixture(&one, &one, &.{});    try expectChangedFixture(&one, &two, &.{"b.py"});    try expectChangedFixture(&two, &one, &.{"b.py"});    try expectChangedFixture(&one, &modified, &.{"a.py"});    const mixed_previous = [_]FileState{        .{ .path = "a.py", .size = 1, .mtime = 1 },        .{ .path = "b.py", .size = 2, .mtime = 2 },        .{ .path = "c.py", .size = 3, .mtime = 3 },        .{ .path = "d.py", .size = 4, .mtime = 4 },    };    const mixed_current = [_]FileState{        .{ .path = "a.py", .size = 1, .mtime = 1 },        .{ .path = "b.py", .size = 5, .mtime = 2 },        .{ .path = "d.py", .size = 4, .mtime = 4 },        .{ .path = "e.py", .size = 5, .mtime = 5 },    };    try expectChangedFixture(        &mixed_previous,        &mixed_current,        &.{ "b.py", "c.py", "e.py" },    );    const duplicate_previous = [_]FileState{        .{ .path = "a.py", .size = 1, .mtime = 1 },        .{ .path = "a.py", .size = 2, .mtime = 2 },    };    const duplicate_current = [_]FileState{        .{ .path = "a.py", .size = 1, .mtime = 1 },        .{ .path = "a.py", .size = 3, .mtime = 3 },    };    try expectChangedFixture(&duplicate_previous, &duplicate_current, &.{"a.py"});}test "watch merge walk path comparisons grow linearly" {    const testing = std.testing;    const entry_count: usize = 4096;    var paths: [entry_count][4]u8 = undefined;    var previous: [entry_count]FileState = undefined;    var current: [entry_count]FileState = undefined;    for (0..entry_count) |index| {        std.mem.writeInt(u32, &paths[index], @intCast(index), .big);        previous[index] = .{ .path = &paths[index], .size = 1, .mtime = 1 };        current[index] = .{            .path = &paths[index],            .size = if (index + 1 == entry_count) 2 else 1,            .mtime = 1,        };    }    const scanned = try scanChangedFiles(testing.allocator, &previous, &current);    defer testing.allocator.free(scanned.files);    try testing.expectEqual(@as(usize, 1), scanned.files.len);    try testing.expectEqualSlices(u8, &paths[entry_count - 1], scanned.files[0]);    const comparison_max = entry_count * 3;    try testing.expect(scanned.path_comparisons <= comparison_max);}test "watch snapshot replacements retain only live snapshot bytes" {    const testing = std.testing;    const root = try std.fmt.allocPrint(        testing.allocator,        "/tmp/smg-watch-retention-{x}",        .{@intFromPtr(&interrupted)},    );    defer testing.allocator.free(root);    sys.fs.deleteTree(root) catch {};    defer sys.fs.deleteTree(root) catch {};    try sys.fs.createDirPath(root);    const file_path = try std.fs.path.join(testing.allocator, &.{ root, "main.zig" });    defer testing.allocator.free(file_path);    try text.writeFile(file_path, "pub fn main() void {}\n");    var meter = alloc.LimitAllocator.init(testing.allocator, 1024 * 1024);    defer meter.deinit();    const paths = [_][]const u8{root};    var stable = try snapshot(meter.allocator(), &paths);    defer stable.deinit(meter.allocator());    try testing.expectEqual(@as(usize, 1), stable.states.len);    const retained_bytes = stable.retainedBytes();    try testing.expectEqual(retained_bytes, meter.liveBytes());    const replacement_count: usize = 32;    for (0..replacement_count) |_| {        const next = try snapshot(meter.allocator(), &paths);        stable.replace(meter.allocator(), next);        try testing.expectEqual(retained_bytes, stable.retainedBytes());        try testing.expectEqual(retained_bytes, meter.liveBytes());    }}test "watch stop text matches Python interrupt footer" {    try std.testing.expectEqualStrings("\nStopped.\n", stoppedText());}test "watch debounce sleep observes interrupts" {    interrupted.store(false, .release);    handleInterrupt(.INT);    sleepDebounce(10);    try std.testing.expect(interrupted.load(.acquire));    interrupted.store(false, .release);}

Complete call list for watch.run

9 direct calls.

Audit

Definitions15
Public names15
Members7
Version26.7.0
Revisiondaab053ee433