diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ff3353..b936a6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,22 @@ jobs: - run: make - run: make test-terminals + esctest: + runs-on: ubuntu-24.04 + continue-on-error: true + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - run: sudo apt-get update && sudo apt-get install -y libncurses-dev python3-pexpect + - run: make + - run: git clone https://github.com/ThomasDickey/esctest2.git "$RUNNER_TEMP/esctest2" + - run: python3 test/run_esctest.py "$RUNNER_TEMP/esctest2" | tee esctest.txt + - if: always() + uses: actions/upload-artifact@v4 + with: + name: esctest + path: esctest.txt + fuzz: runs-on: ubuntu-24.04 steps: diff --git a/test/run_esctest.py b/test/run_esctest.py new file mode 100644 index 0000000..0a78799 --- /dev/null +++ b/test/run_esctest.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Run esctest (https://github.com/ThomasDickey/esctest2) inside dvtm. + +usage: run_esctest.py /path/to/esctest2 [extra esctest.py arguments] + +Prints the failing tests and a summary. Exits 0 even when tests fail, since +this is a conformance report and not a gate. +""" +import os +import re +import shlex +import sys +import tempfile +import time + +import pexpect + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +TIMEOUT = 1500 + + +def main(): + esctest = os.path.join(os.path.abspath(sys.argv[1]), 'esctest') + extra = sys.argv[2:] or ['--max-vt-level', '4'] + tmp = tempfile.mkdtemp() + out = os.path.join(tmp, 'out') + done = os.path.join(tmp, 'done') + log = os.path.join(tmp, 'log') + shell = os.path.join(tmp, 'shell') + cmd = ' '.join(['python3', 'esctest.py', '--expected-terminal', 'xterm', + '--logfile', shlex.quote(log), '--no-print-logs'] + + [shlex.quote(a) for a in extra]) + with open(shell, 'w') as f: + f.write(f'#!/bin/sh\ncd {shlex.quote(esctest)}\n' + f'{cmd} 2> {shlex.quote(out)}\n' + f'touch {shlex.quote(done)}\nsleep 60\n') + os.chmod(shell, 0o755) + + env = dict(os.environ, SHELL=shell, TERM='xterm') + p = pexpect.spawn(os.path.join(ROOT, 'dvtm'), env=env, dimensions=(25, 80)) + start = time.time() + while not os.path.exists(done) and time.time() - start < TIMEOUT: + try: + p.read_nonblocking(65536, 0.5) + except (pexpect.TIMEOUT, pexpect.EOF): + pass + p.terminate(force=True) + + if not os.path.exists(done): + print('esctest did not finish') + return 0 + with open(out, errors='replace') as f: + text = f.read() + with open(log, errors='replace') as f: + failed = re.findall(r'^\*\*\* TEST (\S+) FAILED', f.read(), re.M) + for name in failed: + print('FAILED', name) + if not failed: + print(text[-2000:]) + print(f'{len(failed)} test(s) failed') + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/test/test-vt.c b/test/test-vt.c index a8f96a6..3313758 100644 --- a/test/test-vt.c +++ b/test/test-vt.c @@ -164,6 +164,27 @@ static void test_device_attributes(void) } } +static void test_text_area_size(void) +{ + term = "XTWINOPS"; + int fds[2]; + if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds)) + abort(); + Vt *t = vt_create(5, 10, 0); + t->pty = fds[0]; + const char *query = "\033[18t\033[14t"; + if (write(fds[1], query, strlen(query)) != (ssize_t)strlen(query)) + abort(); + vt_process(t); + fcntl(fds[1], F_SETFL, O_NONBLOCK); + char buf[32] = ""; + ssize_t len = read(fds[1], buf, sizeof buf - 1); + buf[len > 0 ? len : 0] = '\0'; + CHECK(!strcmp(buf, "\033[8;5;10t")); + close(fds[1]); + vt_destroy(t); +} + int main(void) { setlocale(LC_CTYPE, "C.UTF-8"); @@ -191,6 +212,7 @@ int main(void) setenv("TERMINFO", dir, 1); test_device_attributes(); + test_text_area_size(); test_term("xterm", COLOR_RED, COLOR_RED, COLOR_BLACK, COLOR_BLACK); test_term("xterm-256color", 196, 196, 16, 16); if (direct) { diff --git a/vt.c b/vt.c index 93f3691..9bd3a4d 100644 --- a/vt.c +++ b/vt.c @@ -1123,6 +1123,13 @@ static void interpret_csi(Vt *t) if (param_count == 1 && csiparam[0] == 6) send_curs(t); break; + case 't': /* report text area size in characters */ + if (param_count == 1 && csiparam[0] == 18) { + char reply[32]; + snprintf(reply, sizeof reply, "\e[8;%d;%dt", t->buffer->rows, t->buffer->cols); + vt_write(t, reply, strlen(reply)); + } + break; case 'c': /* primary device attributes, answer as a VT100 with advanced video */ if (t->ebuf[1] != '>' && t->ebuf[1] != '=' && (param_count == 0 || csiparam[0] == 0)) vt_write(t, "[?1;2c", 7);