|
17 | 17 | // along with this program. If not, see <https://www.gnu.org/licenses/>. |
18 | 18 |
|
19 | 19 | const std = @import("std"); |
20 | | -const log = std.log.scoped(.console); |
| 20 | +const builtin = @import("builtin"); |
| 21 | + |
| 22 | +const JsObject = @import("../env.zig").Env.JsObject; |
| 23 | +const SessionState = @import("../env.zig").SessionState; |
| 24 | + |
| 25 | +const log = if (builtin.is_test) &test_capture else std.log.scoped(.console); |
21 | 26 |
|
22 | 27 | pub const Console = struct { |
23 | 28 | // TODO: configurable writer |
| 29 | + timers: std.StringHashMapUnmanaged(u32) = .{}, |
| 30 | + counts: std.StringHashMapUnmanaged(u32) = .{}, |
| 31 | + |
| 32 | + pub fn _log(_: *const Console, values: []JsObject, state: *SessionState) !void { |
| 33 | + if (values.len == 0) { |
| 34 | + return; |
| 35 | + } |
| 36 | + log.info("{s}", .{try serializeValues(values, state)}); |
| 37 | + } |
| 38 | + |
| 39 | + pub fn _info(console: *const Console, values: []JsObject, state: *SessionState) !void { |
| 40 | + return console._log(values, state); |
| 41 | + } |
| 42 | + |
| 43 | + pub fn _debug(_: *const Console, values: []JsObject, state: *SessionState) !void { |
| 44 | + if (values.len == 0) { |
| 45 | + return; |
| 46 | + } |
| 47 | + log.debug("{s}", .{try serializeValues(values, state)}); |
| 48 | + } |
| 49 | + |
| 50 | + pub fn _warn(_: *const Console, values: []JsObject, state: *SessionState) !void { |
| 51 | + if (values.len == 0) { |
| 52 | + return; |
| 53 | + } |
| 54 | + log.warn("{s}", .{try serializeValues(values, state)}); |
| 55 | + } |
| 56 | + |
| 57 | + pub fn _error(_: *const Console, values: []JsObject, state: *SessionState) !void { |
| 58 | + if (values.len == 0) { |
| 59 | + return; |
| 60 | + } |
| 61 | + log.err("{s}", .{try serializeValues(values, state)}); |
| 62 | + } |
| 63 | + |
| 64 | + pub fn _clear(_: *const Console) void {} |
| 65 | + |
| 66 | + pub fn _count(self: *Console, label_: ?[]const u8, state: *SessionState) !void { |
| 67 | + const label = label_ orelse "default"; |
| 68 | + const gop = try self.counts.getOrPut(state.arena, label); |
| 69 | + |
| 70 | + var current: u32 = 0; |
| 71 | + if (gop.found_existing) { |
| 72 | + current = gop.value_ptr.*; |
| 73 | + } else { |
| 74 | + gop.key_ptr.* = try state.arena.dupe(u8, label); |
| 75 | + } |
| 76 | + |
| 77 | + const count = current + 1; |
| 78 | + gop.value_ptr.* = count; |
| 79 | + |
| 80 | + log.info("{s}: {d}", .{ label, count }); |
| 81 | + } |
| 82 | + |
| 83 | + pub fn _countReset(self: *Console, label_: ?[]const u8) !void { |
| 84 | + const label = label_ orelse "default"; |
| 85 | + const kv = self.counts.fetchRemove(label) orelse { |
| 86 | + log.warn("Counter \"{s}\" doesn't exist.", .{label}); |
| 87 | + return; |
| 88 | + }; |
| 89 | + |
| 90 | + log.info("{s}: {d}", .{ label, kv.value }); |
| 91 | + } |
| 92 | + |
| 93 | + pub fn _time(self: *Console, label_: ?[]const u8, state: *SessionState) !void { |
| 94 | + const label = label_ orelse "default"; |
| 95 | + const gop = try self.timers.getOrPut(state.arena, label); |
| 96 | + |
| 97 | + if (gop.found_existing) { |
| 98 | + log.warn("Timer \"{s}\" already exists.", .{label}); |
| 99 | + return; |
| 100 | + } |
| 101 | + gop.key_ptr.* = try state.arena.dupe(u8, label); |
| 102 | + gop.value_ptr.* = timestamp(); |
| 103 | + } |
| 104 | + |
| 105 | + pub fn _timeLog(self: *Console, label_: ?[]const u8) void { |
| 106 | + const elapsed = timestamp(); |
| 107 | + const label = label_ orelse "default"; |
| 108 | + const start = self.timers.get(label) orelse { |
| 109 | + log.warn("Timer \"{s}\" doesn't exist.", .{label}); |
| 110 | + return; |
| 111 | + }; |
| 112 | + |
| 113 | + log.info("\"{s}\": {d}ms", .{ label, elapsed - start }); |
| 114 | + } |
| 115 | + |
| 116 | + pub fn _timeStop(self: *Console, label_: ?[]const u8) void { |
| 117 | + const elapsed = timestamp(); |
| 118 | + const label = label_ orelse "default"; |
| 119 | + const kv = self.timers.fetchRemove(label) orelse { |
| 120 | + log.warn("Timer \"{s}\" doesn't exist.", .{label}); |
| 121 | + return; |
| 122 | + }; |
| 123 | + |
| 124 | + log.info("\"{s}\": {d}ms - timer ended", .{ label, elapsed - kv.value }); |
| 125 | + } |
| 126 | + |
| 127 | + pub fn _assert(_: *Console, assertion: JsObject, values: []JsObject, state: *SessionState) !void { |
| 128 | + if (assertion.isTruthy()) { |
| 129 | + return; |
| 130 | + } |
| 131 | + var serialized_values: []const u8 = ""; |
| 132 | + if (values.len > 0) { |
| 133 | + serialized_values = try serializeValues(values, state); |
| 134 | + } |
| 135 | + log.err("Assertion failed: {s}", .{serialized_values}); |
| 136 | + } |
| 137 | + |
| 138 | + fn serializeValues(values: []JsObject, state: *SessionState) ![]const u8 { |
| 139 | + const arena = state.call_arena; |
| 140 | + var arr: std.ArrayListUnmanaged(u8) = .{}; |
| 141 | + try arr.appendSlice(arena, try values[0].toString()); |
| 142 | + for (values[1..]) |value| { |
| 143 | + try arr.append(arena, ' '); |
| 144 | + try arr.appendSlice(arena, try value.toString()); |
| 145 | + } |
| 146 | + return arr.items; |
| 147 | + } |
| 148 | +}; |
| 149 | + |
| 150 | +fn timestamp() u32 { |
| 151 | + const ts = std.posix.clock_gettime(std.posix.CLOCK.MONOTONIC) catch unreachable; |
| 152 | + return @intCast(ts.sec); |
| 153 | +} |
| 154 | + |
| 155 | +var test_capture = TestCapture{}; |
| 156 | +const testing = @import("../../testing.zig"); |
| 157 | +test "Browser.Console" { |
| 158 | + var runner = try testing.jsRunner(testing.tracking_allocator, .{}); |
| 159 | + defer runner.deinit(); |
| 160 | + |
| 161 | + defer testing.reset(); |
| 162 | + |
| 163 | + { |
| 164 | + try runner.testCases(&.{ |
| 165 | + .{ "console.log('a')", "undefined" }, |
| 166 | + .{ "console.warn('hello world', 23, true, new Object())", "undefined" }, |
| 167 | + }, .{}); |
| 168 | + |
| 169 | + const captured = test_capture.captured.items; |
| 170 | + try testing.expectEqual("a", captured[0]); |
| 171 | + try testing.expectEqual("hello world 23 true [object Object]", captured[1]); |
| 172 | + } |
| 173 | + |
| 174 | + { |
| 175 | + test_capture.reset(); |
| 176 | + try runner.testCases(&.{ |
| 177 | + .{ "console.countReset()", "undefined" }, |
| 178 | + .{ "console.count()", "undefined" }, |
| 179 | + .{ "console.count('teg')", "undefined" }, |
| 180 | + .{ "console.count('teg')", "undefined" }, |
| 181 | + .{ "console.count('teg')", "undefined" }, |
| 182 | + .{ "console.count()", "undefined" }, |
| 183 | + .{ "console.countReset('teg')", "undefined" }, |
| 184 | + .{ "console.countReset()", "undefined" }, |
| 185 | + .{ "console.count()", "undefined" }, |
| 186 | + }, .{}); |
| 187 | + |
| 188 | + const captured = test_capture.captured.items; |
| 189 | + try testing.expectEqual("Counter \"default\" doesn't exist.", captured[0]); |
| 190 | + try testing.expectEqual("default: 1", captured[1]); |
| 191 | + try testing.expectEqual("teg: 1", captured[2]); |
| 192 | + try testing.expectEqual("teg: 2", captured[3]); |
| 193 | + try testing.expectEqual("teg: 3", captured[4]); |
| 194 | + try testing.expectEqual("default: 2", captured[5]); |
| 195 | + try testing.expectEqual("teg: 3", captured[6]); |
| 196 | + try testing.expectEqual("default: 2", captured[7]); |
| 197 | + try testing.expectEqual("default: 1", captured[8]); |
| 198 | + } |
| 199 | + |
| 200 | + { |
| 201 | + test_capture.reset(); |
| 202 | + try runner.testCases(&.{ |
| 203 | + .{ "console.assert(true)", "undefined" }, |
| 204 | + .{ "console.assert('a', 2, 3, 4)", "undefined" }, |
| 205 | + .{ "console.assert('')", "undefined" }, |
| 206 | + .{ "console.assert('', 'x', true)", "undefined" }, |
| 207 | + .{ "console.assert(false, 'x')", "undefined" }, |
| 208 | + }, .{}); |
| 209 | + |
| 210 | + const captured = test_capture.captured.items; |
| 211 | + try testing.expectEqual("Assertion failed: ", captured[0]); |
| 212 | + try testing.expectEqual("Assertion failed: x true", captured[1]); |
| 213 | + try testing.expectEqual("Assertion failed: x", captured[2]); |
| 214 | + } |
| 215 | +} |
| 216 | + |
| 217 | +const TestCapture = struct { |
| 218 | + captured: std.ArrayListUnmanaged([]const u8) = .{}, |
| 219 | + |
| 220 | + fn reset(self: *TestCapture) void { |
| 221 | + self.captured = .{}; |
| 222 | + } |
| 223 | + |
| 224 | + fn debug(self: *TestCapture, comptime fmt: []const u8, args: anytype) void { |
| 225 | + const str = std.fmt.allocPrint(testing.arena_allocator, fmt, args) catch unreachable; |
| 226 | + self.captured.append(testing.arena_allocator, str) catch unreachable; |
| 227 | + } |
| 228 | + |
| 229 | + fn info(self: *TestCapture, comptime fmt: []const u8, args: anytype) void { |
| 230 | + self.debug(fmt, args); |
| 231 | + } |
| 232 | + |
| 233 | + fn warn(self: *TestCapture, comptime fmt: []const u8, args: anytype) void { |
| 234 | + self.debug(fmt, args); |
| 235 | + } |
24 | 236 |
|
25 | | - pub fn _log(_: *const Console, str: []const u8) void { |
26 | | - log.debug("{s}\n", .{str}); |
| 237 | + fn err(self: *TestCapture, comptime fmt: []const u8, args: anytype) void { |
| 238 | + self.debug(fmt, args); |
27 | 239 | } |
28 | 240 | }; |
0 commit comments