linecraft 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/plan.md +952 -0
- package/LICENSE +22 -0
- package/README.md +111 -0
- package/TESTING.md +102 -0
- package/build.zig +100 -0
- package/examples/basic-progress.ts +21 -0
- package/examples/multi-lane.ts +29 -0
- package/examples/spinner.ts +20 -0
- package/examples/test-basic.ts +23 -0
- package/lib/components/progress-bar.d.ts +19 -0
- package/lib/components/progress-bar.d.ts.map +1 -0
- package/lib/components/progress-bar.js +43 -0
- package/lib/components/progress-bar.js.map +1 -0
- package/lib/components/spinner.d.ts +18 -0
- package/lib/components/spinner.d.ts.map +1 -0
- package/lib/components/spinner.js +48 -0
- package/lib/components/spinner.js.map +1 -0
- package/lib/index.d.ts +12 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +16 -0
- package/lib/index.js.map +1 -0
- package/lib/native.d.ts +12 -0
- package/lib/native.d.ts.map +1 -0
- package/lib/native.js +65 -0
- package/lib/native.js.map +1 -0
- package/lib/region.d.ts +17 -0
- package/lib/region.d.ts.map +1 -0
- package/lib/region.js +74 -0
- package/lib/region.js.map +1 -0
- package/lib/types.d.ts +32 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/lib/utils/colors.d.ts +3 -0
- package/lib/utils/colors.d.ts.map +1 -0
- package/lib/utils/colors.js +61 -0
- package/lib/utils/colors.js.map +1 -0
- package/package.json +46 -0
- package/src/ts/components/progress-bar.ts +53 -0
- package/src/ts/components/spinner.ts +56 -0
- package/src/ts/index.ts +37 -0
- package/src/ts/native.ts +86 -0
- package/src/ts/region.ts +89 -0
- package/src/ts/types/ffi-napi.d.ts +11 -0
- package/src/ts/types/ref-napi.d.ts +5 -0
- package/src/ts/types.ts +53 -0
- package/src/ts/utils/colors.ts +72 -0
- package/src/zig/ansi.zig +21 -0
- package/src/zig/buffer.zig +37 -0
- package/src/zig/diff.zig +43 -0
- package/src/zig/region.zig +292 -0
- package/src/zig/renderer.zig +92 -0
- package/src/zig/test_ansi.zig +66 -0
- package/src/zig/test_buffer.zig +82 -0
- package/src/zig/test_diff.zig +220 -0
- package/src/zig/test_integration.zig +76 -0
- package/src/zig/test_region.zig +191 -0
- package/src/zig/test_runner.zig +27 -0
- package/src/zig/test_throttle.zig +59 -0
- package/src/zig/throttle.zig +38 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const buffer = @import("buffer.zig");
|
|
3
|
+
const testing = std.testing;
|
|
4
|
+
|
|
5
|
+
test "Buffer: initialization" {
|
|
6
|
+
const stdout = std.fs.File{ .handle = 1 }; // stdout handle
|
|
7
|
+
var buf = buffer.RenderBuffer.init(testing.allocator, stdout);
|
|
8
|
+
defer buf.deinit();
|
|
9
|
+
|
|
10
|
+
try testing.expect(buf.data.items.len == 0);
|
|
11
|
+
std.debug.print(" ✓ Buffer initialized\n", .{});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
test "Buffer: write" {
|
|
15
|
+
const stdout = std.fs.File{ .handle = 1 }; // stdout handle
|
|
16
|
+
var buf = buffer.RenderBuffer.init(testing.allocator, stdout);
|
|
17
|
+
defer buf.deinit();
|
|
18
|
+
|
|
19
|
+
try buf.write("hello");
|
|
20
|
+
try buf.write(" world");
|
|
21
|
+
|
|
22
|
+
try testing.expectEqualStrings("hello world", buf.data.items);
|
|
23
|
+
std.debug.print(" ✓ Buffer write: {s}\n", .{buf.data.items});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test "Buffer: clear" {
|
|
27
|
+
const stdout = std.fs.File{ .handle = 1 }; // stdout handle
|
|
28
|
+
var buf = buffer.RenderBuffer.init(testing.allocator, stdout);
|
|
29
|
+
defer buf.deinit();
|
|
30
|
+
|
|
31
|
+
try buf.write("test");
|
|
32
|
+
buf.clear();
|
|
33
|
+
|
|
34
|
+
try testing.expect(buf.data.items.len == 0);
|
|
35
|
+
std.debug.print(" ✓ Buffer cleared\n", .{});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
test "Buffer: flush" {
|
|
39
|
+
// Use stderr for test output to avoid conflicts with test runner
|
|
40
|
+
// In production, this would be stdout, but for tests stderr is safer
|
|
41
|
+
const stderr = std.fs.File{ .handle = 2 };
|
|
42
|
+
var buf = buffer.RenderBuffer.init(testing.allocator, stderr);
|
|
43
|
+
defer buf.deinit();
|
|
44
|
+
|
|
45
|
+
try buf.write("flush test\n");
|
|
46
|
+
// Flush to stderr - this should work and be visible
|
|
47
|
+
try buf.flush();
|
|
48
|
+
|
|
49
|
+
try testing.expect(buf.data.items.len == 0);
|
|
50
|
+
std.debug.print(" ✓ Buffer flushed (wrote to stderr)\n", .{});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
test "Buffer: write large content" {
|
|
54
|
+
const stdout = std.fs.File{ .handle = 1 }; // stdout handle
|
|
55
|
+
var buf = buffer.RenderBuffer.init(testing.allocator, stdout);
|
|
56
|
+
defer buf.deinit();
|
|
57
|
+
|
|
58
|
+
const large_content = "x" ** 1000;
|
|
59
|
+
try buf.write(large_content);
|
|
60
|
+
|
|
61
|
+
try testing.expect(buf.data.items.len == 1000);
|
|
62
|
+
std.debug.print(" ✓ Buffer handles large content: {} bytes\n", .{buf.data.items.len});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
test "Buffer: multiple writes and flush" {
|
|
66
|
+
// Use stderr for test output
|
|
67
|
+
const stderr = std.fs.File{ .handle = 2 };
|
|
68
|
+
var buf = buffer.RenderBuffer.init(testing.allocator, stderr);
|
|
69
|
+
defer buf.deinit();
|
|
70
|
+
|
|
71
|
+
try buf.write("part1");
|
|
72
|
+
try buf.write("part2");
|
|
73
|
+
try buf.write("part3\n");
|
|
74
|
+
|
|
75
|
+
// Verify all parts are in buffer
|
|
76
|
+
try testing.expectEqualStrings("part1part2part3\n", buf.data.items);
|
|
77
|
+
|
|
78
|
+
// Flush to stderr - should be visible
|
|
79
|
+
try buf.flush();
|
|
80
|
+
try testing.expect(buf.data.items.len == 0);
|
|
81
|
+
std.debug.print(" ✓ Buffer multiple writes and flush works\n", .{});
|
|
82
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const diff = @import("diff.zig");
|
|
3
|
+
const testing = std.testing;
|
|
4
|
+
|
|
5
|
+
test "Diff: identical frames" {
|
|
6
|
+
const allocator = testing.allocator;
|
|
7
|
+
|
|
8
|
+
var prev = std.ArrayList([]u8){};
|
|
9
|
+
defer {
|
|
10
|
+
for (prev.items) |line| allocator.free(line);
|
|
11
|
+
prev.deinit(allocator);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
var curr = std.ArrayList([]u8){};
|
|
15
|
+
defer {
|
|
16
|
+
for (curr.items) |line| allocator.free(line);
|
|
17
|
+
curr.deinit(allocator);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try prev.append(allocator, try allocator.dupe(u8, "line1"));
|
|
21
|
+
try prev.append(allocator, try allocator.dupe(u8, "line2"));
|
|
22
|
+
|
|
23
|
+
try curr.append(allocator, try allocator.dupe(u8, "line1"));
|
|
24
|
+
try curr.append(allocator, try allocator.dupe(u8, "line2"));
|
|
25
|
+
|
|
26
|
+
const ops = try diff.diff_frames(prev.items, curr.items, allocator);
|
|
27
|
+
defer allocator.free(ops);
|
|
28
|
+
|
|
29
|
+
try testing.expect(ops.len == 2);
|
|
30
|
+
for (ops) |op| {
|
|
31
|
+
try testing.expect(op == .no_change);
|
|
32
|
+
}
|
|
33
|
+
std.debug.print(" ✓ Diff identical frames: {} no_change ops\n", .{ops.len});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test "Diff: changed line" {
|
|
37
|
+
const allocator = testing.allocator;
|
|
38
|
+
|
|
39
|
+
var prev = std.ArrayList([]u8){};
|
|
40
|
+
defer {
|
|
41
|
+
for (prev.items) |line| allocator.free(line);
|
|
42
|
+
prev.deinit(allocator);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
var curr = std.ArrayList([]u8){};
|
|
46
|
+
defer {
|
|
47
|
+
for (curr.items) |line| allocator.free(line);
|
|
48
|
+
curr.deinit(allocator);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try prev.append(allocator, try allocator.dupe(u8, "line1"));
|
|
52
|
+
try prev.append(allocator, try allocator.dupe(u8, "line2"));
|
|
53
|
+
|
|
54
|
+
try curr.append(allocator, try allocator.dupe(u8, "line1"));
|
|
55
|
+
try curr.append(allocator, try allocator.dupe(u8, "line2_changed"));
|
|
56
|
+
|
|
57
|
+
const ops = try diff.diff_frames(prev.items, curr.items, allocator);
|
|
58
|
+
defer allocator.free(ops);
|
|
59
|
+
|
|
60
|
+
try testing.expect(ops.len == 2);
|
|
61
|
+
try testing.expect(ops[0] == .no_change);
|
|
62
|
+
try testing.expect(ops[1] == .update_line);
|
|
63
|
+
if (ops[1] == .update_line) {
|
|
64
|
+
try testing.expectEqual(@as(u32, 1), ops[1].update_line.line);
|
|
65
|
+
try testing.expectEqualStrings("line2_changed", ops[1].update_line.content);
|
|
66
|
+
}
|
|
67
|
+
std.debug.print(" ✓ Diff changed line detected\n", .{});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
test "Diff: inserted line" {
|
|
71
|
+
const allocator = testing.allocator;
|
|
72
|
+
|
|
73
|
+
var prev = std.ArrayList([]u8){};
|
|
74
|
+
defer {
|
|
75
|
+
for (prev.items) |line| allocator.free(line);
|
|
76
|
+
prev.deinit(allocator);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
var curr = std.ArrayList([]u8){};
|
|
80
|
+
defer {
|
|
81
|
+
for (curr.items) |line| allocator.free(line);
|
|
82
|
+
curr.deinit(allocator);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try prev.append(allocator, try allocator.dupe(u8, "line1"));
|
|
86
|
+
|
|
87
|
+
try curr.append(allocator, try allocator.dupe(u8, "line1"));
|
|
88
|
+
try curr.append(allocator, try allocator.dupe(u8, "line2"));
|
|
89
|
+
|
|
90
|
+
const ops = try diff.diff_frames(prev.items, curr.items, allocator);
|
|
91
|
+
defer allocator.free(ops);
|
|
92
|
+
|
|
93
|
+
try testing.expect(ops.len == 2);
|
|
94
|
+
try testing.expect(ops[0] == .no_change);
|
|
95
|
+
try testing.expect(ops[1] == .insert_line);
|
|
96
|
+
if (ops[1] == .insert_line) {
|
|
97
|
+
try testing.expectEqual(@as(u32, 1), ops[1].insert_line.line);
|
|
98
|
+
try testing.expectEqualStrings("line2", ops[1].insert_line.content);
|
|
99
|
+
}
|
|
100
|
+
std.debug.print(" ✓ Diff inserted line detected\n", .{});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
test "Diff: deleted line" {
|
|
104
|
+
const allocator = testing.allocator;
|
|
105
|
+
|
|
106
|
+
var prev = std.ArrayList([]u8){};
|
|
107
|
+
defer {
|
|
108
|
+
for (prev.items) |line| allocator.free(line);
|
|
109
|
+
prev.deinit(allocator);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
var curr = std.ArrayList([]u8){};
|
|
113
|
+
defer {
|
|
114
|
+
for (curr.items) |line| allocator.free(line);
|
|
115
|
+
curr.deinit(allocator);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try prev.append(allocator, try allocator.dupe(u8, "line1"));
|
|
119
|
+
try prev.append(allocator, try allocator.dupe(u8, "line2"));
|
|
120
|
+
|
|
121
|
+
try curr.append(allocator, try allocator.dupe(u8, "line1"));
|
|
122
|
+
|
|
123
|
+
const ops = try diff.diff_frames(prev.items, curr.items, allocator);
|
|
124
|
+
defer allocator.free(ops);
|
|
125
|
+
|
|
126
|
+
try testing.expect(ops.len == 2);
|
|
127
|
+
try testing.expect(ops[0] == .no_change);
|
|
128
|
+
try testing.expect(ops[1] == .delete_line);
|
|
129
|
+
if (ops[1] == .delete_line) {
|
|
130
|
+
try testing.expectEqual(@as(u32, 1), ops[1].delete_line);
|
|
131
|
+
}
|
|
132
|
+
std.debug.print(" ✓ Diff deleted line detected\n", .{});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
test "Diff: multiple changes" {
|
|
136
|
+
const allocator = testing.allocator;
|
|
137
|
+
|
|
138
|
+
var prev = std.ArrayList([]u8){};
|
|
139
|
+
defer {
|
|
140
|
+
for (prev.items) |line| allocator.free(line);
|
|
141
|
+
prev.deinit(allocator);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
var curr = std.ArrayList([]u8){};
|
|
145
|
+
defer {
|
|
146
|
+
for (curr.items) |line| allocator.free(line);
|
|
147
|
+
curr.deinit(allocator);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try prev.append(allocator, try allocator.dupe(u8, "line1"));
|
|
151
|
+
try prev.append(allocator, try allocator.dupe(u8, "line2"));
|
|
152
|
+
try prev.append(allocator, try allocator.dupe(u8, "line3"));
|
|
153
|
+
|
|
154
|
+
try curr.append(allocator, try allocator.dupe(u8, "line1_changed"));
|
|
155
|
+
try curr.append(allocator, try allocator.dupe(u8, "line2"));
|
|
156
|
+
try curr.append(allocator, try allocator.dupe(u8, "line3"));
|
|
157
|
+
try curr.append(allocator, try allocator.dupe(u8, "line4"));
|
|
158
|
+
|
|
159
|
+
const ops = try diff.diff_frames(prev.items, curr.items, allocator);
|
|
160
|
+
defer allocator.free(ops);
|
|
161
|
+
|
|
162
|
+
try testing.expect(ops.len == 4);
|
|
163
|
+
try testing.expect(ops[0] == .update_line);
|
|
164
|
+
try testing.expect(ops[1] == .no_change);
|
|
165
|
+
try testing.expect(ops[2] == .no_change);
|
|
166
|
+
try testing.expect(ops[3] == .insert_line);
|
|
167
|
+
std.debug.print(" ✓ Diff multiple changes: {} ops\n", .{ops.len});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
test "Diff: empty frames" {
|
|
171
|
+
const allocator = testing.allocator;
|
|
172
|
+
|
|
173
|
+
var prev = std.ArrayList([]u8){};
|
|
174
|
+
defer {
|
|
175
|
+
for (prev.items) |line| allocator.free(line);
|
|
176
|
+
prev.deinit(allocator);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
var curr = std.ArrayList([]u8){};
|
|
180
|
+
defer {
|
|
181
|
+
for (curr.items) |line| allocator.free(line);
|
|
182
|
+
curr.deinit(allocator);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const ops = try diff.diff_frames(prev.items, curr.items, allocator);
|
|
186
|
+
defer allocator.free(ops);
|
|
187
|
+
|
|
188
|
+
try testing.expect(ops.len == 0);
|
|
189
|
+
std.debug.print(" ✓ Diff empty frames: {} ops\n", .{ops.len});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
test "Diff: completely different frames" {
|
|
193
|
+
const allocator = testing.allocator;
|
|
194
|
+
|
|
195
|
+
var prev = std.ArrayList([]u8){};
|
|
196
|
+
defer {
|
|
197
|
+
for (prev.items) |line| allocator.free(line);
|
|
198
|
+
prev.deinit(allocator);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
var curr = std.ArrayList([]u8){};
|
|
202
|
+
defer {
|
|
203
|
+
for (curr.items) |line| allocator.free(line);
|
|
204
|
+
curr.deinit(allocator);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try prev.append(allocator, try allocator.dupe(u8, "old1"));
|
|
208
|
+
try prev.append(allocator, try allocator.dupe(u8, "old2"));
|
|
209
|
+
|
|
210
|
+
try curr.append(allocator, try allocator.dupe(u8, "new1"));
|
|
211
|
+
try curr.append(allocator, try allocator.dupe(u8, "new2"));
|
|
212
|
+
|
|
213
|
+
const ops = try diff.diff_frames(prev.items, curr.items, allocator);
|
|
214
|
+
defer allocator.free(ops);
|
|
215
|
+
|
|
216
|
+
try testing.expect(ops.len == 2);
|
|
217
|
+
try testing.expect(ops[0] == .update_line);
|
|
218
|
+
try testing.expect(ops[1] == .update_line);
|
|
219
|
+
std.debug.print(" ✓ Diff completely different frames: {} update ops\n", .{ops.len});
|
|
220
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Integration tests that test multiple modules together
|
|
2
|
+
const std = @import("std");
|
|
3
|
+
const region = @import("region.zig");
|
|
4
|
+
const testing = std.testing;
|
|
5
|
+
|
|
6
|
+
test "Integration: region with diff and throttle" {
|
|
7
|
+
const allocator = testing.allocator;
|
|
8
|
+
const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
|
|
9
|
+
|
|
10
|
+
var r = try region.Region.init(allocator, 0, 0, 80, 2, stdout);
|
|
11
|
+
r.disable_rendering = true; // Disable actual rendering in tests
|
|
12
|
+
defer r.deinit();
|
|
13
|
+
|
|
14
|
+
// Set initial lines
|
|
15
|
+
try r.set_line(1, "initial line 1");
|
|
16
|
+
try r.set_line(2, "initial line 2");
|
|
17
|
+
|
|
18
|
+
// Change one line
|
|
19
|
+
try r.set_line(1, "updated line 1");
|
|
20
|
+
|
|
21
|
+
// Verify pending frame has updates
|
|
22
|
+
try testing.expectEqualStrings("updated line 1", r.pending_frame.items[0]);
|
|
23
|
+
try testing.expectEqualStrings("initial line 2", r.pending_frame.items[1]);
|
|
24
|
+
|
|
25
|
+
std.debug.print(" ✓ Integration: region diff and throttle work together\n", .{});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test "Integration: region expansion with multiple operations" {
|
|
29
|
+
const allocator = testing.allocator;
|
|
30
|
+
const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
|
|
31
|
+
|
|
32
|
+
var r = try region.Region.init(allocator, 0, 0, 80, 1, stdout);
|
|
33
|
+
r.disable_rendering = true; // Disable actual rendering in tests
|
|
34
|
+
defer r.deinit();
|
|
35
|
+
|
|
36
|
+
// Start with 1 line, expand to 5
|
|
37
|
+
try r.set_line(1, "line1");
|
|
38
|
+
try r.set_line(2, "line2");
|
|
39
|
+
try r.set_line(3, "line3");
|
|
40
|
+
try r.set_line(4, "line4");
|
|
41
|
+
try r.set_line(5, "line5");
|
|
42
|
+
|
|
43
|
+
try testing.expect(r.height >= 5);
|
|
44
|
+
try testing.expect(r.pending_frame.items.len >= 5);
|
|
45
|
+
|
|
46
|
+
// Verify all lines are correct
|
|
47
|
+
try testing.expectEqualStrings("line1", r.pending_frame.items[0]);
|
|
48
|
+
try testing.expectEqualStrings("line2", r.pending_frame.items[1]);
|
|
49
|
+
try testing.expectEqualStrings("line3", r.pending_frame.items[2]);
|
|
50
|
+
try testing.expectEqualStrings("line4", r.pending_frame.items[3]);
|
|
51
|
+
try testing.expectEqualStrings("line5", r.pending_frame.items[4]);
|
|
52
|
+
|
|
53
|
+
std.debug.print(" ✓ Integration: region expansion with multiple operations\n", .{});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
test "Integration: set then set_line" {
|
|
57
|
+
const allocator = testing.allocator;
|
|
58
|
+
const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
|
|
59
|
+
|
|
60
|
+
var r = try region.Region.init(allocator, 0, 0, 80, 1, stdout);
|
|
61
|
+
r.disable_rendering = true; // Disable actual rendering in tests
|
|
62
|
+
defer r.deinit();
|
|
63
|
+
|
|
64
|
+
// Set entire content
|
|
65
|
+
try r.set("line1\nline2\nline3");
|
|
66
|
+
|
|
67
|
+
// Then update individual line
|
|
68
|
+
try r.set_line(2, "line2_updated");
|
|
69
|
+
|
|
70
|
+
try testing.expect(r.pending_frame.items.len >= 3);
|
|
71
|
+
try testing.expectEqualStrings("line1", r.pending_frame.items[0]);
|
|
72
|
+
try testing.expectEqualStrings("line2_updated", r.pending_frame.items[1]);
|
|
73
|
+
try testing.expectEqualStrings("line3", r.pending_frame.items[2]);
|
|
74
|
+
|
|
75
|
+
std.debug.print(" ✓ Integration: set then set_line works correctly\n", .{});
|
|
76
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const region = @import("region.zig");
|
|
3
|
+
const testing = std.testing;
|
|
4
|
+
|
|
5
|
+
test "Region: initialization" {
|
|
6
|
+
const allocator = testing.allocator;
|
|
7
|
+
const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
|
|
8
|
+
|
|
9
|
+
var r = try region.Region.init(allocator, 0, 0, 80, 5, stdout);
|
|
10
|
+
r.disable_rendering = true; // Disable actual rendering in tests
|
|
11
|
+
defer r.deinit();
|
|
12
|
+
|
|
13
|
+
try testing.expectEqual(@as(u32, 0), r.x);
|
|
14
|
+
try testing.expectEqual(@as(u32, 0), r.y);
|
|
15
|
+
try testing.expectEqual(@as(u32, 80), r.width);
|
|
16
|
+
try testing.expectEqual(@as(u32, 5), r.height);
|
|
17
|
+
try testing.expect(r.pending_frame.items.len == 5);
|
|
18
|
+
try testing.expect(r.previous_frame.items.len == 5);
|
|
19
|
+
std.debug.print(" ✓ Region initialized: {}x{} at ({}, {})\n", .{ r.width, r.height, r.x, r.y });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test "Region: set_line expands automatically" {
|
|
23
|
+
const allocator = testing.allocator;
|
|
24
|
+
const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
|
|
25
|
+
|
|
26
|
+
var r = try region.Region.init(allocator, 0, 0, 80, 1, stdout);
|
|
27
|
+
r.disable_rendering = true; // Disable actual rendering in tests
|
|
28
|
+
defer r.deinit();
|
|
29
|
+
|
|
30
|
+
// Set line 1 (should work)
|
|
31
|
+
try r.set_line(1, "line1");
|
|
32
|
+
try testing.expectEqual(@as(u32, 1), r.height);
|
|
33
|
+
|
|
34
|
+
// Set line 5 (should expand)
|
|
35
|
+
try r.set_line(5, "line5");
|
|
36
|
+
try testing.expect(r.height >= 5);
|
|
37
|
+
try testing.expect(r.pending_frame.items.len >= 5);
|
|
38
|
+
std.debug.print(" ✓ Region expanded from 1 to {} lines\n", .{r.height});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test "Region: set_line rejects line 0" {
|
|
42
|
+
const allocator = testing.allocator;
|
|
43
|
+
const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
|
|
44
|
+
|
|
45
|
+
var r = try region.Region.init(allocator, 0, 0, 80, 1, stdout);
|
|
46
|
+
r.disable_rendering = true; // Disable actual rendering in tests
|
|
47
|
+
defer r.deinit();
|
|
48
|
+
|
|
49
|
+
const result = r.set_line(0, "invalid");
|
|
50
|
+
try testing.expectError(error.InvalidLineNumber, result);
|
|
51
|
+
std.debug.print(" ✓ Region correctly rejects line 0\n", .{});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
test "Region: set with newlines" {
|
|
55
|
+
const allocator = testing.allocator;
|
|
56
|
+
const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
|
|
57
|
+
|
|
58
|
+
var r = try region.Region.init(allocator, 0, 0, 80, 1, stdout);
|
|
59
|
+
r.disable_rendering = true; // Disable actual rendering in tests
|
|
60
|
+
defer r.deinit();
|
|
61
|
+
|
|
62
|
+
try r.set("line1\nline2\nline3");
|
|
63
|
+
try testing.expect(r.height >= 3);
|
|
64
|
+
try testing.expect(r.pending_frame.items.len >= 3);
|
|
65
|
+
try testing.expectEqualStrings("line1", r.pending_frame.items[0]);
|
|
66
|
+
try testing.expectEqualStrings("line2", r.pending_frame.items[1]);
|
|
67
|
+
try testing.expectEqualStrings("line3", r.pending_frame.items[2]);
|
|
68
|
+
std.debug.print(" ✓ Region set with {} lines\n", .{r.pending_frame.items.len});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
test "Region: expand_to" {
|
|
72
|
+
const allocator = testing.allocator;
|
|
73
|
+
const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
|
|
74
|
+
|
|
75
|
+
var r = try region.Region.init(allocator, 0, 0, 80, 2, stdout);
|
|
76
|
+
r.disable_rendering = true; // Disable actual rendering in tests
|
|
77
|
+
defer r.deinit();
|
|
78
|
+
|
|
79
|
+
try r.expand_to(10);
|
|
80
|
+
try testing.expectEqual(@as(u32, 10), r.height);
|
|
81
|
+
try testing.expect(r.pending_frame.items.len == 10);
|
|
82
|
+
try testing.expect(r.previous_frame.items.len == 10);
|
|
83
|
+
std.debug.print(" ✓ Region expanded to {} lines\n", .{r.height});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
test "Region: clear_line" {
|
|
87
|
+
const allocator = testing.allocator;
|
|
88
|
+
const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
|
|
89
|
+
|
|
90
|
+
var r = try region.Region.init(allocator, 0, 0, 80, 3, stdout);
|
|
91
|
+
r.disable_rendering = true; // Disable actual rendering in tests
|
|
92
|
+
defer r.deinit();
|
|
93
|
+
|
|
94
|
+
try r.set_line(2, "test");
|
|
95
|
+
try r.clear_line(2);
|
|
96
|
+
try testing.expectEqualStrings("", r.pending_frame.items[1]);
|
|
97
|
+
std.debug.print(" ✓ Region clear_line works\n", .{});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
test "Region: throttle configuration" {
|
|
101
|
+
const allocator = testing.allocator;
|
|
102
|
+
const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
|
|
103
|
+
|
|
104
|
+
var r = try region.Region.init(allocator, 0, 0, 80, 1, stdout);
|
|
105
|
+
r.disable_rendering = true; // Disable actual rendering in tests
|
|
106
|
+
defer r.deinit();
|
|
107
|
+
|
|
108
|
+
r.set_throttle_fps(30);
|
|
109
|
+
try testing.expectEqual(@as(u32, 30), r.throttle_state.fps);
|
|
110
|
+
std.debug.print(" ✓ Region throttle set to {} FPS\n", .{r.throttle_state.fps});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
test "Region: set_line with empty string" {
|
|
114
|
+
const allocator = testing.allocator;
|
|
115
|
+
const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
|
|
116
|
+
|
|
117
|
+
var r = try region.Region.init(allocator, 0, 0, 80, 2, stdout);
|
|
118
|
+
r.disable_rendering = true; // Disable actual rendering in tests
|
|
119
|
+
defer r.deinit();
|
|
120
|
+
|
|
121
|
+
try r.set_line(1, "test");
|
|
122
|
+
try r.set_line(1, "");
|
|
123
|
+
try testing.expectEqualStrings("", r.pending_frame.items[0]);
|
|
124
|
+
std.debug.print(" ✓ Region set_line with empty string works\n", .{});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
test "Region: set with empty content" {
|
|
128
|
+
const allocator = testing.allocator;
|
|
129
|
+
const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
|
|
130
|
+
|
|
131
|
+
var r = try region.Region.init(allocator, 0, 0, 80, 1, stdout);
|
|
132
|
+
r.disable_rendering = true; // Disable actual rendering in tests
|
|
133
|
+
defer r.deinit();
|
|
134
|
+
|
|
135
|
+
try r.set("");
|
|
136
|
+
try testing.expect(r.pending_frame.items.len >= 1);
|
|
137
|
+
std.debug.print(" ✓ Region set with empty content works\n", .{});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
test "Region: set with single line (no newline)" {
|
|
141
|
+
const allocator = testing.allocator;
|
|
142
|
+
const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
|
|
143
|
+
|
|
144
|
+
var r = try region.Region.init(allocator, 0, 0, 80, 1, stdout);
|
|
145
|
+
r.disable_rendering = true; // Disable actual rendering in tests
|
|
146
|
+
defer r.deinit();
|
|
147
|
+
|
|
148
|
+
try r.set("single line");
|
|
149
|
+
try testing.expect(r.pending_frame.items.len >= 1);
|
|
150
|
+
try testing.expectEqualStrings("single line", r.pending_frame.items[0]);
|
|
151
|
+
std.debug.print(" ✓ Region set with single line works\n", .{});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
test "Region: multiple set_line calls" {
|
|
155
|
+
const allocator = testing.allocator;
|
|
156
|
+
const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
|
|
157
|
+
|
|
158
|
+
var r = try region.Region.init(allocator, 0, 0, 80, 1, stdout);
|
|
159
|
+
r.disable_rendering = true; // Disable actual rendering in tests
|
|
160
|
+
defer r.deinit();
|
|
161
|
+
|
|
162
|
+
try r.set_line(1, "line1");
|
|
163
|
+
try r.set_line(2, "line2");
|
|
164
|
+
try r.set_line(3, "line3");
|
|
165
|
+
|
|
166
|
+
try testing.expect(r.height >= 3);
|
|
167
|
+
try testing.expectEqualStrings("line1", r.pending_frame.items[0]);
|
|
168
|
+
try testing.expectEqualStrings("line2", r.pending_frame.items[1]);
|
|
169
|
+
try testing.expectEqualStrings("line3", r.pending_frame.items[2]);
|
|
170
|
+
std.debug.print(" ✓ Multiple set_line calls work correctly\n", .{});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
test "Region: clear all lines" {
|
|
174
|
+
const allocator = testing.allocator;
|
|
175
|
+
const stdout = std.fs.File{ .handle = 2 }; // Use stderr for tests to avoid blocking
|
|
176
|
+
|
|
177
|
+
var r = try region.Region.init(allocator, 0, 0, 80, 3, stdout);
|
|
178
|
+
r.disable_rendering = true; // Disable actual rendering in tests
|
|
179
|
+
defer r.deinit();
|
|
180
|
+
|
|
181
|
+
try r.set_line(1, "line1");
|
|
182
|
+
try r.set_line(2, "line2");
|
|
183
|
+
try r.set_line(3, "line3");
|
|
184
|
+
try r.clear();
|
|
185
|
+
|
|
186
|
+
// All lines should be empty
|
|
187
|
+
for (r.pending_frame.items) |line| {
|
|
188
|
+
try testing.expectEqualStrings("", line);
|
|
189
|
+
}
|
|
190
|
+
std.debug.print(" ✓ Region clear works for all lines\n", .{});
|
|
191
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Test runner that imports and runs all test modules
|
|
2
|
+
// Zig's built-in test runner will discover all test functions
|
|
3
|
+
const std = @import("std");
|
|
4
|
+
|
|
5
|
+
// Import all test modules so their tests are discovered
|
|
6
|
+
// Zig's test runner automatically discovers all test functions in imported modules
|
|
7
|
+
// We use unique names to avoid duplicate declaration errors
|
|
8
|
+
const _test_ansi = @import("test_ansi.zig");
|
|
9
|
+
const _test_throttle = @import("test_throttle.zig");
|
|
10
|
+
const _test_buffer = @import("test_buffer.zig");
|
|
11
|
+
const _test_diff = @import("test_diff.zig");
|
|
12
|
+
const _test_region = @import("test_region.zig");
|
|
13
|
+
const _test_integration = @import("test_integration.zig");
|
|
14
|
+
|
|
15
|
+
// Suppress unused variable warnings
|
|
16
|
+
comptime {
|
|
17
|
+
_ = _test_ansi;
|
|
18
|
+
_ = _test_throttle;
|
|
19
|
+
_ = _test_buffer;
|
|
20
|
+
_ = _test_diff;
|
|
21
|
+
_ = _test_region;
|
|
22
|
+
_ = _test_integration;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// This file serves as the root for test discovery
|
|
26
|
+
// All test functions in imported modules will be run
|
|
27
|
+
// Each test prints its own output using std.debug.print() for visibility
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const throttle = @import("throttle.zig");
|
|
3
|
+
const testing = std.testing;
|
|
4
|
+
|
|
5
|
+
test "Throttle: initialization" {
|
|
6
|
+
const t = throttle.Throttle.init(60);
|
|
7
|
+
try testing.expectEqual(@as(u32, 60), t.fps);
|
|
8
|
+
try testing.expect(t.min_frame_interval > 0);
|
|
9
|
+
std.debug.print(" ✓ Throttle initialized: {} FPS, {}ns interval\n", .{ t.fps, t.min_frame_interval });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
test "Throttle: set_fps" {
|
|
13
|
+
var t = throttle.Throttle.init(30);
|
|
14
|
+
t.set_fps(120);
|
|
15
|
+
try testing.expectEqual(@as(u32, 120), t.fps);
|
|
16
|
+
std.debug.print(" ✓ Throttle FPS changed to: {}\n", .{t.fps});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test "Throttle: should_render allows first frame" {
|
|
20
|
+
var t = throttle.Throttle.init(60);
|
|
21
|
+
// First call should always allow render
|
|
22
|
+
const should = t.should_render();
|
|
23
|
+
try testing.expect(should);
|
|
24
|
+
std.debug.print(" ✓ First frame allowed\n", .{});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test "Throttle: should_render respects interval" {
|
|
28
|
+
var t = throttle.Throttle.init(1000); // 1 FPS = 1 second interval
|
|
29
|
+
_ = t.should_render(); // First frame
|
|
30
|
+
|
|
31
|
+
// Immediately after, should not render
|
|
32
|
+
const should = t.should_render();
|
|
33
|
+
try testing.expect(!should);
|
|
34
|
+
std.debug.print(" ✓ Throttle correctly blocks immediate second frame\n", .{});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
test "Throttle: time_until_next_frame" {
|
|
38
|
+
var t = throttle.Throttle.init(10); // 10 FPS = 100ms interval
|
|
39
|
+
_ = t.should_render();
|
|
40
|
+
|
|
41
|
+
const remaining = t.time_until_next_frame();
|
|
42
|
+
try testing.expect(remaining > 0);
|
|
43
|
+
try testing.expect(remaining <= 100_000_000); // Should be <= 100ms in nanoseconds
|
|
44
|
+
std.debug.print(" ✓ Time until next frame: {}ns\n", .{remaining});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test "Throttle: very high FPS" {
|
|
48
|
+
const t = throttle.Throttle.init(1000); // 1000 FPS
|
|
49
|
+
try testing.expectEqual(@as(u32, 1000), t.fps);
|
|
50
|
+
try testing.expect(t.min_frame_interval > 0);
|
|
51
|
+
std.debug.print(" ✓ Throttle handles high FPS: {} FPS, {}ns interval\n", .{ t.fps, t.min_frame_interval });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
test "Throttle: very low FPS" {
|
|
55
|
+
const t = throttle.Throttle.init(1); // 1 FPS
|
|
56
|
+
try testing.expectEqual(@as(u32, 1), t.fps);
|
|
57
|
+
try testing.expect(t.min_frame_interval == 1_000_000_000); // 1 second
|
|
58
|
+
std.debug.print(" ✓ Throttle handles low FPS: {} FPS, {}ns interval\n", .{ t.fps, t.min_frame_interval });
|
|
59
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Throttling for frame rate limiting
|
|
2
|
+
const std = @import("std");
|
|
3
|
+
|
|
4
|
+
pub const Throttle = struct {
|
|
5
|
+
last_frame_time: i64,
|
|
6
|
+
min_frame_interval: i64, // nanoseconds
|
|
7
|
+
fps: u32,
|
|
8
|
+
|
|
9
|
+
pub fn init(fps: u32) Throttle {
|
|
10
|
+
const interval_ns = @divTrunc(1_000_000_000, @as(i64, fps));
|
|
11
|
+
return .{
|
|
12
|
+
.last_frame_time = 0,
|
|
13
|
+
.min_frame_interval = interval_ns,
|
|
14
|
+
.fps = fps,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pub fn set_fps(self: *Throttle, fps: u32) void {
|
|
19
|
+
self.fps = fps;
|
|
20
|
+
self.min_frame_interval = @divTrunc(1_000_000_000, @as(i64, fps));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
pub fn should_render(self: *Throttle) bool {
|
|
24
|
+
const now = @as(i64, @intCast(std.time.nanoTimestamp()));
|
|
25
|
+
if (now - self.last_frame_time >= self.min_frame_interval) {
|
|
26
|
+
self.last_frame_time = now;
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
pub fn time_until_next_frame(self: *Throttle) i64 {
|
|
33
|
+
const now = @as(i64, @intCast(std.time.nanoTimestamp()));
|
|
34
|
+
const elapsed = now - self.last_frame_time;
|
|
35
|
+
const remaining = self.min_frame_interval - elapsed;
|
|
36
|
+
return if (remaining > 0) @as(i64, @intCast(remaining)) else 0;
|
|
37
|
+
}
|
|
38
|
+
};
|