aboutsummaryrefslogtreecommitdiff
path: root/lib/std
diff options
context:
space:
mode:
authorAndrew Kelley <andrew@ziglang.org>2019-10-17 22:08:39 -0400
committerAndrew Kelley <andrew@ziglang.org>2019-10-17 22:08:39 -0400
commite42d86b657c2fae093fb2545e8b4b85614a0c906 (patch)
treee7b0f9f6f509e34edeb2226569b2ae78d34cfb5a /lib/std
parent17aa8c3ee29fae4bfbd6acc91eed8d229f627c32 (diff)
parent2d5b2bf1c986d037ef965bf8c9b4d8dfd5967478 (diff)
downloadzig-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.zig13
-rw-r--r--lib/std/progress.zig258
-rw-r--r--lib/std/special/test_runner.zig25
-rw-r--r--lib/std/std.zig11
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;