less/backport-makecheck-0000-add-lesstest.patch

1902 lines
50 KiB
Diff

From 815ba9ae9aeb64af9d7b40cf4c5def86017e6de9 Mon Sep 17 00:00:00 2001
From: EibzChan <chenbingzhao@huawei.com>
Date: Thu, 15 Dec 2022 13:24:00 +0800
Subject: [PATCH] add lesstest
---
lesstest/Makefile | 18 ++
lesstest/display.c | 93 +++++++++++
lesstest/env.c | 109 ++++++++++++
lesstest/gen | 56 +++++++
lesstest/lesstest.c | 96 +++++++++++
lesstest/lesstest.h | 86 ++++++++++
lesstest/log.c | 106 ++++++++++++
lesstest/lt_screen.c | 382 +++++++++++++++++++++++++++++++++++++++++++
lesstest/lt_types.h | 28 ++++
lesstest/parse.c | 150 +++++++++++++++++
lesstest/pipeline.c | 171 +++++++++++++++++++
lesstest/run | 74 +++++++++
lesstest/run.c | 197 ++++++++++++++++++++++
lesstest/term.c | 85 ++++++++++
lesstest/unicode.c | 28 ++++
lesstest/wchar.c | 70 ++++++++
lesstest/wchar.h | 5 +
17 files changed, 1754 insertions(+)
create mode 100644 lesstest/Makefile
create mode 100644 lesstest/display.c
create mode 100644 lesstest/env.c
create mode 100755 lesstest/gen
create mode 100644 lesstest/lesstest.c
create mode 100644 lesstest/lesstest.h
create mode 100644 lesstest/log.c
create mode 100644 lesstest/lt_screen.c
create mode 100644 lesstest/lt_types.h
create mode 100644 lesstest/parse.c
create mode 100644 lesstest/pipeline.c
create mode 100755 lesstest/run
create mode 100644 lesstest/run.c
create mode 100644 lesstest/term.c
create mode 100644 lesstest/unicode.c
create mode 100644 lesstest/wchar.c
create mode 100644 lesstest/wchar.h
diff --git a/lesstest/Makefile b/lesstest/Makefile
new file mode 100644
index 0000000..d7e4987
--- /dev/null
+++ b/lesstest/Makefile
@@ -0,0 +1,18 @@
+CC = gcc
+CFLAGS = -Wall -g
+TERMLIB = -lncurses
+
+all: lesstest lt_screen
+
+LESSTEST_OBJ = display.o env.o lesstest.o parse.o pipeline.o log.o run.o term.o wchar.o
+lesstest: $(LESSTEST_OBJ)
+ $(CC) $(CFLAGS) -o lesstest $(LESSTEST_OBJ) $(TERMLIB)
+
+LT_SCREEN_OBJ = lt_screen.o unicode.o wchar.o
+lt_screen: $(LT_SCREEN_OBJ)
+ $(CC) $(CFLAGS) -o lt_screen $(LT_SCREEN_OBJ)
+
+*.o: lesstest.h lt_types.h wchar.h
+
+clean:
+ rm -f lesstest lt_screen *.o
diff --git a/lesstest/display.c b/lesstest/display.c
new file mode 100644
index 0000000..83e9f81
--- /dev/null
+++ b/lesstest/display.c
@@ -0,0 +1,93 @@
+#include <termcap.h>
+#include "lesstest.h"
+
+extern TermInfo terminfo;
+
+void display_attr(Attr attr) {
+ static Attr prev_attr = 0;
+ if (attr == prev_attr)
+ return;
+ if (prev_attr & ATTR_STANDOUT)
+ printf("%s", terminfo.exit_standout);
+ if (prev_attr & ATTR_BLINK)
+ printf("%s", terminfo.exit_blink);
+ if (prev_attr & ATTR_BOLD)
+ printf("%s", terminfo.exit_bold);
+ if (prev_attr & ATTR_UNDERLINE)
+ printf("%s", terminfo.exit_underline);
+ if (attr & ATTR_UNDERLINE)
+ printf("%s", terminfo.enter_underline);
+ if (attr & ATTR_BOLD)
+ printf("%s", terminfo.enter_bold);
+ if (attr & ATTR_BLINK)
+ printf("%s", terminfo.enter_blink);
+ if (attr & ATTR_STANDOUT)
+ printf("%s", terminfo.enter_standout);
+ prev_attr = attr;
+}
+
+void display_color(Color fg_color, Color bg_color) {
+printf("{%x/%x}", fg_color, bg_color);
+}
+
+void display_screen(const byte* img, int imglen, int screen_width, int screen_height, int move_cursor) {
+ int x = 0;
+ int y = 0;
+ int cursor_x = 0;
+ int cursor_y = 0;
+ int literal = 0;
+ while (imglen-- > 0) {
+ wchar ch = load_wchar(&img);
+ if (!literal) {
+ if (ch == '\\') {
+ literal = 1;
+ continue;
+ } else if (ch == '@') {
+ Attr attr = *img++;
+ display_attr(attr);
+ continue;
+ } else if (ch == '$') {
+ Color fg_color = *img++;
+ Color bg_color = *img++;
+ display_color(fg_color, bg_color);
+ continue;
+ } else if (ch == '#') {
+ cursor_x = x;
+ cursor_y = y;
+ continue;
+ }
+ }
+ literal = 0;
+ if (ch != 0) {
+ byte cbuf[UNICODE_MAX_BYTES];
+ byte* cp = cbuf;
+ store_wchar(&cp, ch);
+ fwrite(cbuf, 1, cp-cbuf, stdout);
+ }
+ if (++x >= screen_width) {
+ printf("\n");
+ x = 0;
+ if (++y >= screen_height)
+ break;
+ }
+ }
+ if (move_cursor)
+ printf("%s", tgoto(terminfo.cursor_move, cursor_x, cursor_y));
+ fflush(stdout);
+}
+
+void print_strings(const char* title, char* const* strings) {
+ fprintf(stderr, "%s:\n", title);
+ char* const* s;
+ for (s = strings; *s != NULL; ++s) {
+ fprintf(stderr, " ");
+ const char* p;
+ for (p = *s; *p != '\0'; ++p) {
+ if (is_ascii(*p))
+ fprintf(stderr, "%c", (char) *p);
+ else
+ fprintf(stderr, "\\x%04x", *p);
+ }
+ fprintf(stderr, "\n");
+ }
+}
diff --git a/lesstest/env.c b/lesstest/env.c
new file mode 100644
index 0000000..48ff205
--- /dev/null
+++ b/lesstest/env.c
@@ -0,0 +1,109 @@
+#include "lesstest.h"
+
+extern TermInfo terminfo;
+
+void env_init(EnvBuf* env) {
+ env->env_estr = (char*) env->env_buf;
+ env->env_list = env->env_buf + sizeof(env->env_buf)/sizeof(char*);
+ *--(env->env_list) = NULL;
+}
+
+void env_check(EnvBuf* env) {
+ if (env->env_estr >= (const char*) env->env_list) {
+ fprintf(stderr, "ENVBUF_SIZE too small!\n");
+ abort();
+ }
+}
+
+void env_addchar(EnvBuf* env, char ch) {
+ *(env->env_estr)++ = ch;
+ env_check(env);
+}
+
+void env_addlstr(EnvBuf* env, const char* str, int strlen) {
+ while (strlen-- > 0)
+ env_addchar(env, *str++);
+}
+
+void env_addstr(EnvBuf* env, const char* str) {
+ env_addlstr(env, str, strlen(str));
+}
+
+void env_addlpair(EnvBuf* env, const char* name, int namelen, const char* value) {
+ *--(env->env_list) = env->env_estr;
+ env_check(env);
+ env_addlstr(env, name, namelen);
+ env_addstr(env, "=");
+ env_addstr(env, value);
+ env_addchar(env, '\0');
+}
+
+void env_addpair(EnvBuf* env, const char* name, const char* value) {
+ env_addlpair(env, name, strlen(name), value);
+}
+
+void env_addintpair(EnvBuf* env, const char* name, int value) {
+ char buf[64];
+ snprintf(buf, sizeof(buf), "%d", value);
+ env_addpair(env, name, buf);
+}
+
+void env_setup(EnvBuf* env, char* const* prog_env, const char* env_prefix) {
+ env_addpair(env, "LESS_TERMCAP_am", "1");
+ env_addpair(env, "LESS_TERMCAP_cd", "\33S");
+ env_addpair(env, "LESS_TERMCAP_ce", "\33L");
+ env_addpair(env, "LESS_TERMCAP_cl", "\33A");
+ env_addpair(env, "LESS_TERMCAP_cr", "\33<");
+ env_addpair(env, "LESS_TERMCAP_cm", "\33%p2%d;%p1%dj");
+ env_addpair(env, "LESS_TERMCAP_ho", "\33h");
+ env_addpair(env, "LESS_TERMCAP_ll", "\33l");
+ env_addpair(env, "LESS_TERMCAP_mb", "\33b");
+ env_addpair(env, "LESS_TERMCAP_md", "\33d");
+ env_addpair(env, "LESS_TERMCAP_md", "\33e");
+ env_addpair(env, "LESS_TERMCAP_se", "\33t");
+ env_addpair(env, "LESS_TERMCAP_so", "\33s");
+ env_addpair(env, "LESS_TERMCAP_sr", "\33r");
+ env_addpair(env, "LESS_TERMCAP_ue", "\33v");
+ env_addpair(env, "LESS_TERMCAP_uo", "\33u");
+ env_addpair(env, "LESS_TERMCAP_vb", "\33g");
+ env_addpair(env, "LESS_TERMCAP_kr", terminfo.key_right ? terminfo.key_right : "");
+ env_addpair(env, "LESS_TERMCAP_kl", terminfo.key_left ? terminfo.key_left : "");
+ env_addpair(env, "LESS_TERMCAP_ku", terminfo.key_up ? terminfo.key_up : "");
+ env_addpair(env, "LESS_TERMCAP_kd", terminfo.key_down ? terminfo.key_down : "");
+ env_addpair(env, "LESS_TERMCAP_kh", terminfo.key_home ? terminfo.key_home : "");
+ env_addpair(env, "LESS_TERMCAP_@7", terminfo.key_end ? terminfo.key_end : "");
+
+ char* const* envp;
+ int len = strlen(env_prefix);
+ for (envp = prog_env; *envp != NULL; ++envp) {
+ if (strncmp(*envp, env_prefix, len) == 0) {
+ const char* ename = *envp + len;
+ const char* eq = strchr(ename, '=');
+ if (eq != NULL) {
+ env_addlpair(env, ename, eq-ename, eq+1);
+ log_env(ename, eq-ename, eq+1);
+ }
+ }
+ }
+}
+
+const char* get_envp(char* const* envp, const char* name) {
+ for (; *envp != NULL; ++envp) {
+ const char* ename = *envp;
+ const char* eq = strchr(ename, '=');
+ if (eq != NULL && strlen(name) == eq-ename && strncmp(name, ename, eq-ename) == 0)
+ return eq+1;
+ }
+ return NULL;
+}
+
+char* const* less_envp(char* const* envp, const char* env_prefix) {
+ static EnvBuf less_env;
+ static int init = 0;
+ if (!init) {
+ env_init(&less_env);
+ env_setup(&less_env, envp, env_prefix);
+ init = 1;
+ }
+ return less_env.env_list;
+}
diff --git a/lesstest/gen b/lesstest/gen
new file mode 100755
index 0000000..00dbf62
--- /dev/null
+++ b/lesstest/gen
@@ -0,0 +1,56 @@
+#!/usr/bin/perl
+use strict;
+
+# Generate a lesstest file.
+
+my $usage = "usage: [LT_LESSVAR=value]... gen [-w#] [-h#] less.exe [lessflags] textfile\n";
+
+use Getopt::Std;
+
+my $testdir = ".";
+my $lesstest = "$testdir/lesstest";
+my $outdir = "$testdir/suite";
+
+exit (main() ? 0 : 1);
+sub main {
+ my %opt;
+ die $usage if not getopts('h:w:', \%opt);
+ my ($width, $height) = term_size();
+ $width -= 2 if $width > 2;
+ $height -= 1 if $height > 1;
+ $width = $opt{w} if $opt{w};
+ $height = $opt{h} if $opt{h};
+
+ die $usage if @ARGV < 2;
+ my $less = $ARGV[0];
+ my $file = $ARGV[$#ARGV];
+ die $usage if not defined $less or not defined $file;
+ if (not -f $file) {
+ print "$0: cannot open $file\n";
+ return 0;
+ }
+ my ($base) = $file;
+ if ($base =~ m|.*/([^/]+)$|) { $base = $1; }
+
+ my $outfile;
+ for (my $n = 1;; ++$n) {
+ $outfile = "$outdir/$base-$n.lt";
+ last if not -s $outfile;
+ }
+
+ my $cmd = '';
+ $cmd .= "LT_COLUMNS=$width " if $width > 0;
+ $cmd .= "LT_LINES=$height " if $height > 0;
+ $cmd .= "$lesstest -s $testdir/lt_screen -o $outfile -- " . join(' ', @ARGV);
+ if (system $cmd) {
+ print "$0: error running $lesstest\n";
+ }
+ return 1;
+}
+
+sub term_size {
+ my $stty = `stty -a`;
+ my ($x, $lines) = $stty =~ /(rows|lines)\s+(\d+)/;
+ my ($columns) = $stty =~ /columns\s+(\d+)/;
+ return ($columns, $lines);
+}
diff --git a/lesstest/lesstest.c b/lesstest/lesstest.c
new file mode 100644
index 0000000..6f03d6f
--- /dev/null
+++ b/lesstest/lesstest.c
@@ -0,0 +1,96 @@
+#include <sys/wait.h>
+#include <setjmp.h>
+#include "lesstest.h"
+
+extern TermInfo terminfo;
+
+int verbose = 0;
+int less_quit = 0;
+int details = 0;
+char* lt_screen = "./lt_screen";
+int run_catching = 0;
+jmp_buf run_catch;
+
+static char* testfile = NULL;
+
+int usage(void) {
+ fprintf(stderr, "usage: lesstest -o file.lt [-w#] [-h#] [--] less.exe [flags] textfile\n");
+ fprintf(stderr, " or: lesstest -t file.lt less.exe\n");
+ return 0;
+}
+
+void child_handler(int signum) {
+ int status;
+ pid_t child = wait(&status);
+ if (verbose) fprintf(stderr, "child %d died, status 0x%x\n", child, status);
+}
+
+void intr_handler(int signum) {
+ less_quit = 1;
+ if (run_catching) longjmp(run_catch, 1);
+}
+
+int setup(int argc, char* const* argv) {
+ char* logfile = NULL;
+ int ch;
+ while ((ch = getopt(argc, argv, "do:s:t:v")) != -1) {
+ switch (ch) {
+ case 'd':
+ details = 1;
+ break;
+ case 'o':
+ logfile = optarg;
+ break;
+ case 's':
+ lt_screen = optarg;
+ break;
+ case 't':
+ testfile = optarg;
+ break;
+ case 'v':
+ verbose = 1;
+ break;
+ default:
+ return usage();
+ }
+ }
+ if (logfile != NULL && !log_open(logfile)) {
+ fprintf(stderr, "cannot create %s: %s\n", logfile, strerror(errno));
+ return 0;
+ }
+ return 1;
+}
+
+int main(int argc, char* const* argv, char* const* envp) {
+ signal(SIGCHLD, child_handler);
+ signal(SIGINT, intr_handler);
+ signal(SIGQUIT, intr_handler);
+ signal(SIGKILL, intr_handler);
+ if (!setup(argc, argv))
+ return RUN_ERR;
+ setup_term();
+ int ok = 0;
+ if (testfile != NULL) { // run existing test
+ if (optind+1 != argc) {
+ usage();
+ return RUN_ERR;
+ }
+ //if (setjmp(run_catch)) {
+ // fprintf(stderr, "\nINTR test interrupted\n");
+ // ok = 0;
+ //} else {
+ ok = run_testfile(testfile, argv[optind]);
+ //}
+ } else { // gen; create new test
+ if (optind+2 > argc) {
+ usage();
+ return RUN_ERR;
+ }
+ log_file_header();
+ printf("%s%s", terminfo.init_term, terminfo.enter_keypad);
+ ok = run_interactive(argv+optind, argc-optind, envp);
+ printf("%s%s", terminfo.exit_keypad, terminfo.deinit_term);
+ log_close();
+ }
+ return ok ? RUN_OK : RUN_ERR;
+}
diff --git a/lesstest/lesstest.h b/lesstest/lesstest.h
new file mode 100644
index 0000000..230cf2a
--- /dev/null
+++ b/lesstest/lesstest.h
@@ -0,0 +1,86 @@
+#include <stdio.h>
+#include <unistd.h>
+#include <string.h>
+#include <stdlib.h>
+#include <errno.h>
+#include "lt_types.h"
+#include "wchar.h"
+
+#define ENVBUF_SIZE 4096
+typedef struct EnvBuf {
+ char** env_list;
+ char* env_estr;
+ char* env_buf[ENVBUF_SIZE/sizeof(char*)];
+} EnvBuf;
+
+typedef struct TestSetup {
+ char* setup_name;
+ char* textfile;
+ char** argv;
+ int argc;
+ EnvBuf env;
+} TestSetup;
+
+typedef struct LessPipeline {
+ int less_in;
+ int screen_out;
+ int rstat_file;
+ int screen_width;
+ int screen_height;
+ pid_t screen_pid;
+ const char* tempfile;
+ int less_in_pipe[2];
+ int screen_in_pipe[2];
+ int screen_out_pipe[2];
+} LessPipeline;
+
+typedef struct TermInfo {
+ char backspace_key;
+ char* enter_underline;
+ char* exit_underline;
+ char* enter_bold;
+ char* exit_bold;
+ char* enter_blink;
+ char* exit_blink;
+ char* enter_standout;
+ char* exit_standout;
+ char* clear_screen;
+ char* cursor_move;
+ char* key_right;
+ char* key_left;
+ char* key_up;
+ char* key_down;
+ char* key_home;
+ char* key_end;
+ char* enter_keypad;
+ char* exit_keypad;
+ char* init_term;
+ char* deinit_term;
+} TermInfo;
+
+int log_open(char const* logfile);
+void log_close(void);
+int log_file_header(void);
+int log_test_header(char* const* argv, int argc, const char* textfile);
+int log_test_footer(void);
+int log_env(const char* name, int namelen, const char* value);
+int log_tty_char(wchar ch);
+int log_screen(byte const* img, int len);
+LessPipeline* create_less_pipeline(char* const* argv, int argc, char* const* envp);
+void destroy_less_pipeline(LessPipeline* pipeline);
+void print_strings(const char* title, char* const* strings);
+void free_test_setup(TestSetup* setup);
+TestSetup* read_test_setup(FILE* fd, char const* less);
+int read_zline(FILE* fd, char* line, int line_len);
+void raw_mode(int tty, int on);
+int setup_term(void);
+void display_screen(const byte* img, int imglen, int screen_width, int screen_height, int move_cursor);
+const char* get_envp(char* const* envp, const char* name);
+int run_interactive(char* const* argv, int argc, char* const* envp);
+int run_testfile(const char* testfile, const char* less);
+void env_init(EnvBuf* env);
+void env_addchar(EnvBuf* env, char ch);
+void env_addstr(EnvBuf* env, const char* str);
+void env_addpair(EnvBuf* env, const char* name, const char* value);
+void env_setup(EnvBuf* env, char* const* prog_env, const char* env_prefix);
+char* const* less_envp(char* const* envp, const char* env_prefix);
diff --git a/lesstest/log.c b/lesstest/log.c
new file mode 100644
index 0000000..ee564f8
--- /dev/null
+++ b/lesstest/log.c
@@ -0,0 +1,106 @@
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include "lesstest.h"
+
+static FILE* logf = NULL;
+
+int log_open(const char* logfile) {
+ if (logf != NULL) fclose(logf);
+ logf = (strcmp(logfile, "-") == 0) ? stdout : fopen(logfile, "w");
+ if (logf == NULL) {
+ fprintf(stderr, "cannot create %s\n", logfile);
+ return 0;
+ }
+ return 1;
+}
+
+void log_close(void) {
+ if (logf == NULL) return;
+ if (logf == stdout) return;
+ fclose(logf);
+ logf = NULL;
+}
+
+int log_file_header(void) {
+ if (logf == NULL) return 0;
+ fprintf(logf, "!lesstest!\n");
+ return 1;
+}
+
+int log_env(const char* name, int namelen, const char* value) {
+ if (logf == NULL) return 0;
+ fprintf(logf, "E \"%.*s\" \"%s\"\n", namelen, name, value);
+ return 1;
+}
+
+int log_tty_char(wchar ch) {
+ if (logf == NULL) return 0;
+ fprintf(logf, "+%lx\n", ch);
+ return 1;
+}
+
+int log_screen(const byte* img, int len) {
+ if (logf == NULL) return 0;
+ fwrite("=", 1, 1, logf);
+ fwrite(img, 1, len, logf);
+ fwrite("\n", 1, 1, logf);
+ return 1;
+}
+
+int log_command(char* const* argv, int argc, const char* textfile) {
+ if (logf == NULL) return 0;
+ fprintf(logf, "A");
+ int a;
+ for (a = 1; a < argc; ++a)
+ fprintf(logf, " \"%s\"", (a < argc-1) ? argv[a] : textfile);
+ fprintf(logf, "\n");
+ return 1;
+}
+
+int log_textfile(const char* textfile) {
+ if (logf == NULL) return 0;
+ struct stat st;
+ if (stat(textfile, &st) < 0) {
+ fprintf(stderr, "cannot stat %s\n", textfile);
+ return 0;
+ }
+ FILE* fd = fopen(textfile, "r");
+ if (fd == NULL) {
+ fprintf(stderr, "cannot open %s\n", textfile);
+ return 0;
+ }
+ fprintf(logf, "F \"%s\" %ld\n", textfile, (long) st.st_size);
+ off_t nread = 0;
+ while (nread < st.st_size) {
+ char buf[4096];
+ size_t n = fread(buf, 1, sizeof(buf), fd);
+ if (n <= 0) {
+ fprintf(stderr, "read only %ld/%ld from %s\n", (long) nread, (long) st.st_size, textfile);
+ fclose(fd);
+ return 0;
+ }
+ nread += n;
+ fwrite(buf, 1, n, logf);
+ }
+ fclose(fd);
+ return 1;
+}
+
+int log_test_header(char* const* argv, int argc, const char* textfile) {
+ if (logf == NULL) return 0;
+ fprintf(logf, "T \"%s\"\n", textfile);
+ if (!log_command(argv, argc, textfile))
+ return 0;
+ if (!log_textfile(textfile))
+ return 0;
+ fprintf(logf, "R\n");
+ return 1;
+}
+
+int log_test_footer(void) {
+ if (logf == NULL) return 0;
+ fprintf(logf, "Q\n");
+ return 1;
+}
diff --git a/lesstest/lt_screen.c b/lesstest/lt_screen.c
new file mode 100644
index 0000000..e349452
--- /dev/null
+++ b/lesstest/lt_screen.c
@@ -0,0 +1,382 @@
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <fcntl.h>
+#include "lt_types.h"
+#include "wchar.h"
+
+static const char version[] = "lt_screen|v=1";
+
+int usage() {
+ fprintf(stderr, "usage: lt_screen\n");
+ return 0;
+}
+
+// ------------------------------------------------------------------
+
+#define MAX_PARAMS 3
+
+typedef struct ScreenChar {
+ wchar ch;
+ Attr attr;
+ Color fg_color;
+ Color bg_color;
+} ScreenChar;
+
+typedef struct ScreenState {
+ ScreenChar* chars;
+ int w;
+ int h;
+ int cx;
+ int cy;
+ Attr curr_attr;
+ Color curr_fg_color;
+ Color curr_bg_color;
+ int param_top;
+ int params[MAX_PARAMS+1];
+ int in_esc;
+} ScreenState;
+
+static ScreenState screen;
+static int ttyin; // input text and control sequences
+static int ttyout; // output for screen dump
+static int quiet = 0;
+static int verbose = 0;
+
+// ------------------------------------------------------------------
+
+void screen_init() {
+ screen.w = 80;
+ screen.h = 24;
+ screen.cx = 0;
+ screen.cy = 0;
+ screen.in_esc = 0;
+ screen.curr_attr = 0;
+ screen.curr_fg_color = screen.curr_bg_color = 0;
+ screen.param_top = -1;
+ screen.params[0] = 0;
+}
+
+void param_print() {
+ int i;
+ fprintf(stderr, "(");
+ for (i = 0; i <= screen.param_top; ++i)
+ fprintf(stderr, "%d ", screen.params[i]);
+ fprintf(stderr, ")");
+}
+
+void param_push(int v) {
+ if (screen.param_top >= (int) countof(screen.params)-1)
+ return;
+ screen.params[++screen.param_top] = v;
+}
+
+int param_pop(){
+ if (screen.param_top < 0)
+ return 0; // missing param is assumed to be 0
+ return screen.params[screen.param_top--];
+}
+
+int screen_x(int x) {
+ if (x < 0) x = 0;
+ if (x >= screen.w) x = screen.w-1;
+ return x;
+}
+
+int screen_y(int y) {
+ if (y < 0) y = 0;
+ if (y >= screen.h) y = screen.h-1;
+ return y;
+}
+
+ScreenChar* screen_char(int x, int y) {
+ x = screen_x(x);
+ y = screen_x(y);
+ return &screen.chars[y * screen.w + x];
+}
+
+int screen_incr(int* px, int* py) {
+ if (++(*px) >= screen.w) {
+ *px = 0;
+ if (++(*py) >= screen.h) {
+ *py = 0;
+ return 0;
+ }
+ }
+ return 1;
+}
+
+void screen_char_set(int x, int y, wchar ch, Attr attr, Color fg_color, Color bg_color) {
+ ScreenChar* sc = screen_char(x, y);
+ sc->ch = ch;
+ sc->attr = attr;
+ sc->fg_color = fg_color;
+ sc->bg_color = bg_color;
+}
+
+int screen_clear(int x, int y, int count) {
+ while (count-- > 0) {
+ screen_char_set(x, y, '_', 0, 0, 0);
+ screen_incr(&x, &y);
+ }
+ return 1;
+}
+
+int screen_busy() {
+ write(ttyout, "*BUSY*\n", 7);
+ return 1;
+}
+int screen_read(int x, int y, int count) {
+ //write(ttyout, "$|", 2);
+ int attr = 0;
+ int fg_color = 0;
+ int bg_color = 0;
+ while (count-- > 0) {
+ byte buf[32];
+ byte* bufp = buf;
+ ScreenChar* sc = screen_char(x, y);
+ if (sc->attr != attr) {
+ attr = sc->attr;
+ *bufp++ = '@';
+ *bufp++ = attr;
+ }
+ if (sc->fg_color != fg_color || sc->bg_color != bg_color) {
+ fg_color = sc->fg_color;
+ bg_color = sc->bg_color;
+ *bufp++ = '$';
+ *bufp++ = fg_color;
+ *bufp++ = bg_color;
+ }
+ if (x == screen.cx && y == screen.cy)
+ *bufp++ = '#';
+ if (sc->ch == '@' || sc->ch == '$' || sc->ch == '\\' || sc->ch == '#')
+ *bufp++ = '\\';
+ store_wchar(&bufp, sc->ch);
+ write(ttyout, buf, bufp-buf);
+ screen_incr(&x, &y);
+ }
+ write(ttyout, "\n", 1);
+ return 1;
+}
+
+int screen_move(int x, int y) {
+ screen.cx = x;
+ screen.cy = y;
+ return 1;
+}
+
+int screen_cr() {
+ screen.cx = 0;
+ return 1;
+}
+
+int screen_bs() {
+ if (screen.cx <= 0) return 0;
+ --screen.cx;
+ return 1;
+}
+
+int screen_scroll() {
+ int len = screen.w * (screen.h-1);
+ memmove(screen_char(0,0), screen_char(0,1), len * sizeof(ScreenChar));
+ screen_clear(0, screen.h-1, screen.w);
+ return 1;
+}
+
+int screen_rscroll() {
+ int len = screen.w * (screen.h-1);
+ memmove(screen_char(0,1), screen_char(0,0), len * sizeof(ScreenChar));
+ screen_clear(0, 0, screen.w);
+ return 1;
+}
+
+int screen_set_attr(int attr) {
+ screen.curr_attr |= attr;
+ return 0;
+}
+
+int screen_clear_attr(int attr) {
+ screen.curr_attr &= ~attr;
+ return 0;
+}
+
+// ------------------------------------------------------------------
+
+void beep() {
+ if (!quiet)
+ fprintf(stderr, "\7");
+}
+
+int exec_esc(wchar ch) {
+ int x, y, count;
+ if (verbose) {
+ fprintf(stderr, "exec ESC-%c ", (char)ch);
+ param_print();
+ fprintf(stderr, "\n");
+ }
+ switch (ch) {
+ case 'A': // clear all
+ return screen_clear(0, 0, screen.w * screen.h);
+ case 'L': // clear from cursor to end of line
+ return screen_clear(screen.cx, screen.cy, screen.w - screen.cx);
+ case 'S': // clear from cursor to end of screen
+ return screen_clear(screen.cx, screen.cy,
+ (screen.w - screen.cx) + (screen.h - screen.cy -1) * screen.w);
+ case 'R': // read N3 chars starting at (N1,N2)
+ count = param_pop();
+ y = param_pop();
+ x = param_pop();
+ return screen_read(x, y, count);
+ case 'j': // jump cursor to (N1,N2)
+ y = param_pop();
+ x = param_pop();
+ return screen_move(x, y);
+ case 'g': // visual bell
+ return 0;
+ case 'h': // cursor home
+ return screen_move(0, 0);
+ case 'l': // cursor lower left
+ return screen_move(0, screen.h-1);
+ case 'r': // reverse scroll
+ return screen_rscroll();
+ case '<': // cursor left to start of line
+ return screen_cr();
+ case 's': // enter standout
+ return screen_set_attr(ATTR_STANDOUT);
+ case 't': // exit standout
+ return screen_clear_attr(ATTR_STANDOUT);
+ case 'u': // enter underline
+ return screen_set_attr(ATTR_UNDERLINE);
+ case 'v': // exit underline
+ return screen_clear_attr(ATTR_UNDERLINE);
+ case 'd': // enter bold
+ return screen_set_attr(ATTR_BOLD);
+ case 'e': // exit bold
+ return screen_clear_attr(ATTR_BOLD);
+ case 'b': // enter blink
+ return screen_set_attr(ATTR_BLINK);
+ case 'c': // exit blink
+ return screen_clear_attr(ATTR_BLINK);
+ case '?': // print version string
+ write(ttyout, version, strlen(version));
+ return 1;
+ default:
+ return 0;
+ }
+}
+
+int add_char(wchar ch) {
+ if (verbose) fprintf(stderr, "add %lx at %d,%d\n", ch, screen.cx, screen.cy);
+ screen_char_set(screen.cx, screen.cy, ch, screen.curr_attr, screen.curr_fg_color, screen.curr_bg_color);
+ int fits = screen_incr(&screen.cx, &screen.cy);
+ if (fits && is_wide_char(ch)) {
+ screen_char_set(screen.cx, screen.cy, 0, 0, 0, 0);
+ fits = screen_incr(&screen.cx, &screen.cy);
+ }
+ if (!fits) { // Wrap at bottom of screen = scroll
+ screen.cx = 0;
+ screen.cy = screen.h-1;
+ return screen_scroll();
+ }
+ return 1;
+}
+
+int process_char(wchar ch) {
+ int ok = 1;
+ if (screen.in_esc) {
+ if (ch >= '0' && ch <= '9') {
+ param_push(10 * param_pop() + ch - '0');
+ } else if (ch == ';') {
+ param_push(0);
+ } else {
+ screen.in_esc = 0;
+ ok = exec_esc(ch);
+ }
+ } else if (ch == ESC) {
+ screen.in_esc = 1;
+ } else if (ch == '\r') {
+ screen_cr();
+ } else if (ch == '\b') {
+ screen_bs();
+ } else if (ch == '\n') {
+ if (screen.cy < screen.h-1)
+ ++screen.cy;
+ else
+ screen_scroll();
+ screen.cx = 0; // auto CR
+ } else if (ch == '\7') {
+ beep();
+ } else if (ch == '\t') {
+ ok = add_char(' ');
+ } else if (ch >= '\40') {
+ ok = add_char(ch);
+ }
+ return ok;
+}
+
+void screen_dump_handler(int signum) {
+ // (signum == LTSIG_READ_SCREEN)
+ if (verbose) fprintf(stderr, "screen: rcv dump signal\n");
+ (void) screen_read(0, 0, screen.w * screen.h);
+}
+
+// ------------------------------------------------------------------
+
+int setup(int argc, char** argv) {
+ int ch;
+ int ready_pid = 0;
+ screen_init();
+ while ((ch = getopt(argc, argv, "h:qr:vw:")) != -1) {
+ switch (ch) {
+ case 'h':
+ screen.h = atoi(optarg);
+ break;
+ case 'q':
+ quiet = 1;
+ break;
+ case 'r':
+ ready_pid = atoi(optarg);
+ break;
+ case 'v':
+ ++verbose;
+ break;
+ case 'w':
+ screen.w = atoi(optarg);
+ break;
+ default:
+ return usage();
+ }
+ }
+ int len = screen.w * screen.h;
+ screen.chars = malloc(len * sizeof(ScreenChar));
+ screen_clear(0, 0, len);
+ if (optind >= argc) {
+ ttyin = 0;
+ ttyout = 1;
+ } else {
+ ttyin = ttyout = open(argv[optind], O_RDWR);
+ if (ttyin < 0) {
+ fprintf(stderr, "cannot open %s\n", argv[optind]);
+ return 0;
+ }
+ }
+ signal(LTSIG_SCREEN_DUMP, screen_dump_handler);
+ if (ready_pid != 0)
+ kill(ready_pid, LTSIG_SCREEN_READY);
+ return 1;
+}
+
+int main(int argc, char** argv) {
+ if (!setup(argc, argv))
+ return RUN_ERR;
+ for (;;) {
+ wchar ch = read_wchar(ttyin);
+ if (verbose) fprintf(stderr, "screen read %c (%lx)\n", pr_ascii(ch), ch);
+ if (ch == 0)
+ break;
+ if (!process_char(ch))
+ beep();
+ }
+ return RUN_OK;
+}
diff --git a/lesstest/lt_types.h b/lesstest/lt_types.h
new file mode 100644
index 0000000..d258731
--- /dev/null
+++ b/lesstest/lt_types.h
@@ -0,0 +1,28 @@
+#include <signal.h>
+
+typedef unsigned long wchar;
+typedef unsigned char byte;
+typedef unsigned char Attr;
+typedef unsigned char Color;
+
+#define ATTR_BOLD (1<<0)
+#define ATTR_UNDERLINE (1<<1)
+#define ATTR_STANDOUT (1<<2)
+#define ATTR_BLINK (1<<3)
+
+#define ESC '\33'
+#define UNICODE_MAX_BYTES 4
+#define MAX_SCREENBUF_SIZE 8192
+#define LT_ENV_PREFIX "LT_"
+
+#define LTSIG_SCREEN_READY SIGUSR1
+#define LTSIG_SCREEN_DUMP SIGUSR2
+
+#define RUN_OK 0
+#define RUN_ERR 1
+
+#define is_ascii(ch) ((ch) >= ' ' && (ch) < 0x7f)
+#define pr_ascii(ch) (is_ascii(ch) ? ((char)ch) : '.')
+
+#undef countof
+#define countof(a) (sizeof(a)/sizeof(*a))
diff --git a/lesstest/parse.c b/lesstest/parse.c
new file mode 100644
index 0000000..c4d81f6
--- /dev/null
+++ b/lesstest/parse.c
@@ -0,0 +1,150 @@
+#include <stdio.h>
+#include "lesstest.h"
+
+extern int verbose;
+
+char* parse_qstring(const char** s) {
+ while (*(*s) == ' ') ++(*s);
+ if (*(*s)++ != '"') return NULL;
+ const char* start = *s;
+ while (*(*s) != '"' && *(*s) != '\0') ++(*s);
+ char* ret = strndup(start, (*s)-start);
+ if (*(*s) == '"') ++(*s);
+ return ret;
+}
+
+int parse_int(const char** s) {
+ return (int) strtol(*s, (char**)s, 0);
+}
+
+int parse_env(TestSetup* setup, const char* line, int line_len) {
+ char* name = parse_qstring(&line);
+ char* value = parse_qstring(&line);
+ env_addpair(&setup->env, name, value);
+ free(name);
+ free(value);
+ return 1;
+}
+
+int parse_command(TestSetup* setup, const char* less, const char* line, int line_len) {
+ setup->argv = (char**) malloc(32*sizeof(const char*));
+ setup->argc = 1;
+ setup->argv[0] = (char*) less;
+ for (;;) {
+ const char* arg = parse_qstring(&line);
+ setup->argv[setup->argc] = (char*) arg;
+ if (arg == NULL) break;
+ setup->argc++;
+ }
+ return 1;
+}
+
+int parse_textfile(TestSetup* setup, const char* line, int line_len, FILE* fd) {
+ const char* filename = parse_qstring(&line);
+ if (access(filename, F_OK) == 0) {
+ fprintf(stderr, "%s already exists\n", filename);
+ return 0;
+ }
+ int fsize = parse_int(&line);
+ int len = strlen(filename)+1;
+ setup->textfile = malloc(len);
+ strcpy(setup->textfile, filename);
+ FILE* textfd = fopen(setup->textfile, "w");
+ if (textfd == NULL) {
+ fprintf(stderr, "cannot create %s\n", setup->textfile);
+ return 0;
+ }
+ int nread = 0;
+ while (nread < fsize) {
+ char buf[4096];
+ int chunk = fsize - nread;
+ if (chunk > sizeof(buf)) chunk = sizeof(buf);
+ size_t len = fread(buf, 1, chunk, fd);
+ fwrite(buf, 1, len, textfd);
+ nread += len;
+ }
+ fclose(textfd);
+ return 1;
+}
+
+TestSetup* new_test_setup(void) {
+ TestSetup* setup = (TestSetup*) malloc(sizeof(TestSetup));
+ setup->textfile = NULL;
+ setup->argv = NULL;
+ setup->argc = 0;
+ env_init(&setup->env);
+ return setup;
+}
+
+void free_test_setup(TestSetup* setup) {
+ if (setup->textfile != NULL) {
+ unlink(setup->textfile);
+ free(setup->textfile);
+ }
+ int i;
+ for (i = 1; i < setup->argc; ++i)
+ free(setup->argv[i]);
+ free((void*)setup->argv);
+ free(setup);
+}
+
+int read_zline(FILE* fd, char* line, int line_len) {
+ int nread = 0;
+ while (nread < line_len-1) {
+ int ch = fgetc(fd);
+ if (ch == EOF) return -1;
+ if (ch == '\n') break;
+ line[nread++] = (char) ch;
+ }
+ line[nread] = '\0';
+ return nread;
+}
+
+TestSetup* read_test_setup(FILE* fd, const char* less) {
+ TestSetup* setup = new_test_setup();
+ int hdr_complete = 0;
+ while (!hdr_complete) {
+ char line[10000];
+ int line_len = read_zline(fd, line, sizeof(line));
+ if (line_len < 0)
+ break;
+ if (line_len < 1)
+ continue;
+ switch (line[0]) {
+ case '!': // file header
+ break;
+ case 'T': // test header
+ break;
+ case 'R': // end of test header; start run
+ hdr_complete = 1;
+ break;
+ case 'E': // environment variable
+ if (!parse_env(setup, line+1, line_len-1)) {
+ free_test_setup(setup);
+ return NULL;
+ }
+ break;
+ case 'F': // text file
+ if (!parse_textfile(setup, line+1, line_len-1, fd)) {
+ free_test_setup(setup);
+ return NULL;
+ }
+ break;
+ case 'A': // less cmd line parameters
+ if (!parse_command(setup, less, line+1, line_len-1)) {
+ free_test_setup(setup);
+ return NULL;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ if (setup->textfile == NULL || setup->argv == NULL) {
+ free_test_setup(setup);
+ return NULL;
+ }
+ if (verbose) { fprintf(stderr, "setup: textfile %s\n", setup->textfile); print_strings("argv:", setup->argv); }
+ return setup;
+}
+
diff --git a/lesstest/pipeline.c b/lesstest/pipeline.c
new file mode 100644
index 0000000..381cfe8
--- /dev/null
+++ b/lesstest/pipeline.c
@@ -0,0 +1,171 @@
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <fcntl.h>
+#include "lesstest.h"
+
+#define RD 0
+#define WR 1
+
+extern int verbose;
+extern char* lt_screen;
+static char* rstat_file_name = "less.rstat";
+static const int run_less = 1;
+
+static void dup_and_close(int dup0, int dup1, int close0, int close1) {
+ if (close0 >= 0) close(close0);
+ if (close1 >= 0) close(close1);
+ if (dup0 >= 0) dup2(dup0, 0);
+ if (dup1 >= 0) dup2(dup1, 1);
+}
+
+const char* basename(const char* path) {
+ const char* slash = strrchr(path, '/');
+ if (slash == NULL) return path;
+ return slash+1;
+}
+
+void become_child_less(char* less, int argc, char* const* argv, char* const* envp, const char* tempfile, int less_in_pipe[2], int screen_in_pipe[2]) {
+ if (verbose) fprintf(stderr, "less child: in %d, out %d, close %d,%d\n", less_in_pipe[RD], screen_in_pipe[WR], less_in_pipe[WR], screen_in_pipe[RD]);
+ dup_and_close(less_in_pipe[RD], screen_in_pipe[WR],
+ less_in_pipe[WR], screen_in_pipe[RD]);
+ char** less_argv = malloc(sizeof(char*) * (argc + 6));
+ less_argv[0] = less;
+ less_argv[1] = "--tty";
+ less_argv[2] = "/dev/stdin";
+ less_argv[3] = "--rstat";
+ less_argv[4] = rstat_file_name;
+ int less_argc = 5;
+ while (--argc > 0) {
+ char* arg = *++argv;
+ less_argv[less_argc++] = (argc > 1 || tempfile == NULL) ? arg : (char*) tempfile;
+ }
+ less_argv[less_argc] = NULL;
+ if (verbose) { print_strings("less argv", less_argv); print_strings("less envp", envp); }
+ execve(less, less_argv, envp);
+ fprintf(stderr, "cannot exec %s: %s\n", less, strerror(errno));
+ exit(1);
+}
+
+void become_child_screen(char* lt_screen, int screen_width, int screen_height, int screen_in_pipe[2], int screen_out_pipe[2]) {
+ if (verbose) fprintf(stderr, "screen child: in %d, out %d, close %d\n", screen_in_pipe[RD], screen_out_pipe[WR], screen_out_pipe[RD]);
+ dup_and_close(screen_in_pipe[RD], screen_out_pipe[WR], screen_out_pipe[RD], -1);
+ char* screen_argv[10];
+ int screen_argc = 0;
+ char sw[16];
+ char sh[16];
+ screen_argv[screen_argc++] = lt_screen;
+ if (screen_width >= 0) {
+ snprintf(sw, sizeof(sw), "%d", screen_width);
+ screen_argv[screen_argc++] = "-w";
+ screen_argv[screen_argc++] = sw;
+ }
+ if (screen_height >= 0) {
+ snprintf(sh, sizeof(sh), "%d", screen_height);
+ screen_argv[screen_argc++] = "-h";
+ screen_argv[screen_argc++] = sh;
+ }
+ if (1)
+ screen_argv[screen_argc++] = "-q";
+ if (verbose)
+ screen_argv[screen_argc++] = "-v";
+ screen_argv[screen_argc] = NULL;
+ if (verbose) print_strings("screen argv", screen_argv);
+ char* const screen_envp[] = { NULL };
+ execve(lt_screen, screen_argv, screen_envp);
+ fprintf(stderr, "cannot exec %s: %s\n", lt_screen, strerror(errno));
+ exit(1);
+}
+
+LessPipeline* new_pipeline() {
+ LessPipeline* pipeline = malloc(sizeof(LessPipeline));
+ pipeline->less_in_pipe[RD] = pipeline->less_in_pipe[WR] = -1;
+ pipeline->screen_in_pipe[RD] = pipeline->screen_in_pipe[WR] = -1;
+ pipeline->screen_out_pipe[RD] = pipeline->screen_out_pipe[WR] = -1;
+ pipeline->less_in = pipeline->screen_out = -1;
+ pipeline->rstat_file = -1;
+ pipeline->tempfile = NULL;
+ pipeline->screen_pid = 0;
+ pipeline->screen_width = pipeline->screen_height = 0;
+ return pipeline;
+}
+
+LessPipeline* create_less_pipeline(char* const* argv, int argc, char* const* envp) {
+ // If textfile contains a slash, create a temporary link from
+ // the named text file to its basename, and run less on the link.
+ LessPipeline* pipeline = new_pipeline();
+ const char* textfile = argv[argc-1];
+ const char* textbase = basename(textfile);
+ if (textbase != textfile) {
+ pipeline->tempfile = textbase;
+ if (link(textfile, textbase) < 0) {
+ fprintf(stderr, "cannot link %s to %s: %s\n", textfile, textbase, strerror(errno));
+ return NULL;
+ }
+ textfile = textbase;
+ }
+ if (pipe(pipeline->screen_in_pipe) < 0) {
+ destroy_less_pipeline(pipeline);
+ return NULL;
+ }
+ unlink(rstat_file_name);
+ pipeline->rstat_file = open(rstat_file_name, O_CREAT|O_RDONLY, 0664);
+ if (pipeline->rstat_file < 0) {
+ fprintf(stderr, "cannot create %s: %s\n", rstat_file_name, strerror(errno));
+ destroy_less_pipeline(pipeline);
+ return NULL;
+ }
+ const char* w = get_envp(envp, "COLUMNS");
+ const char* h = get_envp(envp, "LINES");
+ if (w != NULL) pipeline->screen_width = atoi(w);
+ if (h != NULL) pipeline->screen_height = atoi(h);
+ if (verbose) fprintf(stderr, "less out pipe %d,%d\n", pipeline->screen_in_pipe[0], pipeline->screen_in_pipe[1]);
+ if (run_less) {
+ if (pipe(pipeline->less_in_pipe) < 0) {
+ destroy_less_pipeline(pipeline);
+ return 0;
+ }
+ if (verbose) fprintf(stderr, "less in pipe %d,%d\n", pipeline->less_in_pipe[RD], pipeline->less_in_pipe[WR]);
+ char* less = argv[0];
+ if (verbose) fprintf(stderr, "testing %s on %s\n", less, textfile);
+ pid_t less_pid = fork();
+ if (less_pid < 0) {
+ destroy_less_pipeline(pipeline);
+ return NULL;
+ }
+ if (!less_pid)
+ become_child_less(less, argc, argv, envp, pipeline->tempfile, pipeline->less_in_pipe, pipeline->screen_in_pipe);
+ if (verbose) fprintf(stderr, "less child %ld\n", (long) less_pid);
+ close(pipeline->less_in_pipe[RD]); pipeline->less_in_pipe[RD] = -1;
+ close(pipeline->screen_in_pipe[WR]); pipeline->screen_in_pipe[WR] = -1;
+ }
+ if (pipe(pipeline->screen_out_pipe) < 0) {
+ destroy_less_pipeline(pipeline);
+ return NULL;
+ }
+ if (verbose) fprintf(stderr, "screen out pipe %d,%d\n", pipeline->screen_out_pipe[RD], pipeline->screen_out_pipe[WR]);
+ pipeline->screen_pid = fork();
+ if (!pipeline->screen_pid) // child: lt_screen
+ become_child_screen(lt_screen, pipeline->screen_width, pipeline->screen_height, pipeline->screen_in_pipe, pipeline->screen_out_pipe);
+ if (verbose) fprintf(stderr, "screen child %ld\n", (long) pipeline->screen_pid);
+ close(pipeline->screen_out_pipe[WR]); pipeline->screen_out_pipe[WR] = -1;
+ close(pipeline->screen_in_pipe[RD]); pipeline->screen_in_pipe[RD] = -1;
+
+ pipeline->less_in = run_less ? pipeline->less_in_pipe[WR] : pipeline->screen_in_pipe[WR];
+ pipeline->screen_out = pipeline->screen_out_pipe[RD];
+ if (verbose) fprintf(stderr, "less in %d, screen out %d, pid %ld\n", pipeline->less_in, pipeline->screen_out, (long) pipeline->screen_pid);
+ return pipeline;
+}
+
+void destroy_less_pipeline(LessPipeline* pipeline) {
+ close(pipeline->rstat_file);
+ close(pipeline->less_in);
+ close(pipeline->screen_out);
+ close(pipeline->less_in_pipe[0]); close(pipeline->less_in_pipe[1]);
+ close(pipeline->screen_in_pipe[0]); close(pipeline->screen_in_pipe[1]);
+ close(pipeline->screen_out_pipe[0]); close(pipeline->screen_out_pipe[1]);
+ if (pipeline->tempfile != NULL)
+ unlink(pipeline->tempfile);
+ unlink(rstat_file_name);
+ free(pipeline);
+}
diff --git a/lesstest/run b/lesstest/run
new file mode 100755
index 0000000..04e8a4f
--- /dev/null
+++ b/lesstest/run
@@ -0,0 +1,74 @@
+#!/usr/bin/perl
+use strict;
+
+# Run one or more test files.
+
+my $usage = "usage: run [-l less.exe] [-o lesstest-opts] [file.lt | dir]...\n";
+
+use Getopt::Std;
+
+my $testdir = ".";
+my $lesstest = "$testdir/lesstest";
+my $less = "$testdir/../obj/less";
+my $lesstest_opts = "";
+my %opt;
+
+exit (main() ? 0 : 1);
+sub main {
+ die $usage if not getopts('l:o:v', \%opt);
+ die $usage if not @ARGV;
+ $less = $opt{l} if $opt{l};
+ $lesstest_opts = $opt{o};
+ $lesstest_opts =~ s/^\s*//;
+ $lesstest_opts =~ s/\s*$//;
+ $lesstest_opts = '-'.$lesstest_opts if $lesstest_opts and $lesstest_opts !~ /^-/;
+
+ my $errs = 0;
+ foreach my $file (@ARGV) {
+ $errs += run($file);
+ }
+ if ($errs > 0) {
+ print STDERR "ERRS $errs errors\n";
+ return 0;
+ }
+ return 1;
+}
+
+sub run {
+ my ($file) = @_;
+ if (-d $file) {
+ return run_dir($file);
+ }
+ if ($file !~ /\.lt$/) {
+ print "$0: WARNING skipping $file: not .lt file\n";
+ return 0;
+ }
+ if (not -f $file) {
+ print STDERR "ERR cannot open $file\n";
+ return 1;
+ }
+ ##print STDERR "FILE $file\n";
+ my $cmd = "$lesstest -s $testdir/lt_screen -t $file $lesstest_opts $less";
+ print STDERR "RUN $cmd\n" if $opt{v};
+ if (system $cmd) {
+ print STDERR "ERR running $cmd\n";
+ return 1;
+ }
+ return 0;
+}
+
+sub run_dir {
+ my ($dir) = @_;
+ my $errs = 0;
+ my $dd;
+ if (not opendir($dd, $dir)) {
+ print STDERR "ERR cannot open directory $dir\n";
+ return 1;
+ }
+ while (my $entry = readdir($dd)) {
+ next if $entry =~ /^\./;
+ $errs += run("$dir/$entry");
+ }
+ closedir $dd;
+ return $errs;
+}
diff --git a/lesstest/run.c b/lesstest/run.c
new file mode 100644
index 0000000..d403ad6
--- /dev/null
+++ b/lesstest/run.c
@@ -0,0 +1,197 @@
+#include <time.h>
+#include <errno.h>
+#include <setjmp.h>
+#include "lesstest.h"
+
+extern int verbose;
+extern int less_quit;
+extern int details;
+extern TermInfo terminfo;
+extern int run_catching;
+extern jmp_buf run_catch;
+
+void sleep_ms(int ms) {
+ #define NS_PER_MS (1000*1000)
+ struct timespec tm;
+ tm.tv_sec = ms / 1000;
+ tm.tv_nsec = (ms % 1000) * NS_PER_MS;
+ if (nanosleep(&tm, NULL) < 0)
+ fprintf(stderr, "sleep error: %s\n", strerror(errno));
+}
+
+void send_char(LessPipeline* pipeline, wchar ch) {
+ if (verbose) fprintf(stderr, "send %lx\n", ch);
+ byte cbuf[UNICODE_MAX_BYTES];
+ byte* cp = cbuf;
+ store_wchar(&cp, ch);
+ write(pipeline->less_in, cbuf, cp-cbuf);
+}
+
+
+void wait_less_ready(LessPipeline* pipeline) {
+ if (pipeline->rstat_file < 0) return;
+ for (;;) {
+ lseek(pipeline->rstat_file, SEEK_SET, 0);
+ char st;
+ if (read(pipeline->rstat_file, &st, 1) == 1) {
+ if (st == 'R')
+ break;
+ if (st == 'Q') {
+ less_quit = 1;
+ fprintf(stderr, "less quit\n");
+ break;
+ }
+ }
+ sleep_ms(1);
+ }
+ sleep_ms(25); // why is this needed? rstat should prevent need for this
+}
+
+int read_screen(LessPipeline* pipeline, byte* buf, int buflen) {
+ if (verbose) fprintf(stderr, "gen: read screen\n");
+ wait_less_ready(pipeline);
+ if (less_quit)
+ return 0;
+ kill(pipeline->screen_pid, LTSIG_SCREEN_DUMP);
+ int rn = 0;
+ for (; rn <= buflen; ++rn) {
+ if (read(pipeline->screen_out, &buf[rn], 1) != 1)
+ break;
+ if (buf[rn] == '\n')
+ break;
+ }
+ return rn;
+}
+
+void read_and_display_screen(LessPipeline* pipeline) {
+ byte rbuf[MAX_SCREENBUF_SIZE];
+ int rn = read_screen(pipeline, rbuf, sizeof(rbuf));
+ if (rn == 0) return;
+ printf("%s", terminfo.clear_screen);
+ display_screen(rbuf, rn, pipeline->screen_width, pipeline->screen_height, 1);
+ log_screen(rbuf, rn);
+}
+
+int curr_screen_match(LessPipeline* pipeline, const byte* img, int imglen) {
+ byte curr[MAX_SCREENBUF_SIZE];
+ int currlen = read_screen(pipeline, curr, sizeof(curr));
+ if (currlen == imglen && memcmp(img, curr, imglen) == 0)
+ return 1;
+ if (details) {
+ fprintf(stderr, "MISMATCH: expect:\n");
+ display_screen(img, imglen, pipeline->screen_width, pipeline->screen_height, 0);
+ fprintf(stderr, "got:\n");
+ display_screen(curr, currlen, pipeline->screen_width, pipeline->screen_height, 0);
+ }
+ return 0;
+}
+
+int run_interactive(char* const* argv, int argc, char* const* prog_envp) {
+ char* const* envp = less_envp(prog_envp, LT_ENV_PREFIX);
+ LessPipeline* pipeline = create_less_pipeline(argv, argc, envp);
+ if (pipeline == NULL)
+ return 0;
+ const char* textfile = (pipeline->tempfile != NULL) ? pipeline->tempfile : argv[argc-1];
+ if (!log_test_header(argv, argc, textfile)) {
+ destroy_less_pipeline(pipeline);
+ return 0;
+ }
+ less_quit = 0;
+ int ttyin = 0; // stdin
+ raw_mode(ttyin, 1);
+ read_and_display_screen(pipeline);
+ while (!less_quit) {
+ wchar ch = read_wchar(ttyin);
+ if (ch == terminfo.backspace_key)
+ ch = '\b';
+ if (verbose) fprintf(stderr, "tty %c (%lx)\n", pr_ascii(ch), ch);
+ log_tty_char(ch);
+ send_char(pipeline, ch);
+ read_and_display_screen(pipeline);
+ }
+ log_test_footer();
+ raw_mode(ttyin, 0);
+ destroy_less_pipeline(pipeline);
+ return 1;
+}
+
+int run_test(TestSetup* setup, FILE* testfd) {
+ const char* setup_name = setup->argv[setup->argc-1];
+ fprintf(stderr, "RUN %s\n", setup_name);
+ LessPipeline* pipeline = create_less_pipeline(setup->argv, setup->argc,
+ less_envp(setup->env.env_list, ""));
+ if (pipeline == NULL)
+ return 0;
+ less_quit = 0;
+ wchar last_char = 0;
+ int ok = 1;
+ int cmds = 0;
+ if (setjmp(run_catch)) {
+ fprintf(stderr, "\nINTR test interrupted\n");
+ ok = 0;
+ } else {
+ run_catching = 1;
+ while (!less_quit) {
+ char line[10000];
+ int line_len = read_zline(testfd, line, sizeof(line));
+ if (line_len < 0)
+ break;
+ if (line_len < 1)
+ continue;
+ switch (line[0]) {
+ case '+':
+ last_char = (wchar) strtol(line+1, NULL, 16);
+ send_char(pipeline, last_char);
+ ++cmds;
+ break;
+ case '=':
+ if (!curr_screen_match(pipeline, (byte*)line+1, line_len-1)) {
+ ok = 0;
+ fprintf(stderr, "FAIL %s on cmd #%d (%c %lx)\n",
+ setup_name, cmds, pr_ascii(last_char), last_char);
+ }
+ break;
+ case 'Q':
+ less_quit = 1;
+ break;
+ case '\n':
+ case '!':
+ break;
+ default:
+ fprintf(stderr, "unrecognized char at start of \"%s\"\n", line);
+ return 0;
+ }
+ }
+ }
+ run_catching = 0;
+ destroy_less_pipeline(pipeline);
+ fprintf(stderr, "%s %s (%d commands)\n", ok ? "OK " : "FAIL", setup_name, cmds);
+ return ok;
+}
+
+int run_testfile(const char* testfile, const char* less) {
+ FILE* testfd = fopen(testfile, "r");
+ if (testfd == NULL) {
+ fprintf(stderr, "cannot open %s\n", testfile);
+ return 0;
+ }
+ int tests = 0;
+ int fails = 0;
+ for (;;) {
+ TestSetup* setup = read_test_setup(testfd, less);
+ if (setup == NULL)
+ break;
+ ++tests;
+ int ok = run_test(setup, testfd);
+ free_test_setup(setup);
+ if (!ok) ++fails;
+ }
+#if 0
+ fprintf(stderr, "DONE %d test%s", tests, tests==1?"":"s");
+ if (tests > fails) fprintf(stderr, ", %d ok", tests-fails);
+ if (fails > 0) fprintf(stderr, ", %d failed", fails);
+ fprintf(stderr, "\n");
+#endif
+ fclose(testfd);
+ return (fails == 0);
+}
diff --git a/lesstest/term.c b/lesstest/term.c
new file mode 100644
index 0000000..93701ff
--- /dev/null
+++ b/lesstest/term.c
@@ -0,0 +1,85 @@
+#include <string.h>
+#include <fcntl.h>
+#include <termios.h>
+#include <termcap.h>
+#include <sys/ioctl.h>
+#include "lesstest.h"
+
+TermInfo terminfo;
+
+void raw_mode(int tty, int on) {
+ struct termios s;
+ static struct termios save_term;
+ if (!on) {
+ s = save_term;
+ } else {
+ tcgetattr(tty, &s);
+ save_term = s;
+ s.c_lflag &= ~(ICANON | ECHO | ECHOE | ECHOK | ECHONL);
+ s.c_oflag |= (TAB3 | OPOST | ONLCR);
+ s.c_oflag &= ~(OCRNL | ONOCR | ONLRET);
+ s.c_cc[VMIN] = 1;
+ s.c_cc[VTIME] = 0;
+ }
+ tcsetattr(tty, TCSADRAIN, &s);
+}
+
+int env_number(const char* s) {
+ return (s == NULL) ? 0 : atoi(s);
+}
+
+#if 0
+int get_screen_size(int* screen_width, int* screen_height) {
+ *screen_width = env_number("COLUMNS");
+ *screen_height = env_number("ROWS");
+ if (*screen_width == 0 || *screen_height == 0) {
+ struct winsize w;
+ if (ioctl(2, TIOCGWINSZ, &w) == 0) {
+ *screen_height = w.ws_row;
+ *screen_width = w.ws_col;
+ }
+ }
+ return (*screen_width > 0 && *screen_height > 0);
+}
+#endif
+
+void setup_mode(char* enter_cap, char* exit_cap, char** enter_str, char** exit_str, char** spp) {
+ *enter_str = tgetstr(enter_cap, spp);
+ if (*enter_str == NULL) *enter_str = "";
+ *exit_str = tgetstr(exit_cap, spp);
+ if (*exit_str == NULL) *exit_str = tgetstr("me", spp);
+ if (*exit_str == NULL) *exit_str = "";
+}
+
+int setup_term(void) {
+ static char termbuf[4096];
+ static char sbuf[4096];
+ char* term = getenv("TERM");
+ if (term == NULL) term = "dumb";
+ if (tgetent(termbuf, term) <= 0) {
+ fprintf(stderr, "cannot setup terminal %s\n", term);
+ return 0;
+ }
+ char* sp = sbuf;
+ setup_mode("so", "se", &terminfo.enter_standout, &terminfo.exit_standout, &sp);
+ setup_mode("us", "ue", &terminfo.enter_underline, &terminfo.exit_underline, &sp);
+ setup_mode("md", "me", &terminfo.enter_bold, &terminfo.exit_bold, &sp);
+ setup_mode("mb", "me", &terminfo.enter_blink, &terminfo.exit_blink, &sp);
+ terminfo.cursor_move = tgetstr("cm", &sp);
+ if (terminfo.cursor_move == NULL) terminfo.cursor_move = "";
+ terminfo.clear_screen = tgetstr("cl", &sp);
+ if (terminfo.clear_screen == NULL) terminfo.clear_screen = "";
+ char* bs = tgetstr("kb", &sp);
+ terminfo.backspace_key = (bs != NULL && strlen(bs) == 1) ? *bs : '\b';
+ terminfo.init_term = tgetstr("ti", &sp);
+ terminfo.deinit_term = tgetstr("te", &sp);
+ terminfo.enter_keypad = tgetstr("ks", &sp);
+ terminfo.exit_keypad = tgetstr("ke", &sp);
+ terminfo.key_right = tgetstr("kr", &sp);
+ terminfo.key_left = tgetstr("kl", &sp);
+ terminfo.key_up = tgetstr("ku", &sp);
+ terminfo.key_down = tgetstr("kd", &sp);
+ terminfo.key_home = tgetstr("kh", &sp);
+ terminfo.key_end = tgetstr("@7", &sp);
+ return 1;
+}
diff --git a/lesstest/unicode.c b/lesstest/unicode.c
new file mode 100644
index 0000000..beb9bfc
--- /dev/null
+++ b/lesstest/unicode.c
@@ -0,0 +1,28 @@
+#include "lt_types.h"
+
+typedef struct wchar_range { wchar first, last; } wchar_range;
+
+static wchar_range wide_chars[] = {
+#include "../wide.uni"
+};
+
+static int is_in_table(wchar ch, wchar_range table[], int count) {
+ if (ch < table[0].first)
+ return 0;
+ int lo = 0;
+ int hi = count - 1;
+ while (lo <= hi) {
+ int mid = (lo + hi) / 2;
+ if (ch > table[mid].last)
+ lo = mid + 1;
+ else if (ch < table[mid].first)
+ hi = mid - 1;
+ else
+ return 1;
+ }
+ return 0;
+}
+
+int is_wide_char(wchar ch) {
+ return is_in_table(ch, wide_chars, countof(wide_chars));
+}
diff --git a/lesstest/wchar.c b/lesstest/wchar.c
new file mode 100644
index 0000000..7e0e3fb
--- /dev/null
+++ b/lesstest/wchar.c
@@ -0,0 +1,70 @@
+#include <unistd.h>
+#include "lt_types.h"
+
+// Return number of bytes in the UTF-8 sequence which begins with a given byte.
+int wchar_len(byte b) {
+ if ((b & 0xE0) == 0xC0) return 2;
+ if ((b & 0xF0) == 0xE0) return 3;
+ if ((b & 0xF8) == 0xF0) return 4;
+ return 1;
+}
+
+void store_wchar(byte** p, wchar ch) {
+ if (ch < 0x80) {
+ *(*p)++ = (char) ch;
+ } else if (ch < 0x800) {
+ *(*p)++ = (byte) (0xC0 | ((ch >> 6) & 0x1F));
+ *(*p)++ = (byte) (0x80 | (ch & 0x3F));
+ } else if (ch < 0x10000) {
+ *(*p)++ = (byte) (0xE0 | ((ch >> 12) & 0x0F));
+ *(*p)++ = (byte) (0x80 | ((ch >> 6) & 0x3F));
+ *(*p)++ = (byte) (0x80 | (ch & 0x3F));
+ } else {
+ *(*p)++ = (byte) (0xF0 | ((ch >> 18) & 0x07));
+ *(*p)++ = (byte) (0x80 | ((ch >> 12) & 0x3F));
+ *(*p)++ = (byte) (0x80 | ((ch >> 6) & 0x3F));
+ *(*p)++ = (byte) (0x80 | (ch & 0x3F));
+ }
+}
+
+wchar load_wchar(const byte** p) {
+ wchar ch;
+ switch (wchar_len(**p)) {
+ default:
+ ch = *(*p)++ & 0xFF;
+ break;
+ case 2:
+ ch = (*(*p)++ & 0x1F) << 6;
+ ch |= *(*p)++ & 0x3F;
+ break;
+ case 3:
+ ch = (*(*p)++ & 0x0F) << 12;
+ ch |= (*(*p)++ & 0x3F) << 6;
+ ch |= (*(*p)++ & 0x3F);
+ break;
+ case 4:
+ ch = (*(*p)++ & 0x07) << 18;
+ ch |= (*(*p)++ & 0x3F) << 12;
+ ch |= (*(*p)++ & 0x3F) << 6;
+ ch |= (*(*p)++ & 0x3F);
+ break;
+ }
+ return ch;
+}
+
+wchar read_wchar(int fd) {
+ byte cbuf[UNICODE_MAX_BYTES];
+ int n = read(fd, &cbuf[0], 1);
+ if (n <= 0)
+ return 0;
+ int len = wchar_len(cbuf[0]);
+ int i;
+ for (i = 1; i < len; ++i) {
+ int n = read(fd, &cbuf[i], 1);
+ if (n != 1) return 0;
+ }
+ const byte* cp = cbuf;
+ wchar ch = load_wchar(&cp);
+ // assert(cp-cbuf == len);
+ return ch;
+}
diff --git a/lesstest/wchar.h b/lesstest/wchar.h
new file mode 100644
index 0000000..0ffd541
--- /dev/null
+++ b/lesstest/wchar.h
@@ -0,0 +1,5 @@
+int wchar_len(byte ch);
+void store_wchar(byte** p, wchar ch);
+wchar load_wchar(const byte** p);
+wchar read_wchar(int fd);
+int is_wide_char(wchar ch);
--
2.27.0