1902 lines
50 KiB
Diff
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
|
|
|