1// the namespace import is a valid Scope
2log satisfies log.Scope;
3
4describe("log scopes", () => {
5 test("scope metadata attaches to messages", () => {
6 const messages: log.Message[] = [];
7 const root = log.headlessScope((m) => messages.push(m));
8
9 const req = root.scoped("http", { method: "GET", path: "/" });
10 req.info("incoming");
11 req.write("chunk");
12 ASSERT(messages[0]?.custom?.method === "GET");
13 ASSERT(messages[1]?.custom?.path === "/");
14
15 // nested scopes merge their fields over inherited ones
16 const child = req.scoped("auth", { method: "POST", user: "clo" });
17 child.warn("denied");
18 ASSERT(messages[2]?.custom?.method === "POST");
19 ASSERT(messages[2]?.custom?.path === "/");
20 ASSERT(messages[2]?.custom?.user === "clo");
21
22 // sibling scopes are unaffected by the child's overrides
23 req.scoped("other").info("hi");
24 ASSERT(messages[3]?.custom?.method === "GET");
25 ASSERT(messages[3]?.custom?.user === undefined);
26
27 // scopes without metadata emit none
28 root.scoped("plain").info("hello");
29 ASSERT(messages[4]?.custom === undefined);
30 });
31});
32
33// these tests for `startWidget` are built on a custom widget host
34// implementation that buffers all bytes in memory. it is a great
35// example of how modular the entire system is.
36describe("log widgets", () => {
37 test("write buffering", () => {
38 const host = new testing.MockScreen();
39 host.writeOutput("hello world ");
40 host.writeOutput("and so on\n");
41 host.expectFrame(0, {
42 stdout: "hello world and so on\n",
43 });
44 host.writeOutput("more logs\n");
45 host.expectFrame(0, {
46 stdout: "more logs\n",
47 });
48 host.writeOutput("abc def");
49 {
50 using lock = host.getDrawLock("long");
51 host.expectFrame(null, {
52 stdout: "abc def",
53 });
54 lock.release("cursor-start-of-line");
55 }
56 host.expectFrame(null, { stdout: "" });
57 {
58 const lock = host.getDrawLock("long");
59 host.writeOutput("abc def ");
60 host.writeOutput("hijk");
61 lock.release("cursor-start-of-line");
62 host.expectFrame(0, {
63 stdout: "abc def hijk",
64 });
65 }
66 host.expectFrame(null, { stdout: "" });
67 host.cancel();
68 });
69
70 test("widget partial line", () => {
71 const host = new testing.MockScreen();
72 const w1 = host.startWidget({ format: ({ now }) => `t=${now}` });
73 host.expectFrame(0, {
74 stderr: testing.MockScreen.sync([
75 "t=0\n",
76 ]),
77 });
78 host.expectNone();
79 host.writeOutput("partial line");
80 host.expectFrame(0, {
81 merged: testing.MockScreen.sync([
82 ansi.cursorUp(1),
83 ansi.clearFullLine,
84 "partial line",
85 "\n",
86 "t=0\n",
87 ]),
88 stdout: "partial line",
89 stderr: testing.MockScreen.sync([
90 ansi.cursorUp(1),
91 ansi.clearFullLine,
92 "\n",
93 "t=0\n",
94 ]),
95 });
96 host.writeOutput(" and more\n");
97 // partial line
98 // t=0
99 // [C]
100 host.expectFrame(0, {
101 merged: testing.MockScreen.sync([
102 ansi.cursorUp(2) + ansi.cursorRight("partial line".length),
103 " and more\n",
104 "t=0\n",
105 ]),
106 stdout: " and more\n",
107 stderr: testing.MockScreen.sync([
108 ansi.cursorUp(2) + ansi.cursorRight("partial line".length),
109 "t=0\n",
110 ]),
111 });
112 host.cancel();
113 });
114
115 test("widget partial line 2", () => {
116 const host = new testing.MockScreen();
117 const w1 = host.startWidget({ format: () => `abc\ndefg` });
118 host.expectFrame(0, {
119 stderr: testing.MockScreen.sync([
120 "abc\ndefg\n",
121 ]),
122 });
123 // abc
124 // defg
125 host.expectNone();
126 host.writeOutput("partial line");
127 // abc
128 // defg
129 // [c]
130 host.expectFrame(0, {
131 merged: testing.MockScreen.sync([
132 ansi.cursorUp(2),
133 ansi.clearFullLine,
134 "partial line",
135 "\n",
136 "abc",
137 ansi.clearToEndOfLine,
138 "\ndefg\n",
139 ]),
140 });
141 host.writeOutput(" and more\na");
142 // partial line[T]
143 // abc
144 // defg
145 // [CURSOR]
146 // move up 3, right 12
147 host.expectFrame(0, {
148 merged: testing.MockScreen.sync([
149 ansi.cursorUp(2),
150 ansi.clearFullLine,
151 ansi.cursorUp(1) + ansi.cursorRight(12),
152 " and more\n",
153 "a",
154 "\n",
155 "abc",
156 ansi.clearToEndOfLine,
157 "\ndefg\n",
158 ]),
159 });
160 host.cancel();
161 });
162
163 test("widget with interweaving logs", () => {
164 const host = new testing.MockScreen();
165
166 using _ = host.startWidget({
167 format: ({ now }) => `line one ${now}\nline two`,
168 });
169
170 host.expectFrame(0, {
171 stderr: testing.MockScreen.sync([
172 "line one 0\nline two\n",
173 ]),
174 });
175
176 host.writeOutput("info: line of log\n");
177 host.expectFrame(0, {
178 merged: testing.MockScreen.sync([
179 ansi.cursorUp(2),
180 ansi.clearFullLine,
181 "info: line of log\n",
182 "line one 0\nline two\n",
183 ]),
184 });
185
186 host.writeOutput("warn: line of log\n");
187 host.writeOutput("error: line of log\n");
188 host.expectFrame(0, {
189 merged: testing.MockScreen.sync([
190 ansi.cursorUp(1),
191 ansi.clearFullLine,
192 ansi.cursorUp(1),
193 ansi.clearFullLine,
194 "warn: line of log\n",
195 "error: line of log\n",
196 "line one 0\nline two\n",
197 ]),
198 });
199 host.cancel();
200 });
201
202 test("temporary drawing lock", () => {
203 const host = new testing.MockScreen();
204
205 using _ = host.startWidget({
206 format: ({ now }) => `line one ${now}\nline two`,
207 });
208
209 host.expectFrame(0, {
210 stderr: testing.MockScreen.sync([
211 "line one 0\nline two\n",
212 ]),
213 });
214
215 host.writeOutput("info: line of log\n");
216 host.expectFrame(0, {
217 merged: testing.MockScreen.sync([
218 ansi.cursorUp(2),
219 ansi.clearFullLine,
220 "info: line of log\n",
221 "line one 0\nline two\n",
222 ]),
223 });
224
225 {
226 host.writeOutput("warn: line of log\n");
227 host.expectWithoutConsume(0);
228 const lock = host.getDrawLock("short");
229 // info: line of log
230 // line one 0
231 // line two
232 // [C]
233 host.expectFrame(null, {
234 merged: [
235 ansi.syncStart,
236 ansi.cursorUp(1),
237 ansi.clearFullLine,
238 ansi.cursorUp(1),
239 ansi.clearFullLine,
240 "warn: line of log\n",
241 ].join(""),
242 stdout: "warn: line of log\n",
243 });
244 // info: line of log
245 // warn: line of log
246 // [C]
247 const lock2 = host.getDrawLock("long");
248 host.expectNone();
249 host.writeOutput("error: line of log\n");
250 host.expectNone();
251 lock2.release("cursor-start-of-line");
252 host.expectNone();
253 lock.release("cursor-start-of-line");
254 // warn: line of log
255 // [c]
256 host.expectFrame(0, {
257 merged: [
258 "error: line of log\n",
259 "line one 0\nline two\n",
260 ansi.syncEnd,
261 ].join(""),
262 });
263 host.cancel();
264 }
265 {
266 const lock = host.getDrawLock("short");
267 // info: line of log
268 // line one 0
269 // line two
270 // [C]
271 host.expectFrame(null, {
272 merged: [
273 ansi.syncStart,
274 ansi.cursorUp(1),
275 ansi.clearFullLine,
276 ansi.cursorUp(1),
277 ansi.clearFullLine,
278 ].join(""),
279 stdout: "",
280 });
281 const lock2 = host.getDrawLock("long");
282 host.expectNone();
283 lock2.release("cursor-start-of-line");
284 host.expectNone();
285 lock.release("cursor-start-of-line");
286 // warn: line of log
287 // [c]
288 host.expectFrame(0, {
289 merged: [
290 "line one 0\nline two\n",
291 ansi.syncEnd,
292 ].join(""),
293 });
294 host.cancel();
295 }
296 });
297
298 test("draw lock middle of cursor restoration short", () => {
299 const host = new testing.MockScreen();
300
301 using _ = host.startWidget({
302 format: ({ now }) => `line one ${now}\nline two`,
303 });
304
305 host.expectFrame(0, {
306 stderr: testing.MockScreen.sync([
307 "line one 0\nline two\n",
308 ]),
309 });
310
311 host.writeOutput("info: line of log\n");
312 host.expectFrame(0, {
313 merged: testing.MockScreen.sync([
314 ansi.cursorUp(2),
315 ansi.clearFullLine,
316 "info: line of log\n",
317 "line one 0\nline two\n",
318 ]),
319 });
320
321 host.writeOutput("warn: line of log\n");
322 host.expectWithoutConsume(0);
323 const lock = host.getDrawLock("short");
324 // info: line of log
325 // line one 0
326 // line two
327 // [C]
328 host.expectFrame(null, {
329 merged: [
330 ansi.syncStart,
331 ansi.cursorUp(1),
332 ansi.clearFullLine,
333 ansi.cursorUp(1),
334 ansi.clearFullLine,
335 "warn: line of log\n",
336 ].join(""),
337 stdout: "warn: line of log\n",
338 });
339 // info: line of log
340 // warn: line of log
341 // [C]
342 const lock2 = host.getDrawLock("long");
343 host.expectNone();
344 // now we will pretend the environment writes "hello " without
345 // a newline. without extra write calls, it has to save and
346 // restore the cursor perfectly. when multiple locks are held,
347 // it's a bit ambiguous, so the implementation takes the slow path
348 // if any releases are uncertain of the terminal state.
349 lock2.release("cursor-middle-of-line");
350 host.expectNone();
351 lock.release("cursor-start-of-line");
352 // warn: line of log
353 // [c]
354 host.expectFrame(0, {
355 merged: [
356 ansi.cursorSave,
357 "\n",
358 "line one 0\nline two\n",
359 ansi.syncEnd,
360 ].join(""),
361 });
362 const lock3 = host.getDrawLock("short");
363 // hello [S]
364 // line one 0
365 // line two
366 // [C]
367 host.expectFrame(null, {
368 merged: [
369 ansi.syncStart,
370 ansi.cursorUp(1),
371 ansi.clearFullLine,
372 ansi.cursorUp(1),
373 ansi.clearFullLine,
374 ansi.cursorRestore,
375 ].join(""),
376 });
377 lock3.release("cursor-middle-of-line");
378 host.expectFrame(0, {
379 merged: [
380 ansi.cursorSave,
381 "\n",
382 "line one 0\nline two\n",
383 ansi.syncEnd,
384 ].join(""),
385 });
386 host.cancel();
387 });
388
389 test("draw lock middle of cursor restoration long", () => {
390 const host = new testing.MockScreen();
391
392 using _ = host.startWidget({
393 format: ({ now }) => `line one ${now}\nline two`,
394 });
395
396 host.expectFrame(0, {
397 stderr: testing.MockScreen.sync([
398 "line one 0\nline two\n",
399 ]),
400 });
401
402 host.writeOutput("info: line of log\n");
403 host.expectFrame(0, {
404 merged: testing.MockScreen.sync([
405 ansi.cursorUp(2),
406 ansi.clearFullLine,
407 "info: line of log\n",
408 "line one 0\nline two\n",
409 ]),
410 });
411
412 host.writeOutput("warn: line of log\n");
413 host.expectWithoutConsume(0);
414 const lock = host.getDrawLock("long");
415 // info: line of log
416 // line one 0
417 // line two
418 // [C]
419 host.expectFrame(null, {
420 merged: [
421 ansi.syncStart,
422 ansi.cursorUp(1),
423 ansi.clearFullLine,
424 ansi.cursorUp(1),
425 ansi.clearFullLine,
426 ansi.syncEnd,
427 "warn: line of log\n",
428 ].join(""),
429 stdout: "warn: line of log\n",
430 });
431 // info: line of log
432 // warn: line of log
433 // [C]
434 const lock2 = host.getDrawLock("long");
435 host.expectNone();
436 // now we will pretend the environment writeOutput "hello " without
437 // a newline. without extra writeOutput calls, it has to save and
438 // restore the cursor perfectly. when multiple locks are held,
439 // it's a bit ambiguous, so the implementation takes the slow path
440 // if any releases are uncertain of the terminal state.
441 lock2.release("cursor-middle-of-line");
442 host.expectNone();
443 lock.release("cursor-start-of-line");
444 // warn: line of log
445 // [c]
446 host.expectFrame(0, {
447 merged: [
448 ansi.cursorSave,
449 "\n",
450 ansi.syncStart,
451 "line one 0\nline two\n",
452 ansi.syncEnd,
453 ].join(""),
454 });
455 const lock3 = host.getDrawLock("long");
456 // hello [S]
457 // line one 0
458 // line two
459 // [C]
460 host.expectFrame(null, {
461 merged: [
462 ansi.syncStart,
463 ansi.cursorUp(1),
464 ansi.clearFullLine,
465 ansi.cursorUp(1),
466 ansi.clearFullLine,
467 ansi.syncEnd,
468 ansi.cursorRestore,
469 ].join(""),
470 });
471 lock3.release("cursor-middle-of-line");
472 host.expectFrame(0, {
473 merged: [
474 ansi.cursorSave,
475 "\n",
476 ansi.syncStart,
477 "line one 0\nline two\n",
478 ansi.syncEnd,
479 ].join(""),
480 });
481 host.cancel();
482 });
483
484 test("widget expires", () => {
485 const host = new testing.MockScreen();
486
487 host.writeOutput("log line 1\n");
488 host.writeOutput("log line 2\n");
489
490 using _ = host.startWidget({
491 format: ({ now }) => now > 1000 ? null : `widget line one ${now}\nwidget line two`,
492 fps: 1,
493 });
494
495 host.expectFrame(0, {
496 stdout: "log line 1\nlog line 2\n",
497 merged: testing.MockScreen.sync([
498 "log line 1\n",
499 "log line 2\n",
500 "widget line one 0\nwidget line two\n",
501 ]),
502 });
503 host.expectFrame(1000, {
504 merged: testing.MockScreen.sync([
505 ansi.cursorUp(2),
506 "widget line one 1000\nwidget line two\n",
507 ]),
508 });
509 host.expectFrame(1000, {
510 merged: testing.MockScreen.sync([
511 ansi.cursorUp(1),
512 ansi.clearFullLine,
513 ansi.cursorUp(1),
514 ansi.clearFullLine,
515 ]),
516 });
517 host.cancel();
518 });
519
520 test("widget expires partial line output", () => {
521 const host = new testing.MockScreen();
522
523 host.writeOutput("log line;");
524
525 let flag = true;
526 using _ = host.startWidget({
527 format: ({ now }) => flag ? `widget line one ${now}\nwidget line two` : null,
528 fps: 1,
529 });
530
531 host.expectFrame(0, {
532 stdout: "log line;",
533 merged: testing.MockScreen.sync([
534 "log line;",
535 "\n",
536 "widget line one 0\nwidget line two\n",
537 ]),
538 });
539 host.expectWithoutConsume(1000);
540 host.writeOutput(" rest of line\n");
541 flag = false;
542 host.expectFrame(0, {
543 stdout: " rest of line\n",
544 merged: testing.MockScreen.sync([
545 ansi.cursorUp(1),
546 ansi.clearFullLine,
547 ansi.cursorUp(1),
548 ansi.clearFullLine,
549 ansi.cursorUp(1) + ansi.cursorRight("log line;".length),
550 " rest of line\n",
551 ]),
552 });
553 host.cancel();
554 });
555
556 test("empty string keeps the widget alive", () => {
557 const host = new testing.MockScreen();
558 let text = "";
559 const w = UNWRAP(host.startWidget({ format: () => text }));
560 // an empty frame draws nothing, but the widget is not detached
561 host.expectFrame(0, { merged: "" });
562 text = "now!";
563 w.redraw();
564 host.expectFrame(0, {
565 stderr: testing.MockScreen.sync(["now!\n"]),
566 });
567 text = "";
568 w.redraw();
569 host.expectFrame(0, {
570 stderr: testing.MockScreen.sync([
571 ansi.cursorUp(1),
572 ansi.clearFullLine,
573 ]),
574 });
575 w.stop();
576 host.expectFrame(0, { merged: "" });
577 host.cancel();
578 });
579
580 test("leading blank line in widget output", () => {
581 const host = new testing.MockScreen();
582 using _ = host.startWidget({ format: () => "\nfoo" });
583 host.expectFrame(0, {
584 stderr: testing.MockScreen.sync([
585 "\n",
586 "foo\n",
587 ]),
588 });
589 host.cancel();
590 });
591
592 test("empty widget frame with a mid-line draw lock release", () => {
593 // regression: this combination used to crash an assertion, because an
594 // empty first widget line was treated as "no widgets on screen"
595 const host = new testing.MockScreen();
596 let text = "";
597 const w = UNWRAP(host.startWidget({ format: () => ({ text }) }));
598 host.expectFrame(0, { merged: "" });
599 const lock = host.getDrawLock("short");
600 lock.release("cursor-middle-of-line");
601 host.expectFrame(0, { merged: "" });
602 // the foreign partial line is respected once the widget renders
603 text = "hi";
604 w.redraw();
605 host.expectFrame(0, {
606 stderr: ansi.cursorSave + "\n" + testing.MockScreen.sync(["hi\n"]),
607 });
608 host.cancel();
609 });
610
611 test("temporary unlock is used when available", () => {
612 const host = new testing.MockScreen({ temporaryUnlocking: true });
613 using _ = host.startWidget({ format: () => "w" });
614 host.expectFrame(0, {
615 stderr: testing.MockScreen.sync(["w\n"]),
616 });
617 ASSERT(host.hasTerminalLock === "locked");
618 const lock = host.getDrawLock("short");
619 ASSERT(host.hasTerminalLock === "temporary-unlock");
620 host.expectFrame(null, {
621 stderr: ansi.syncStart + ansi.cursorUp(1) + ansi.clearFullLine,
622 });
623 lock.release("cursor-start-of-line");
624 ASSERT(host.hasTerminalLock === "locked");
625 host.expectFrame(0, {
626 stderr: "w\n" + ansi.syncEnd,
627 });
628 host.cancel();
629 });
630
631 test("cursor restore is not replayed for stale saves", () => {
632 const host = new testing.MockScreen();
633 using _ = host.startWidget({ format: () => "w" });
634 host.expectFrame(0, {
635 stderr: testing.MockScreen.sync(["w\n"]),
636 });
637
638 // a foreign partial line forces a cursor save
639 {
640 const lock = host.getDrawLock("short");
641 host.expectFrame(null, {
642 merged: ansi.syncStart + ansi.cursorUp(1) + ansi.clearFullLine,
643 });
644 lock.release("cursor-middle-of-line");
645 host.expectFrame(0, {
646 merged: ansi.cursorSave + "\n" + "w\n" + ansi.syncEnd,
647 });
648 }
649
650 // clearing the widget restores the saved cursor, exactly once
651 {
652 const lock = host.getDrawLock("short");
653 host.expectFrame(null, {
654 merged: ansi.syncStart + ansi.cursorUp(1) + ansi.clearFullLine
655 + ansi.cursorRestore,
656 });
657 lock.release("cursor-start-of-line");
658 host.expectFrame(0, {
659 merged: "w\n" + ansi.syncEnd,
660 });
661 }
662
663 // regression: the stale save register must not be restored again
664 {
665 const lock = host.getDrawLock("short");
666 host.expectFrame(null, {
667 merged: ansi.syncStart + ansi.cursorUp(1) + ansi.clearFullLine,
668 });
669 lock.release("cursor-start-of-line");
670 host.expectFrame(0, {
671 merged: "w\n" + ansi.syncEnd,
672 });
673 }
674 host.cancel();
675 });
676
677 test("log lines wider than the terminal", () => {
678 const host = new testing.MockScreen();
679 host.columns = 10;
680 using _ = host.startWidget({ format: () => "w1\nw2" });
681 host.expectFrame(0, {
682 stderr: testing.MockScreen.sync(["w1\nw2\n"]),
683 });
684 // a 15 cell line occupies two rows, so both widget rows get cleared
685 host.writeOutput("123456789012345\n");
686 host.expectFrame(0, {
687 merged: testing.MockScreen.sync([
688 ansi.cursorUp(1),
689 ansi.clearFullLine,
690 ansi.cursorUp(1),
691 ansi.clearFullLine,
692 "123456789012345\n",
693 "w1\n",
694 "w2\n",
695 ]),
696 });
697 host.cancel();
698 });
699
700 test("partial line wider than the terminal", () => {
701 const host = new testing.MockScreen();
702 host.columns = 10;
703 using _ = host.startWidget({ format: () => "w1\nw2" });
704 host.expectFrame(0, {
705 stderr: testing.MockScreen.sync(["w1\nw2\n"]),
706 });
707 // 14 cells: one wrapped row plus a partial row at column 4
708 host.writeOutput("12345678901234");
709 host.expectFrame(0, {
710 merged: testing.MockScreen.sync([
711 ansi.cursorUp(1),
712 ansi.clearFullLine,
713 ansi.cursorUp(1),
714 ansi.clearFullLine,
715 "12345678901234",
716 "\n",
717 "w1\n",
718 "w2\n",
719 ]),
720 });
721 // the continuation point is column 4 of the partial row, not column 14
722 host.writeOutput(" more\n");
723 host.expectFrame(0, {
724 merged: testing.MockScreen.sync([
725 ansi.cursorUp(3) + ansi.cursorRight(4),
726 " more\n",
727 "w1\n",
728 "w2\n",
729 ]),
730 });
731 host.cancel();
732 });
733
734 test("partial line with ansi codes measures visible width", () => {
735 const host = new testing.MockScreen();
736 using _ = host.startWidget({ format: () => "w" });
737 host.expectFrame(0, {
738 stderr: testing.MockScreen.sync(["w\n"]),
739 });
740 host.writeOutput(ansi.fgBlue + "blue" + ansi.fgReset);
741 host.expectFrame(0, {
742 merged: testing.MockScreen.sync([
743 ansi.cursorUp(1),
744 ansi.clearFullLine,
745 ansi.fgBlue + "blue" + ansi.fgReset,
746 "\n",
747 "w\n",
748 ]),
749 });
750 // "blue" is 4 cells wide; the escape sequences take none
751 host.writeOutput(" sky\n");
752 host.expectFrame(0, {
753 merged: testing.MockScreen.sync([
754 ansi.cursorUp(2) + ansi.cursorRight(4),
755 " sky\n",
756 "w\n",
757 ]),
758 });
759 host.cancel();
760 });
761
762 test("off-screen output does not move widgets", () => {
763 // models stdout redirected to a file while stderr stays the terminal:
764 // log output flushes to stdout, but the widget block is untouched by it
765 const host = new testing.MockScreen({ outputSharesScreen: false });
766 using _ = host.startWidget({ format: () => "w1\nw2" });
767 host.expectFrame(0, {
768 stderr: testing.MockScreen.sync(["w1\nw2\n"]),
769 });
770 host.writeOutput("a long log line that would have wrapped on screen\n");
771 host.expectFrame(0, {
772 stdout: "a long log line that would have wrapped on screen\n",
773 stderr: testing.MockScreen.sync([
774 ansi.cursorUp(2),
775 "w1\nw2\n",
776 ]),
777 });
778 // partial lines in the redirected stream do not shift the widgets either
779 host.writeOutput("partial");
780 host.expectFrame(0, {
781 stdout: "partial",
782 stderr: testing.MockScreen.sync([
783 ansi.cursorUp(2),
784 "w1\nw2\n",
785 ]),
786 });
787 host.cancel();
788 });
789
790 test("writeError routes to the interactive stream in order", () => {
791 const host = new testing.MockScreen();
792 host.writeOutput("out 1\n");
793 host.writeError("err 1\n");
794 host.writeOutput("out 2\n");
795 host.expectFrame(0, {
796 stdout: "out 1\nout 2\n",
797 stderr: "err 1\n",
798 merged: "out 1\nerr 1\nout 2\n",
799 });
800 host.cancel();
801 });
802
803 test("error text moves widgets like output", () => {
804 const host = new testing.MockScreen();
805 using _ = host.startWidget({ format: () => "w" });
806 host.expectFrame(0, { stderr: testing.MockScreen.sync(["w\n"]) });
807 host.writeError("oops\n");
808 host.expectFrame(0, {
809 stdout: "",
810 stderr: testing.MockScreen.sync([
811 ansi.cursorUp(1),
812 ansi.clearFullLine,
813 "oops\n",
814 "w\n",
815 ]),
816 });
817 host.cancel();
818 });
819
820 test("mixed stream partial lines share cursor math", () => {
821 const host = new testing.MockScreen();
822 using _ = host.startWidget({ format: () => "w" });
823 host.expectFrame(0, { stderr: testing.MockScreen.sync(["w\n"]) });
824 host.writeOutput("a");
825 host.writeError("b");
826 host.expectFrame(0, {
827 stdout: "a",
828 merged: testing.MockScreen.sync([
829 ansi.cursorUp(1),
830 ansi.clearFullLine,
831 "a",
832 "b",
833 "\n",
834 "w\n",
835 ]),
836 });
837 // the continuation point is column 2, covering both partial chunks
838 host.writeOutput("c\n");
839 host.expectFrame(0, {
840 merged: testing.MockScreen.sync([
841 ansi.cursorUp(2) + ansi.cursorRight(2),
842 "c\n",
843 "w\n",
844 ]),
845 });
846 host.cancel();
847 });
848
849 test("error text stays on screen when output is redirected", () => {
850 const host = new testing.MockScreen({ outputSharesScreen: false });
851 using _ = host.startWidget({ format: () => "w" });
852 host.expectFrame(0, { stderr: testing.MockScreen.sync(["w\n"]) });
853 host.writeOutput("to the file\n");
854 host.writeError("to the screen\n");
855 host.expectFrame(0, {
856 stdout: "to the file\n",
857 stderr: testing.MockScreen.sync([
858 ansi.cursorUp(1),
859 ansi.clearFullLine,
860 "to the screen\n",
861 "w\n",
862 ]),
863 });
864 host.cancel();
865 });
866
867 test("widgets start below a partial line", () => {
868 const host = new testing.MockScreen();
869 let text = "";
870 const w = UNWRAP(host.startWidget({ format: () => text }));
871 host.expectFrame(0, { merged: "" });
872 host.writeOutput("part");
873 host.expectFrame(0, { stdout: "part", merged: "part" });
874 text = "W";
875 w.redraw();
876 host.expectFrame(0, {
877 stderr: testing.MockScreen.sync(["\n", "W\n"]),
878 });
879 // the partial line can still be continued afterwards
880 host.writeOutput(" two\n");
881 host.expectFrame(0, {
882 merged: testing.MockScreen.sync([
883 ansi.cursorUp(2) + ansi.cursorRight(4),
884 " two\n",
885 "W\n",
886 ]),
887 });
888 host.cancel();
889 });
890
891 test("resize invalidates the widget area", () => {
892 const host = new testing.MockScreen();
893 using _ = host.startWidget({ format: () => "w1\nw2" });
894 host.expectFrame(0, {
895 stderr: testing.MockScreen.sync(["w1\nw2\n"]),
896 });
897 host.resize(40);
898 host.expectFrame(0, {
899 merged: ansi.cursorUp(2) + "\r" + ansi.clearToEndOfScreen
900 + testing.MockScreen.sync(["w1\nw2\n"]),
901 });
902 host.cancel();
903 });
904
905 test("widget redraw during another widget's render", () => {
906 const host = new testing.MockScreen();
907 let kicked = false;
908 const b = UNWRAP(host.startWidget({ format: () => "b" }));
909 using _ = host.startWidget({
910 format: () => {
911 if (!kicked) {
912 kicked = true;
913 b.redraw();
914 }
915 return "a";
916 },
917 });
918 host.expectFrame(0, {
919 stderr: testing.MockScreen.sync(["b\na\n"]),
920 });
921 // the request from inside the render pass produces a follow-up frame
922 host.expectFrame(0, {
923 stderr: testing.MockScreen.sync([
924 ansi.cursorUp(2),
925 "b\na\n",
926 ]),
927 });
928 host.expectNone();
929 host.cancel();
930 });
931
932 test("widget stopped during another widget's render", () => {
933 const host = new testing.MockScreen();
934 let b: log.WidgetInstance | null = null;
935 using _ = host.startWidget({
936 format: () => {
937 b?.stop();
938 b = null;
939 return "a";
940 },
941 });
942 b = host.startWidget({ format: () => "b" });
943 host.expectFrame(0, {
944 stderr: testing.MockScreen.sync(["a\n"]),
945 });
946 // the stop request also schedules a follow-up frame; it draws no change
947 host.expectFrame(0, {
948 stderr: testing.MockScreen.sync([
949 ansi.cursorUp(1),
950 "a\n",
951 ]),
952 });
953 host.expectNone();
954 host.cancel();
955 });
956
957 test("fps setter schedules frames", () => {
958 const host = new testing.MockScreen();
959 const w = UNWRAP(host.startWidget({ format: ({ now }) => `t=${now}` }));
960 host.expectFrame(0, {
961 stderr: testing.MockScreen.sync(["t=0\n"]),
962 });
963 host.expectNone();
964 w.fps = 1;
965 host.expectFrame(0, {
966 stderr: testing.MockScreen.sync([
967 ansi.cursorUp(1),
968 "t=0\n",
969 ]),
970 });
971 host.expectWithoutConsume(1000);
972 w.fps = null;
973 host.expectFrame(0, {
974 stderr: testing.MockScreen.sync([
975 ansi.cursorUp(1),
976 "t=0\n",
977 ]),
978 });
979 host.expectNone();
980 w.stop();
981 host.expectFrame(0, {
982 stderr: testing.MockScreen.sync([
983 ansi.cursorUp(1),
984 ansi.clearFullLine,
985 ]),
986 });
987 host.cancel();
988 });
989
990 test("oversized buffers flush whole lines synchronously", () => {
991 const host = new testing.MockScreen();
992 const line = "x".repeat(8191) + "\n";
993 for (let i = 0; i < 7; i += 1) host.writeOutput(line);
994 ASSERT(host.stdout === "", "flushed before reaching the threshold");
995 // crossing the threshold flushes immediately, but the trailing line
996 // under construction stays buffered
997 host.writeOutput(line + "partial");
998 ASSERT(host.stdout === line.repeat(8), "whole lines were not flushed");
999 host.expectFrame(0, { stdout: line.repeat(8) + "partial" });
1000 host.cancel();
1001 });
1002
1003 test("single line larger than the flush threshold still flushes", () => {
1004 const host = new testing.MockScreen();
1005 const giant = "y".repeat(70000);
1006 host.writeOutput(giant);
1007 // no newline is buffered, so memory is bounded by flushing mid-line
1008 host.expectFrame(null, { stdout: giant });
1009 host.cancel();
1010 });
1011
1012 test("starved redraw timer flushes on the next write", () => {
1013 const host = new testing.MockScreen();
1014 host.writeOutput("one\n");
1015 host.expectWithoutConsume(0);
1016 // simulate a blocked event loop: time advances but no timer fires
1017 host.timers.time += 30;
1018 host.writeOutput("two\n");
1019 ASSERT(host.stdout === "", "flushed before the starvation threshold");
1020 host.timers.time += 30;
1021 host.writeOutput("three\n");
1022 host.expectFrame(null, { stdout: "one\ntwo\nthree\n" });
1023 host.cancel();
1024 });
1025
1026 test("node host patches and restores std streams", async () => {
1027 const proc = UNWRAP(node.process);
1028 const origOut = proc.stdout.write;
1029 const origErr = proc.stderr.write;
1030 let stdoutData = "";
1031 let stderrData = "";
1032 proc.stdout.write = (chunk) => (stdoutData += chunk, true);
1033 proc.stderr.write = (chunk) => (stderrData += chunk, true);
1034 const myOut = proc.stdout.write;
1035 const myErr = proc.stderr.write;
1036 try {
1037 const host = log.defaultNodeProcessWidgetHost(proc, true);
1038 for (let i = 0; i < 3; i += 1) {
1039 host.writeOutput(`line ${i}\n`);
1040 await new Promise((resolve) => setTimeout(resolve, 2));
1041 }
1042 host.cancel();
1043 // regression: the unpatch used to clobber stdout with stderr's methods
1044 // and leave a permanently growing wrapper chain on stderr
1045 ASSERT(proc.stdout.write === myOut, "stdout.write was not restored");
1046 ASSERT(proc.stderr.write === myErr, "stderr.write was not restored");
1047 // log output lands on real stdout, so it stays redirectable
1048 ASSERT(stdoutData.includes("line 0\n"));
1049 ASSERT(stdoutData.includes("line 2\n"));
1050 ASSERT(stderrData === "");
1051 } finally {
1052 proc.stdout.write = origOut;
1053 proc.stderr.write = origErr;
1054 }
1055 });
1056
1057 test("writing during a render function is OK", () => {
1058 const host = new testing.MockScreen();
1059
1060 using w = host.startWidget({
1061 format: ({ now }) => {
1062 host.writeOutput("log line\n");
1063 return "widget line";
1064 },
1065 fps: 1,
1066 });
1067
1068 host.expectFrame(0, {
1069 stdout: "log line\n",
1070 merged: testing.MockScreen.sync([
1071 "log line\n",
1072 "widget line\n",
1073 ]),
1074 });
1075 host.expectFrame(1000, {
1076 stdout: "log line\n",
1077 merged: testing.MockScreen.sync([
1078 ansi.cursorUp(1),
1079 ansi.clearFullLine,
1080 "log line\n",
1081 "widget line\n",
1082 ]),
1083 });
1084 w?.stop();
1085 host.expectFrame(0, {
1086 // stdout: "log line\n",
1087 merged: testing.MockScreen.sync([
1088 ansi.cursorUp(1),
1089 ansi.clearFullLine,
1090 ]),
1091 });
1092 host.cancel();
1093 });
1094});
1095
1096import { describe, test } from "vitest";
1097import { ASSERT, UNWRAP } from "./assert.ts";
1098import * as log from "./log.ts";
1099import * as node from "./node.ts";
1100import * as ansi from "./string/ansi.ts";
1101import * as testing from "./testing.ts";