diff options
| author | Andrew Kelley <andrew@ziglang.org> | 2019-10-17 22:08:39 -0400 |
|---|---|---|
| committer | Andrew Kelley <andrew@ziglang.org> | 2019-10-17 22:08:39 -0400 |
| commit | e42d86b657c2fae093fb2545e8b4b85614a0c906 (patch) | |
| tree | e7b0f9f6f509e34edeb2226569b2ae78d34cfb5a /lib/std | |
| parent | 17aa8c3ee29fae4bfbd6acc91eed8d229f627c32 (diff) | |
| parent | 2d5b2bf1c986d037ef965bf8c9b4d8dfd5967478 (diff) | |
| download | zig-e42d86b657c2fae093fb2545e8b4b85614a0c906.tar.gz zig-e42d86b657c2fae093fb2545e8b4b85614a0c906.zip | |
Merge branch 'lun-4-progress-take-2'
closes #3362
Diffstat (limited to 'lib/std')
| -rw-r--r-- | lib/std/fmt.zig | 13 | ||||
| -rw-r--r-- | lib/std/progress.zig | 258 | ||||
| -rw-r--r-- | lib/std/special/test_runner.zig | 25 | ||||
| -rw-r--r-- | lib/std/std.zig | 11 |
4 files changed, 289 insertions, 18 deletions
diff --git a/lib/std/fmt.zig b/lib/std/fmt.zig index 3f3e3fca73..d182214aed 100644 --- a/lib/std/fmt.zig +++ b/lib/std/fmt.zig @@ -1055,14 +1055,21 @@ const BufPrintContext = struct { }; fn bufPrintWrite(context: *BufPrintContext, bytes: []const u8) !void { - if (context.remaining.len < bytes.len) return error.BufferTooSmall; + if (context.remaining.len < bytes.len) { + mem.copy(u8, context.remaining, bytes[0..context.remaining.len]); + return error.BufferTooSmall; + } mem.copy(u8, context.remaining, bytes); context.remaining = context.remaining[bytes.len..]; } -pub fn bufPrint(buf: []u8, comptime fmt: []const u8, args: ...) ![]u8 { +pub const BufPrintError = error{ + /// As much as possible was written to the buffer, but it was too small to fit all the printed bytes. + BufferTooSmall, +}; +pub fn bufPrint(buf: []u8, comptime fmt: []const u8, args: ...) BufPrintError![]u8 { var context = BufPrintContext{ .remaining = buf }; - try format(&context, error{BufferTooSmall}, bufPrintWrite, fmt, args); + try format(&context, BufPrintError, bufPrintWrite, fmt, args); return buf[0 .. buf.len - context.remaining.len]; } diff --git a/lib/std/progress.zig b/lib/std/progress.zig new file mode 100644 index 0000000000..1d29763c10 --- /dev/null +++ b/lib/std/progress.zig @@ -0,0 +1,258 @@ +const std = @import("std"); +const testing = std.testing; +const assert = std.debug.assert; + +/// This API is non-allocating and non-fallible. The tradeoff is that users of +/// this API must provide the storage for each `Progress.Node`. +/// Initialize the struct directly, overriding these fields as desired: +/// * `refresh_rate_ms` +/// * `initial_delay_ms` +pub const Progress = struct { + /// `null` if the current node (and its children) should + /// not print on update() + terminal: ?std.fs.File = undefined, + + root: Node = undefined, + + /// Keeps track of how much time has passed since the beginning. + /// Used to compare with `initial_delay_ms` and `refresh_rate_ms`. + timer: std.time.Timer = undefined, + + /// When the previous refresh was written to the terminal. + /// Used to compare with `refresh_rate_ms`. + prev_refresh_timestamp: u64 = undefined, + + /// This buffer represents the maximum number of bytes written to the terminal + /// with each refresh. + output_buffer: [100]u8 = undefined, + + /// How many nanoseconds between writing updates to the terminal. + refresh_rate_ns: u64 = 50 * std.time.millisecond, + + /// How many nanoseconds to keep the output hidden + initial_delay_ns: u64 = 500 * std.time.millisecond, + + done: bool = true, + + /// Keeps track of how many columns in the terminal have been output, so that + /// we can move the cursor back later. + columns_written: usize = undefined, + + /// Represents one unit of progress. Each node can have children nodes, or + /// one can use integers with `update`. + pub const Node = struct { + context: *Progress, + parent: ?*Node, + completed_items: usize, + name: []const u8, + recently_updated_child: ?*Node = null, + + /// This field may be updated freely. + estimated_total_items: ?usize, + + /// Create a new child progress node. + /// Call `Node.end` when done. + /// TODO solve https://github.com/ziglang/zig/issues/2765 and then change this + /// API to set `self.parent.recently_updated_child` with the return value. + /// Until that is fixed you probably want to call `activate` on the return value. + pub fn start(self: *Node, name: []const u8, estimated_total_items: ?usize) Node { + return Node{ + .context = self.context, + .parent = self, + .completed_items = 0, + .name = name, + .estimated_total_items = estimated_total_items, + }; + } + + /// This is the same as calling `start` and then `end` on the returned `Node`. + pub fn completeOne(self: *Node) void { + if (self.parent) |parent| parent.recently_updated_child = self; + self.completed_items += 1; + self.context.maybeRefresh(); + } + + pub fn end(self: *Node) void { + self.context.maybeRefresh(); + if (self.parent) |parent| { + if (parent.recently_updated_child) |parent_child| { + if (parent_child == self) { + parent.recently_updated_child = null; + } + } + parent.completeOne(); + } else { + self.context.done = true; + self.context.refresh(); + } + } + + /// Tell the parent node that this node is actively being worked on. + pub fn activate(self: *Node) void { + if (self.parent) |parent| parent.recently_updated_child = self; + } + }; + + /// Create a new progress node. + /// Call `Node.end` when done. + /// TODO solve https://github.com/ziglang/zig/issues/2765 and then change this + /// API to return Progress rather than accept it as a parameter. + pub fn start(self: *Progress, name: []const u8, estimated_total_items: ?usize) !*Node { + if (std.io.getStdErr()) |stderr| { + self.terminal = if (stderr.supportsAnsiEscapeCodes()) stderr else null; + } else |_| { + self.terminal = null; + } + self.root = Node{ + .context = self, + .parent = null, + .completed_items = 0, + .name = name, + .estimated_total_items = estimated_total_items, + }; + self.columns_written = 0; + self.prev_refresh_timestamp = 0; + self.timer = try std.time.Timer.start(); + self.done = false; + return &self.root; + } + + /// Updates the terminal if enough time has passed since last update. + pub fn maybeRefresh(self: *Progress) void { + const now = self.timer.read(); + if (now < self.initial_delay_ns) return; + if (now - self.prev_refresh_timestamp < self.refresh_rate_ns) return; + self.refresh(); + } + + /// Updates the terminal and resets `self.next_refresh_timestamp`. + pub fn refresh(self: *Progress) void { + const file = self.terminal orelse return; + + const prev_columns_written = self.columns_written; + var end: usize = 0; + if (self.columns_written > 0) { + // restore cursor position + end += (std.fmt.bufPrint(self.output_buffer[end..], "\x1b[{}D", self.columns_written) catch unreachable).len; + self.columns_written = 0; + + // clear rest of line + end += (std.fmt.bufPrint(self.output_buffer[end..], "\x1b[0K") catch unreachable).len; + } + + if (!self.done) { + var need_ellipse = false; + var maybe_node: ?*Node = &self.root; + while (maybe_node) |node| { + if (need_ellipse) { + self.bufWrite(&end, "..."); + } + need_ellipse = false; + if (node.name.len != 0 or node.estimated_total_items != null) { + if (node.name.len != 0) { + self.bufWrite(&end, "{}", node.name); + need_ellipse = true; + } + if (node.estimated_total_items) |total| { + if (need_ellipse) self.bufWrite(&end, " "); + self.bufWrite(&end, "[{}/{}] ", node.completed_items, total); + need_ellipse = false; + } else if (node.completed_items != 0) { + if (need_ellipse) self.bufWrite(&end, " "); + self.bufWrite(&end, "[{}] ", node.completed_items); + need_ellipse = false; + } + } + maybe_node = node.recently_updated_child; + } + if (need_ellipse) { + self.bufWrite(&end, "..."); + } + } + + _ = file.write(self.output_buffer[0..end]) catch |e| { + // Stop trying to write to this file once it errors. + self.terminal = null; + }; + self.prev_refresh_timestamp = self.timer.read(); + } + + pub fn log(self: *Progress, comptime format: []const u8, args: ...) void { + const file = self.terminal orelse return; + self.refresh(); + file.outStream().stream.print(format, args) catch { + self.terminal = null; + return; + }; + self.columns_written = 0; + } + + fn bufWrite(self: *Progress, end: *usize, comptime format: []const u8, args: ...) void { + if (std.fmt.bufPrint(self.output_buffer[end.*..], format, args)) |written| { + const amt = written.len; + end.* += amt; + self.columns_written += amt; + } else |err| switch (err) { + error.BufferTooSmall => { + self.columns_written += self.output_buffer.len - end.*; + end.* = self.output_buffer.len; + }, + } + const bytes_needed_for_esc_codes_at_end = 11; + const max_end = self.output_buffer.len - bytes_needed_for_esc_codes_at_end; + if (end.* > max_end) { + const suffix = "..."; + self.columns_written = self.columns_written - (end.* - max_end) + suffix.len; + std.mem.copy(u8, self.output_buffer[max_end..], suffix); + end.* = max_end + suffix.len; + } + } +}; + +test "basic functionality" { + var disable = true; + if (disable) { + // This test is disabled because it uses time.sleep() and is therefore slow. It also + // prints bogus progress data to stderr. + return error.SkipZigTest; + } + var progress = Progress{}; + const root_node = try progress.start("", 100); + defer root_node.end(); + + const sub_task_names = [_][]const u8{ + "reticulating splines", + "adjusting shoes", + "climbing towers", + "pouring juice", + }; + var next_sub_task: usize = 0; + + var i: usize = 0; + while (i < 100) : (i += 1) { + var node = root_node.start(sub_task_names[next_sub_task], 5); + node.activate(); + next_sub_task = (next_sub_task + 1) % sub_task_names.len; + + node.completeOne(); + std.time.sleep(5 * std.time.millisecond); + node.completeOne(); + node.completeOne(); + std.time.sleep(5 * std.time.millisecond); + node.completeOne(); + node.completeOne(); + std.time.sleep(5 * std.time.millisecond); + + node.end(); + + std.time.sleep(5 * std.time.millisecond); + } + { + var node = root_node.start("this is a really long name designed to activate the truncation code. let's find out if it works", null); + node.activate(); + std.time.sleep(10 * std.time.millisecond); + progress.refresh(); + std.time.sleep(10 * std.time.millisecond); + node.end(); + } +} diff --git a/lib/std/special/test_runner.zig b/lib/std/special/test_runner.zig index db01293059..8fb1b5842d 100644 --- a/lib/std/special/test_runner.zig +++ b/lib/std/special/test_runner.zig @@ -2,28 +2,33 @@ const std = @import("std"); const io = std.io; const builtin = @import("builtin"); const test_fn_list = builtin.test_functions; -const warn = std.debug.warn; -pub fn main() !void { +pub fn main() anyerror!void { var ok_count: usize = 0; var skip_count: usize = 0; - for (test_fn_list) |test_fn, i| { - warn("{}/{} {}...", i + 1, test_fn_list.len, test_fn.name); + var progress = std.Progress{}; + const root_node = progress.start("Test", test_fn_list.len) catch |err| switch (err) { + // TODO still run tests in this case + error.TimerUnsupported => @panic("timer unsupported"), + }; + for (test_fn_list) |test_fn, i| { + var test_node = root_node.start(test_fn.name, null); + test_node.activate(); if (test_fn.func()) |_| { ok_count += 1; - warn("OK\n"); + test_node.end(); } else |err| switch (err) { error.SkipZigTest => { skip_count += 1; - warn("SKIP\n"); + test_node.end(); + progress.log("{}...SKIP\n", test_fn.name); }, else => return err, } } - if (ok_count == test_fn_list.len) { - warn("All tests passed.\n"); - } else { - warn("{} passed; {} skipped.\n", ok_count, skip_count); + root_node.end(); + if (ok_count != test_fn_list.len) { + progress.log("{} passed; {} skipped.\n", ok_count, skip_count); } } diff --git a/lib/std/std.zig b/lib/std/std.zig index f67d0c9320..f41181f6e9 100644 --- a/lib/std/std.zig +++ b/lib/std/std.zig @@ -6,20 +6,21 @@ pub const BufMap = @import("buf_map.zig").BufMap; pub const BufSet = @import("buf_set.zig").BufSet; pub const Buffer = @import("buffer.zig").Buffer; pub const BufferOutStream = @import("io.zig").BufferOutStream; +pub const ChildProcess = @import("child_process.zig").ChildProcess; pub const DynLib = @import("dynamic_library.zig").DynLib; pub const HashMap = @import("hash_map.zig").HashMap; pub const Mutex = @import("mutex.zig").Mutex; -pub const PackedIntArrayEndian = @import("packed_int_array.zig").PackedIntArrayEndian; pub const PackedIntArray = @import("packed_int_array.zig").PackedIntArray; -pub const PackedIntSliceEndian = @import("packed_int_array.zig").PackedIntSliceEndian; +pub const PackedIntArrayEndian = @import("packed_int_array.zig").PackedIntArrayEndian; pub const PackedIntSlice = @import("packed_int_array.zig").PackedIntSlice; +pub const PackedIntSliceEndian = @import("packed_int_array.zig").PackedIntSliceEndian; pub const PriorityQueue = @import("priority_queue.zig").PriorityQueue; -pub const SinglyLinkedList = @import("linked_list.zig").SinglyLinkedList; -pub const StaticallyInitializedMutex = @import("statically_initialized_mutex.zig").StaticallyInitializedMutex; +pub const Progress = @import("progress.zig").Progress; pub const SegmentedList = @import("segmented_list.zig").SegmentedList; +pub const SinglyLinkedList = @import("linked_list.zig").SinglyLinkedList; pub const SpinLock = @import("spinlock.zig").SpinLock; +pub const StaticallyInitializedMutex = @import("statically_initialized_mutex.zig").StaticallyInitializedMutex; pub const StringHashMap = @import("hash_map.zig").StringHashMap; -pub const ChildProcess = @import("child_process.zig").ChildProcess; pub const TailQueue = @import("linked_list.zig").TailQueue; pub const Thread = @import("thread.zig").Thread; |
