From 815ba9ae9aeb64af9d7b40cf4c5def86017e6de9 Mon Sep 17 00:00:00 2001 From: EibzChan 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 +#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 +#include +#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 +#include +#include +#include +#include +#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 +#include +#include +#include +#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 +#include +#include +#include +#include +#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 + +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 +#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 +#include +#include +#include +#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 +#include +#include +#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 +#include +#include +#include +#include +#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 +#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