# selftest_cmatrix.exp — drives cmatrix over a REAL pty (BRINTOS-339). # # cmatrix has no batch mode and writes nothing to disk. It is a full-screen # curses animation whose entire output is escape sequences and glyphs on a # terminal, so unlike nano (58-nano.test) there is no file to read the verdict # off afterwards: the screen IS the artifact, and the pty is the only place it # exists. Everything 82-cmatrix.test can assert without a terminal — --version, # usage, the argument parser — happens before initscr() and proves nothing about # the curses runtime. This payload is where the port is actually tested. # # WHAT EACH VERDICT CLAIMS, and why it is the assertion chosen: # # M1start Any CSI arrives at all. initscr() resolved $TERM=xterm through # /etc/terminfo and ncurses emitted terminfo-derived output. A port # that linked but could not reach the terminfo database dies here. # M2cursoff The specific sequence civis (ESC [ ? 2 5 l). cmatrix calls # curs_set(0) immediately after initscr(), so this is a named # capability making the round trip source -> terminfo -> pty rather # than "some bytes appeared". # M3paint >= 20 DISTINCT glyphs in 33..123 after every escape sequence is # stripped out. This is the one that separates a working animation # from a convincing corpse: cmatrix picks each cell from # randmin=33..highnum=123, so a real rain paints most of that range # within a second, while a build that initialises curses and then # stalls (a broken timer, a wgetch that never returns ERR, a # refresh() that no-ops) still satisfies M1 and M2 and produces # almost no variety. The count is reported as M-glyphs whatever the # verdict, so a near miss is visible rather than just red. # M4quit 'q' typed at the pty reaches wgetch() and finish() exits 0. # The input half of the terminal, and a clean endwin() teardown. # M5saver -s exits on the first keystroke. A different exit path: it runs # the USE_TIOCSTI block, which pushes the typed bytes back with # ioctl(TIOCSTI) before exiting. cmatrix ignores that ioctl's return # value, so an unsupported TIOCSTI is not a failure here -- what is # asserted is that the path still terminates cleanly. # M6red With -C red, an SGR with parameter 31 appears. Colour is not # assumed to work: has_colors() gates start_color() in cmatrix, so # this asserts the terminfo colour capabilities were both read and # emitted. # M7green The same for the default (green, SGR 32) run, so a pass on M6 # cannot come from a colour that happens to be hard-coded. # # WHY THE SCREEN IS DRAINED IN BOUNDED BURSTS. cmatrix never stops painting and # never goes quiet, so an ordinary `expect` on a pattern that is not there runs # to timeout while the buffer grows without limit. drain() reads a fixed number # of chunks, up to a byte cap, and stops early on a silence -- bounded on all # three axes so the payload cannot hang the runner. # # NOT COVERED HERE: whether the rain LOOKS right. Column advance, trail fading, # and the glyph shapes are a rendering judgement no scrape can make; that is a # human at the browser console, and BRINTOS-420 carries it. # # argv: none. # Emits M- OK/FAIL lines plus M-glyphs/M-bytes measurements, then # ALL-CMATRIX-OK iff every verdict passed. log_user 0 set stty_init "rows 24 columns 80" set env(TERM) "xterm" set timeout 6 set pass 0 set total 0 proc verdict {tag ok} { global pass total incr total if {$ok} { incr pass; puts "M-$tag OK" } else { puts "M-$tag FAIL" } } # Read from the current spawn until any one of three bounds is hit: `maxchunks` # reads, `maxbytes` accumulated, or `stall` seconds of silence. proc drain {maxchunks maxbytes stall} { set buf "" set timeout $stall for {set i 0} {$i < $maxchunks} {incr i} { expect { -re {.+} { append buf $expect_out(0,string) } timeout { break } eof { break } } if {[string length $buf] >= $maxbytes} { break } } return $buf } # Walk the stream, skipping ESC-introduced sequences, and count the distinct # printable characters cmatrix actually painted. Done as a scan rather than a # regsub because the glyph alphabet (33..123) contains every character an escape # sequence is built from, so stripping has to follow the sequence structure. proc glyphs {buf} { array set seen {} set n [string length $buf] set i 0 while {$i < $n} { set ch [string index $buf $i] if {$ch eq "\x1b"} { incr i if {[string index $buf $i] eq "\["} { incr i while {$i < $n && [string match {[0-9;?]} [string index $buf $i]]} { incr i } } incr i continue } if {[scan $ch %c code] == 1 && $code >= 33 && $code <= 123} { set seen($ch) 1 } incr i } return [array size seen] } # True when some SGR sequence in the stream carries `want` among its parameters. # Structural for the same reason: a bare [string first "31m"] would also match # three random glyphs landing in that order, which over tens of KB of rain is # not a rare event. proc has_sgr {buf want} { set n [string length $buf] set i 0 while {$i < $n} { if {[string index $buf $i] ne "\x1b"} { incr i; continue } incr i if {[string index $buf $i] ne "\["} { continue } incr i set params "" while {$i < $n && [string match {[0-9;?]} [string index $buf $i]]} { append params [string index $buf $i]; incr i } set final [string index $buf $i]; incr i if {$final eq "m"} { foreach p [split $params ";"] { if {[string is integer -strict $p] && $p == $want} { return 1 } } } } return 0 } # Send `key`, then reap the child UNCONDITIONALLY. Returns 1 iff it left on its # own with status 0 inside `secs`. Tcl's `wait` has no timeout, so a cmatrix # that ignores the keystroke blocks the payload -- and the runner behind it -- # forever; the SIGKILL is what bounds that. MEASURED on this arm: cmatrix # routinely does NOT leave on `q`, so this is the path taken rather than a # defensive branch, and a suite that hangs reports nothing at all about the # seven verdicts above it. proc quit_and_reap {key secs} { set pid [exp_pid] set timeout $secs send $key set left 0 expect { eof { set left 1 } timeout { } } if {!$left} { catch {exec kill -9 $pid} } if {[catch {wait} w]} { return 0 } if {!$left} { return 0 } lassign $w _p _s oserr rc return [expr {$oserr == 0 && $rc == 0}] } # --- S1: the default run — start, paint, quit --------------------------------- # -u 1 is a 10 ms frame delay (update * 10000 us), the fastest cmatrix accepts, # so a second of wall clock is ~100 frames and the glyph census below is over a # large sample rather than one screen. spawn -noecho cmatrix -u 1 set burst [drain 400 40000 2] verdict M1start [expr {[string first "\x1b\[" $burst] >= 0}] verdict M2cursoff [expr {[string first "\x1b\[?25l" $burst] >= 0}] set g [glyphs $burst] puts "M-glyphs $g" puts "M-bytes [string length $burst]" verdict M3paint [expr {$g >= 20}] verdict M7green [has_sgr $burst 32] verdict M4quit [quit_and_reap "q" 6] # --- S2: screensaver mode exits on the first keystroke ------------------------- spawn -noecho cmatrix -s -u 1 drain 100 20000 2 verdict M5saver [quit_and_reap "z" 6] # --- S3: -C red reaches the terminal as SGR 31 -------------------------------- spawn -noecho cmatrix -C red -u 1 set burst3 [drain 200 30000 2] verdict M6red [has_sgr $burst3 31] quit_and_reap "q" 6 puts "M-pass $pass/$total" if {$pass == $total} { puts "ALL-CMATRIX-OK" } exit 0