Commit Diff


commit - d95864cd5d79ee17fbd13a3ab18ffd394b27f83a
commit + b85a34968b050b10c36277d23e4cbb4a3e903a36
blob - 2328859beb94e97d832f30c6cbb5eb26ac6d3b0e
blob + 618f75b58a5e3c7455952c6678e8793cdef05cae
--- README
+++ README
@@ -52,6 +52,19 @@ store test data in a directory other than /tmp, such a
  $ mkdir ~/got-test
  $ make regress GOT_TEST_ROOT=~/got-test
 
+To run the tog automated test suite compile a tog regress build,
+which cannot make use of pledge(2).
+
+ $ make -C tog clean
+ $ make tog-regress
+ $ make install
+
+Like Got, either individual tests or the entire suite can be run:
+
+ $ cd regress/tog
+ $ make		# run all tests
+ $ ./log.sh	# run log view tests
+
 Man page files in the Got source tree can be viewed with 'man -l':
 
  $ man -l got/got.1
@@ -151,7 +164,6 @@ exist in the regress/ directory. Most such tests are u
 unlikely that a problem found during regular usage will require a test
 to be written in C.
 
-Some areas of code, such as the tog UI, are not covered by automated tests.
 Please always try to find a way to trigger your problem via the command line
 interface before reporting a problem without a written test case included.
 If writing an automated test really turns out to be impossible, please
blob - /dev/null
blob + 71f85ad629bf5914b21e1b50faff97154ab4b516 (mode 644)
--- /dev/null
+++ regress/tog/Makefile
@@ -0,0 +1,12 @@
+REGRESS_TARGETS=log
+NOOBJ=Yes
+
+GOT_TEST_ROOT=/tmp
+
+log:
+	./log.sh -q -r "$(GOT_TEST_ROOT)"
+cleanup:
+	./cleanup.sh -q -r "$(GOT_TEST_ROOT)"
+
+
+.include <bsd.regress.mk>
blob - /dev/null
blob + cba908caf919a31069c38c9bd18d8b7ea8952ba1 (mode 644)
--- /dev/null
+++ regress/tog/common.sh
@@ -0,0 +1,120 @@
+#!/bin/sh
+#
+# Copyright (c) 2019, 2020 Stefan Sperling <stsp@openbsd.org>
+# Copyright (c) 2023 Mark Jamsek <mark@jamsek.dev>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+. ../cmdline/common.sh
+
+unset TOG_VIEW_SPLIT_MODE
+unset LC_ALL
+export LC_ALL=C.UTF-8
+export COLUMNS=80
+export LINES=24
+
+widechar_filename()
+{
+	echo "選り抜き記事"
+}
+
+widechar_file_content()
+{
+	cat <<-EOF
+	ウィリアム・ユワート・グラッドストン(英語: William Ewart Gladstone PC FRS FSS、1809年12月29日 - 1898年5月19日)は、イギリスの政治家。
+
+	ヴィクトリア朝中期から後期にかけて、自由党を指導して、4度にわたり首相を務めた。
+
+	生涯を通じて敬虔なイングランド国教会の信徒であり、キリスト教の精神を政治に反映させることを目指した。多くの自由主義改革を行い、帝国主義にも批判的であった。好敵手である保守党党首ベンジャミン・ディズレーリとともにヴィクトリア朝イギリスの政党政治を代表する人物として知れる。……
+	EOF
+}
+
+widechar_logmsg()
+{
+	cat <<-EOF
+	選り抜き記事ウィリアム・ユワート・グラッドストン(英語: William Ewart Gladstone PC FRS FSS、1809年12月29日 - 1898年5月19日)は、イギリスの政治家。
+
+
+	    良質な記事 おまかせ表示 つまみ読み 選考
+	EOF
+}
+
+widechar_commit()
+{
+	local repo="$1"
+
+	echo "$(widechar_file_content)" > $repo/$(widechar_filename)
+
+	(cd $repo && git add $(widechar_filename) > /dev/null)
+	(cd $repo && git commit -q --cleanup=verbatim -m "$(widechar_logmsg)" \
+	    > /dev/null)
+}
+
+set_test_env()
+{
+	export GOT_TOG_TEST=$1
+	export TOG_SCR_DUMP=$2
+
+	if [ -n "${3}" ]; then
+		export COLUMNS=${3}
+	fi
+
+	if [ -n "${4}" ]; then
+		export LINES=${4}
+	fi
+}
+
+test_init()
+{
+	local testname="$1"
+	local columns="$2"
+	local lines="$3"
+	local no_tree="$4"
+
+	if [ -z "$testname" ]; then
+		echo "No test name provided" >&2
+		return 1
+	fi
+
+	testroot=`mktemp -d "$GOT_TEST_ROOT/tog-test-$testname-XXXXXXXX"`
+
+	set_test_env $testroot/log_test $testroot/view $columns $lines
+
+	mkdir $testroot/repo
+	git_init $testroot/repo
+
+	if [ -z "$no_tree" ]; then
+		make_test_tree $testroot/repo
+		cd $testroot/repo && git add .
+		git_commit $testroot/repo -m "adding the test tree"
+	fi
+}
+
+run_test()
+{
+	testfunc="$1"
+
+	if [ -n "$regress_run_only" ]; then
+		case "$regress_run_only" in
+		*$testfunc*) ;;
+		*) return ;;
+		esac
+	fi
+
+	if [ -z "$GOT_TEST_QUIET" ]; then
+		echo -n "$testfunc "
+	fi
+
+	# run test in subshell to keep defaults unchanged
+	($testfunc)
+}
blob - /dev/null
blob + 17a02a06bcc9a0d2dfc44329aa56339f21148c9d (mode 755)
--- /dev/null
+++ regress/tog/log.sh
@@ -0,0 +1,364 @@
+#!/bin/sh
+#
+# Copyright (c) 2023 Mark Jamsek <mark@jamsek.dev>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+. ./common.sh
+
+test_log_hsplit_diff()
+{
+	test_init log_hsplit_diff
+
+	local head_id=`git_show_head $testroot/repo`
+	local author_time=`git_show_author_time $testroot/repo`
+	local date=`date -u -r $author_time +"%a %b %e %X %Y UTC"`
+	local ymd=`date -u -r $author_time +"%G-%m-%d"`
+
+	cat <<EOF >$testroot/log_test
+KEY_ENTER	open diff view of selected commit
+S		toggle horizontal split
+SCREENDUMP
+EOF
+
+	cat <<EOF >$testroot/view.expected
+commit $head_id [1/1] master
+$ymd flan_hacker  adding the test tree
+
+
+
+
+--------------------------------------------------------------------------------
+[1/40] diff /dev/null $head_id
+commit $head_id (master)
+from: Flan Hacker <flan_hacker@openbsd.org>
+date: $date
+
+adding the test tree
+
+A  alpha         |  1+  0-
+A  beta          |  1+  0-
+A  epsilon/zeta  |  1+  0-
+A  gamma/delta   |  1+  0-
+
+4 files changed, 4 insertions(+), 0 deletions(-)
+
+commit - /dev/null
+commit + $head_id
+blob - /dev/null
+EOF
+
+	cd $testroot/repo && tog log
+	cmp -s $testroot/view.expected $testroot/view
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/view.expected $testroot/view
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
+test_log_vsplit_diff()
+{
+	# make screen wide enough for vsplit
+	test_init log_vsplit_diff 142
+
+	local head_id=`git_show_head $testroot/repo`
+	local author_time=`git_show_author_time $testroot/repo`
+	local date=`date -u -r $author_time +"%a %b %e %X %Y UTC"`
+	local ymd=`date -u -r $author_time +"%G-%m-%d"`
+	local blobid_alpha=`get_blob_id $testroot/repo "" alpha`
+	local blobid_beta=`get_blob_id $testroot/repo "" beta`
+
+	cat <<EOF >$testroot/log_test
+KEY_ENTER	open diff view of selected commit in vertical split
+SCREENDUMP
+EOF
+
+	cat <<EOF >$testroot/view.expected
+commit $head_id [1/1] master |[1/40] diff /dev/null $head_id
+$ymd flan_hacker  adding the test tree                 |commit $head_id (master)
+                                                             |from: Flan Hacker <flan_hacker@openbsd.org>
+                                                             |date: $date
+                                                             |
+                                                             |adding the test tree
+                                                             |
+                                                             |A  alpha         |  1+  0-
+                                                             |A  beta          |  1+  0-
+                                                             |A  epsilon/zeta  |  1+  0-
+                                                             |A  gamma/delta   |  1+  0-
+                                                             |
+                                                             |4 files changed, 4 insertions(+), 0 deletions(-)
+                                                             |
+                                                             |commit - /dev/null
+                                                             |commit + $head_id
+                                                             |blob - /dev/null
+                                                             |blob + $blobid_alpha (mode 644)
+                                                             |--- /dev/null
+                                                             |+++ alpha
+                                                             |@@ -0,0 +1 @@
+                                                             |+alpha
+                                                             |blob - /dev/null
+                                                             |blob + $blobid_beta (mode 644)
+EOF
+
+	cd $testroot/repo && tog log
+	cmp -s $testroot/view.expected $testroot/view
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/view.expected $testroot/view
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
+test_log_show_author()
+{
+	# make view wide enough to show id
+	test_init log_show_author 120 4
+
+	local head_id=`git_show_head $testroot/repo`
+	local author_time=`git_show_author_time $testroot/repo`
+	local date=`date -u -r $author_time +"%a %b %e %X %Y UTC"`
+	local ymd=`date -u -r $author_time +"%G-%m-%d"`
+	local head_id_len8=`trim_obj_id 32 $head_id`
+
+	echo "mod alpha" > $testroot/repo/alpha
+	cd $testroot/repo && git add .
+	cd $testroot/repo && \
+	    git commit --author "Johnny Cash <john@cash.net>" -m author > \
+	    /dev/null
+
+	local commit1=`git_show_head $testroot/repo`
+	local id1_len8=`trim_obj_id 32 $commit1`
+
+	cat <<EOF >$testroot/log_test
+@		toggle show author
+SCREENDUMP
+EOF
+
+	cat <<EOF >$testroot/view.expected
+commit $commit1 [1/2] master
+$ymd $id1_len8 john         author
+$ymd $head_id_len8 flan_hacker  adding the test tree
+:show commit author
+EOF
+
+	cd $testroot/repo && tog log
+	cmp -s $testroot/view.expected $testroot/view
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/view.expected $testroot/view
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
+test_log_scroll_right()
+{
+	test_init log_scroll_right 80 3
+
+	local head_id=`git_show_head $testroot/repo`
+	local author_time=`git_show_author_time $testroot/repo`
+	local date=`date -u -r $author_time +"%a %b %e %X %Y UTC"`
+	local ymd=`date -u -r $author_time +"%G-%m-%d"`
+	local msg="scroll this log message to the right four characters"
+	local scrolled_msg="ll this log message to the right four characters"
+
+	echo "mod alpha" > $testroot/repo/alpha
+	cd $testroot/repo && git add . && git commit -m "$msg" > /dev/null
+
+	local commit1=`git_show_head $testroot/repo`
+
+	cat <<EOF >$testroot/log_test
+l		scroll right
+l		scroll right
+SCREENDUMP
+EOF
+
+	cat <<EOF >$testroot/view.expected
+commit $commit1 [1/2] master
+$ymd flan_hacker  $scrolled_msg
+$ymd flan_hacker  ng the test tree
+EOF
+
+	cd $testroot/repo && tog log
+	cmp -s $testroot/view.expected $testroot/view
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/view.expected $testroot/view
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
+test_log_hsplit_ref()
+{
+	test_init log_hsplit_ref 80 10
+
+	local head_id=`git_show_head $testroot/repo`
+	local author_time=`git_show_author_time $testroot/repo`
+	local date=`date -u -r $author_time +"%a %b %e %X %Y UTC"`
+	local ymd=`date -u -r $author_time +"%G-%m-%d"`
+
+	cat <<EOF >$testroot/log_test
+R		open ref view
+S		toggle horizontal split
+-		reduce size of ref view split
+SCREENDUMP
+EOF
+
+	cat <<EOF >$testroot/view.expected
+commit $head_id [1/1] master
+$ymd flan_hacker  adding the test tree
+
+--------------------------------------------------------------------------------
+references [1/2]
+HEAD -> refs/heads/master
+refs/heads/master
+
+
+
+EOF
+
+	cd $testroot/repo && tog log
+	cmp -s $testroot/view.expected $testroot/view
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/view.expected $testroot/view
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
+test_log_hsplit_tree()
+{
+	test_init log_hsplit_tree 80 10
+
+	local head_id=`git_show_head $testroot/repo`
+	local author_time=`git_show_author_time $testroot/repo`
+	local date=`date -u -r $author_time +"%a %b %e %X %Y UTC"`
+	local ymd=`date -u -r $author_time +"%G-%m-%d"`
+
+	cat <<EOF >$testroot/log_test
+T		open tree view
+S		toggle horizontal split
+j		move selection cursor down one entry to "beta"
+-		reduce size of tree view split
+SCREENDUMP
+EOF
+
+	cat <<EOF >$testroot/view.expected
+commit $head_id [1/1] master
+$ymd flan_hacker  adding the test tree
+
+--------------------------------------------------------------------------------
+commit $head_id
+[2/4] /
+
+  alpha
+  beta
+  epsilon/
+EOF
+
+	cd $testroot/repo && tog log
+	cmp -s $testroot/view.expected $testroot/view
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/view.expected $testroot/view
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
+test_log_logmsg_widechar()
+{
+	# make view wide enough to fit logmsg line length
+	# but short enough so long diff lines are truncated
+	test_init log_logmsg_widechar 182 30
+	widechar_commit $testroot/repo
+
+	local head_id=`git_show_head $testroot/repo`
+	local author_time=`git_show_author_time $testroot/repo`
+	local date=`date -u -r $author_time +"%a %b %e %X %Y UTC"`
+	local commit1=`git_show_parent_commit $testroot/repo`
+	local blobid=`get_blob_id $testroot/repo "" $(widechar_filename)`
+
+	cat <<EOF >$testroot/log_test
+KEY_ENTER	open selected commit in diff view
+F		toggle fullscreen
+SCREENDUMP
+EOF
+
+	cat <<EOF >$testroot/view.expected
+[1/26] diff $commit1 $head_id
+commit $head_id (master)
+from: Flan Hacker <flan_hacker@openbsd.org>
+date: $date
+
+$(widechar_logmsg)
+
+A  $(widechar_filename)  |  5+  0-
+
+1 file changed, 5 insertions(+), 0 deletions(-)
+
+commit - $commit1
+commit + $head_id
+blob - /dev/null
+blob + $blobid (mode 644)
+--- /dev/null
++++ $(widechar_filename)
+@@ -0,0 +1,5 @@
++ウィリアム・ユワート・グラッドストン(英語: William Ewart Gladstone PC FRS FSS、1809年12月29日 - 1898年5月19日)は、イギリスの政治家。
++
++ヴィクトリア朝中期から後期にかけて、自由党を指導して、4度にわたり首相を務めた。
++
++生涯を通じて敬虔なイングランド国教会の信徒であり、キリスト教の精神を政治に反映させることを目指した。多くの自由主義改革を行い、帝国主義にも批判的であった。好敵手である保守党党首ベン
+
+
+
+(END)
+EOF
+
+	cd $testroot/repo && tog log
+	cmp -s $testroot/view.expected $testroot/view
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/view.expected $testroot/view
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
+test_parseargs "$@"
+run_test test_log_hsplit_diff
+run_test test_log_vsplit_diff
+run_test test_log_show_author
+run_test test_log_scroll_right
+run_test test_log_hsplit_ref
+run_test test_log_hsplit_tree
+run_test test_log_logmsg_widechar
blob - d037ba6e7f9ef2bca82a60d6147f9f09ac20098c
blob + 8afaab18b7d71f4b1b6d555d34c39d3d3df20166
--- tog/tog.c
+++ tog/tog.c
@@ -615,6 +615,17 @@ struct tog_key_map {
 	enum tog_keymap_type	 type;
 };
 
+/* curses io for tog regress */
+struct tog_io {
+	FILE	*cin;
+	FILE	*cout;
+	FILE	*f;
+};
+
+#define TOG_SCREEN_DUMP		"SCREENDUMP"
+#define TOG_SCREEN_DUMP_LEN	(sizeof(TOG_SCREEN_DUMP) - 1)
+#define TOG_KEY_SCRDUMP		SHRT_MIN
+
 /*
  * We implement two types of views: parent views and child views.
  *
@@ -1417,6 +1428,122 @@ switch_split(struct tog_view *view)
 }
 
 /*
+ * Strip trailing whitespace from str starting at byte *n;
+ * if *n < 0, use strlen(str). Return new str length in *n.
+ */
+static void
+strip_trailing_ws(char *str, int *n)
+{
+	size_t x = *n;
+
+	if (str == NULL || *str == '\0')
+		return;
+
+	if (x < 0)
+		x = strlen(str);
+
+	while (x-- > 0 && isspace((unsigned char)str[x]))
+		str[x] = '\0';
+
+	*n = x + 1;
+}
+
+/*
+ * Extract visible substring of line y from the curses screen
+ * and strip trailing whitespace. If vline is set and locale is
+ * UTF-8, overwrite line[vline] with '|' because the ACS_VLINE
+ * character is written out as 'x'. Write the line to file f.
+ */
+static const struct got_error *
+view_write_line(FILE *f, int y, int vline)
+{
+	char	line[COLS * MB_LEN_MAX];  /* allow for multibyte chars */
+	int	r, w;
+
+	r = mvwinnstr(curscr, y, 0, line, sizeof(line));
+	if (r == ERR)
+		return got_error_fmt(GOT_ERR_RANGE,
+		    "failed to extract line %d", y);
+
+	/*
+	 * In some views, lines are padded with blanks to COLS width.
+	 * Strip them so we can diff without the -b flag when testing.
+	 */
+	strip_trailing_ws(line, &r);
+
+	if (vline > 0 && got_locale_is_utf8())
+		line[vline] = '|';
+
+	w = fprintf(f, "%s\n", line);
+	if (w != r + 1)		/* \n */
+		return got_ferror(f, GOT_ERR_IO);
+
+	return NULL;
+}
+
+/*
+ * Capture the visible curses screen by writing each line to the
+ * file at the path set via the TOG_SCR_DUMP environment variable.
+ */
+static const struct got_error *
+screendump(struct tog_view *view)
+{
+	const struct got_error	*err;
+	FILE			*f = NULL;
+	const char		*path;
+	int			 i;
+
+	path = getenv("TOG_SCR_DUMP");
+	if (path == NULL || *path == '\0')
+		return got_error_msg(GOT_ERR_BAD_PATH,
+		    "TOG_SCR_DUMP path not set to capture screen dump");
+	f = fopen(path, "wex");
+	if (f == NULL)
+		return got_error_from_errno_fmt("fopen: %s", path);
+
+	if ((view->child && view->child->begin_x) ||
+	    (view->parent && view->begin_x)) {
+		int ncols = view->child ? view->ncols : view->parent->ncols;
+
+		/* vertical splitscreen */
+		for (i = 0; i < view->nlines; ++i) {
+			err = view_write_line(f, i, ncols - 1);
+			if (err)
+				goto done;
+		}
+	} else {
+		int hline = 0;
+
+		/* fullscreen or horizontal splitscreen */
+		if ((view->child && view->child->begin_y) ||
+		    (view->parent && view->begin_y))	/* hsplit */
+			hline = view->child ?
+			    view->child->begin_y : view->begin_y;
+
+		for (i = 0; i < view->lines; i++) {
+			if (hline && got_locale_is_utf8() && i == hline - 1) {
+				int c;
+
+				/* ACS_HLINE writes out as 'q', overwrite it */
+				for (c = 0; c < view->cols; ++c)
+					fputc('-', f);
+				fputc('\n', f);
+				continue;
+			}
+
+			err = view_write_line(f, i, 0);
+			if (err)
+				goto done;
+		}
+	}
+
+done:
+	if (f && fclose(f) == EOF && err == NULL)
+		err = got_ferror(f, GOT_ERR_IO);
+	return err;
+}
+
+/*
  * Compute view->count from numeric input. Assign total to view->count and
  * return first non-numeric key entered.
  */
@@ -1493,9 +1620,48 @@ action_report(struct tog_view *view)
 		view->action = NULL;
 }
 
+/*
+ * Read the next line from the test script and assign
+ * key instruction to *ch. If at EOF, set the *done flag.
+ */
 static const struct got_error *
+tog_read_script_key(FILE *script, int *ch, int *done)
+{
+	const struct got_error	*err = NULL;
+	char			*line = NULL;
+	size_t			 linesz = 0;
+
+	if (getline(&line, &linesz, script) == -1) {
+		if (feof(script)) {
+			*done = 1;
+			goto done;
+		} else {
+			err = got_ferror(script, GOT_ERR_IO);
+			goto done;
+		}
+	} else if (strncasecmp(line, "KEY_ENTER", 9) == 0)
+		*ch = KEY_ENTER;
+	else if (strncasecmp(line, "KEY_RIGHT", 9) == 0)
+		*ch = KEY_RIGHT;
+	else if (strncasecmp(line, "KEY_LEFT", 8) == 0)
+		*ch = KEY_LEFT;
+	else if (strncasecmp(line, "KEY_DOWN", 8) == 0)
+		*ch = KEY_DOWN;
+	else if (strncasecmp(line, "KEY_UP", 6) == 0)
+		*ch = KEY_UP;
+	else if (strncasecmp(line, TOG_SCREEN_DUMP, TOG_SCREEN_DUMP_LEN) == 0)
+		*ch = TOG_KEY_SCRDUMP;
+	else
+		*ch = *line;
+
+done:
+	free(line);
+	return err;
+}
+
+static const struct got_error *
 view_input(struct tog_view **new, int *done, struct tog_view *view,
-    struct tog_view_list_head *views, int fast_refresh)
+    struct tog_view_list_head *views, struct tog_io *tog_io, int fast_refresh)
 {
 	const struct got_error *err = NULL;
 	struct tog_view *v;
@@ -1531,11 +1697,16 @@ view_input(struct tog_view **new, int *done, struct to
 	errcode = pthread_mutex_unlock(&tog_mutex);
 	if (errcode)
 		return got_error_set_errno(errcode, "pthread_mutex_unlock");
-	/* If we have an unfinished count, let C-g or backspace abort. */
-	if (view->count && --view->count) {
+
+	if (tog_io && tog_io->f) {
+		err = tog_read_script_key(tog_io->f, &ch, done);
+		if (err)
+			return err;
+	} else if (view->count && --view->count) {
 		cbreak();
 		nodelay(view->window, TRUE);
 		ch = wgetch(view->window);
+		/* let C-g or backspace abort unfinished count */
 		if (ch == CTRL('g') || ch == KEY_BACKSPACE)
 			view->count = 0;
 		else
@@ -1725,6 +1896,9 @@ view_input(struct tog_view **new, int *done, struct to
 			}
 		}
 		break;
+	case TOG_KEY_SCRDUMP:
+		err = screendump(view);
+		break;
 	default:
 		err = view->input(new, view, ch);
 		break;
@@ -1748,9 +1922,26 @@ view_needs_focus_indication(struct tog_view *view)
 }
 
 static const struct got_error *
-view_loop(struct tog_view *view)
+tog_io_close(struct tog_io *tog_io)
 {
 	const struct got_error *err = NULL;
+
+	if (tog_io->cin && fclose(tog_io->cin) == EOF)
+		err = got_ferror(tog_io->cin, GOT_ERR_IO);
+	if (tog_io->cout && fclose(tog_io->cout) == EOF && err == NULL)
+		err = got_ferror(tog_io->cout, GOT_ERR_IO);
+	if (tog_io->f && fclose(tog_io->f) == EOF && err == NULL)
+		err = got_ferror(tog_io->f, GOT_ERR_IO);
+	free(tog_io);
+	tog_io = NULL;
+
+	return err;
+}
+
+static const struct got_error *
+view_loop(struct tog_view *view, struct tog_io *tog_io)
+{
+	const struct got_error *err = NULL;
 	struct tog_view_list_head views;
 	struct tog_view *new_view;
 	char *mode;
@@ -1782,7 +1973,8 @@ view_loop(struct tog_view *view)
 		if (fast_refresh && --fast_refresh == 0)
 			halfdelay(10); /* switch to once per second */
 
-		err = view_input(&new_view, &done, view, &views, fast_refresh);
+		err = view_input(&new_view, &done, view, &views, tog_io,
+		    fast_refresh);
 		if (err)
 			break;
 
@@ -3969,9 +4161,68 @@ apply_unveil(const char *repo_path, const char *worktr
 	return NULL;
 }
 
-static void
-init_curses(void)
+static const struct got_error *
+init_mock_term(struct tog_io **tog_io, const char *test_script_path)
+{
+	const struct got_error	*err = NULL;
+	struct tog_io		*io;
+
+	if (*tog_io)
+		*tog_io = NULL;
+
+	if (test_script_path == NULL || *test_script_path == '\0')
+		return got_error_msg(GOT_ERR_IO, "GOT_TOG_TEST not defined");
+
+	io = calloc(1, sizeof(*io));
+	if (io == NULL)
+		return got_error_from_errno("calloc");
+
+	io->f = fopen(test_script_path, "re");
+	if (io->f == NULL) {
+		err = got_error_from_errno_fmt("fopen: %s",
+		    test_script_path);
+		goto done;
+	}
+
+	/* test mode, we don't want any output */
+	io->cout = fopen("/dev/null", "w+");
+	if (io->cout == NULL) {
+		err = got_error_from_errno("fopen: /dev/null");
+		goto done;
+	}
+
+	io->cin = fopen("/dev/tty", "r+");
+	if (io->cin == NULL) {
+		err = got_error_from_errno("fopen: /dev/tty");
+		goto done;
+	}
+
+	if (fseeko(io->f, 0L, SEEK_SET) == -1) {
+		err = got_error_from_errno("fseeko");
+		goto done;
+	}
+
+	/*
+	 * XXX Perhaps we should define "xterm" as the terminal
+	 * type for standardised testing instead of using $TERM?
+	 */
+	if (newterm(NULL, io->cout, io->cin) == NULL)
+		err = got_error_msg(GOT_ERR_IO,
+		    "newterm: failed to initialise curses");
+done:
+	if (err)
+		tog_io_close(io);
+	else
+		*tog_io = io;
+	return err;
+}
+
+static const struct got_error *
+init_curses(struct tog_io **tog_io)
 {
+	const struct got_error	*err = NULL;
+	const char		*test_script_path;
+
 	/*
 	 * Override default signal handlers before starting ncurses.
 	 * This should prevent ncurses from installing its own
@@ -3983,7 +4234,14 @@ init_curses(void)
 	signal(SIGINT, tog_sigint);
 	signal(SIGTERM, tog_sigterm);
 
-	initscr();
+	test_script_path = getenv("GOT_TOG_TEST");
+	if (test_script_path != NULL) {
+		err = init_mock_term(tog_io, test_script_path);
+		if (err)
+			return err;
+	} else
+		initscr();
+
 	cbreak();
 	halfdelay(1); /* Do fast refresh while initial view is loading. */
 	noecho();
@@ -3995,6 +4253,8 @@ init_curses(void)
 		start_color();
 		use_default_colors();
 	}
+
+	return NULL;
 }
 
 static const struct got_error *
@@ -4033,7 +4293,7 @@ get_in_repo_path_from_argv0(char **in_repo_path, int a
 static const struct got_error *
 cmd_log(int argc, char *argv[])
 {
-	const struct got_error *error;
+	const struct got_error *io_err, *error;
 	struct got_repository *repo = NULL;
 	struct got_worktree *worktree = NULL;
 	struct got_object_id *start_id = NULL;
@@ -4043,6 +4303,7 @@ cmd_log(int argc, char *argv[])
 	const char *head_ref_name = NULL;
 	int ch, log_branches = 0;
 	struct tog_view *view;
+	struct tog_io *tog_io = NULL;
 	int *pack_fds = NULL;
 
 	while ((ch = getopt(argc, argv, "bc:r:")) != -1) {
@@ -4102,7 +4363,9 @@ cmd_log(int argc, char *argv[])
 	if (error)
 		goto done;
 
-	init_curses();
+	error = init_curses(&tog_io);
+	if (error)
+		goto done;
 
 	error = apply_unveil(got_repo_get_path(repo),
 	    worktree ? got_worktree_get_root_path(worktree) : NULL);
@@ -4149,7 +4412,7 @@ cmd_log(int argc, char *argv[])
 		got_worktree_close(worktree);
 		worktree = NULL;
 	}
-	error = view_loop(view);
+	error = view_loop(view, tog_io);
 done:
 	free(in_repo_path);
 	free(repo_path);
@@ -4170,6 +4433,11 @@ done:
 		    got_repo_pack_fds_close(pack_fds);
 		if (error == NULL)
 			error = pack_err;
+	}
+	if (tog_io != NULL) {
+		io_err = tog_io_close(tog_io);
+		if (error == NULL)
+			error = io_err;
 	}
 	tog_free_refs();
 	return error;
@@ -5540,7 +5808,7 @@ input_diff_view(struct tog_view **new_view, struct tog
 static const struct got_error *
 cmd_diff(int argc, char *argv[])
 {
-	const struct got_error *error = NULL;
+	const struct got_error *io_err, *error;
 	struct got_repository *repo = NULL;
 	struct got_worktree *worktree = NULL;
 	struct got_object_id *id1 = NULL, *id2 = NULL;
@@ -5551,6 +5819,7 @@ cmd_diff(int argc, char *argv[])
 	int ch, force_text_diff = 0;
 	const char *errstr;
 	struct tog_view *view;
+	struct tog_io *tog_io = NULL;
 	int *pack_fds = NULL;
 
 	while ((ch = getopt(argc, argv, "aC:r:w")) != -1) {
@@ -5618,7 +5887,9 @@ cmd_diff(int argc, char *argv[])
 	if (error)
 		goto done;
 
-	init_curses();
+	error = init_curses(&tog_io);
+	if (error)
+		goto done;
 
 	error = apply_unveil(got_repo_get_path(repo), NULL);
 	if (error)
@@ -5647,7 +5918,7 @@ cmd_diff(int argc, char *argv[])
 	    ignore_whitespace, force_text_diff, NULL,  repo);
 	if (error)
 		goto done;
-	error = view_loop(view);
+	error = view_loop(view, tog_io);
 done:
 	free(label1);
 	free(label2);
@@ -5665,6 +5936,11 @@ done:
 		    got_repo_pack_fds_close(pack_fds);
 		if (error == NULL)
 			error = pack_err;
+	}
+	if (tog_io != NULL) {
+		io_err = tog_io_close(tog_io);
+		if (error == NULL)
+			error = io_err;
 	}
 	tog_free_refs();
 	return error;
@@ -6639,7 +6915,7 @@ reset_blame_view(struct tog_view *view)
 static const struct got_error *
 cmd_blame(int argc, char *argv[])
 {
-	const struct got_error *error;
+	const struct got_error *io_err, *error;
 	struct got_repository *repo = NULL;
 	struct got_worktree *worktree = NULL;
 	char *cwd = NULL, *repo_path = NULL, *in_repo_path = NULL;
@@ -6649,6 +6925,7 @@ cmd_blame(int argc, char *argv[])
 	char *commit_id_str = NULL;
 	int ch;
 	struct tog_view *view;
+	struct tog_io *tog_io = NULL;
 	int *pack_fds = NULL;
 
 	while ((ch = getopt(argc, argv, "c:r:")) != -1) {
@@ -6705,7 +6982,9 @@ cmd_blame(int argc, char *argv[])
 	if (error)
 		goto done;
 
-	init_curses();
+	error = init_curses(&tog_io);
+	if (error)
+		goto done;
 
 	error = apply_unveil(got_repo_get_path(repo), NULL);
 	if (error)
@@ -6754,7 +7033,7 @@ cmd_blame(int argc, char *argv[])
 		got_worktree_close(worktree);
 		worktree = NULL;
 	}
-	error = view_loop(view);
+	error = view_loop(view, tog_io);
 done:
 	free(repo_path);
 	free(in_repo_path);
@@ -6776,6 +7055,11 @@ done:
 		if (error == NULL)
 			error = pack_err;
 	}
+	if (tog_io != NULL) {
+		io_err = tog_io_close(tog_io);
+		if (error == NULL)
+			error = io_err;
+	}
 	tog_free_refs();
 	return error;
 }
@@ -7608,7 +7892,7 @@ usage_tree(void)
 static const struct got_error *
 cmd_tree(int argc, char *argv[])
 {
-	const struct got_error *error;
+	const struct got_error *io_err, *error;
 	struct got_repository *repo = NULL;
 	struct got_worktree *worktree = NULL;
 	char *cwd = NULL, *repo_path = NULL, *in_repo_path = NULL;
@@ -7620,6 +7904,7 @@ cmd_tree(int argc, char *argv[])
 	const char *head_ref_name = NULL;
 	int ch;
 	struct tog_view *view;
+	struct tog_io *tog_io = NULL;
 	int *pack_fds = NULL;
 
 	while ((ch = getopt(argc, argv, "c:r:")) != -1) {
@@ -7676,7 +7961,9 @@ cmd_tree(int argc, char *argv[])
 	if (error)
 		goto done;
 
-	init_curses();
+	error = init_curses(&tog_io);
+	if (error)
+		goto done;
 
 	error = apply_unveil(got_repo_get_path(repo), NULL);
 	if (error)
@@ -7729,7 +8016,7 @@ cmd_tree(int argc, char *argv[])
 		got_worktree_close(worktree);
 		worktree = NULL;
 	}
-	error = view_loop(view);
+	error = view_loop(view, tog_io);
 done:
 	free(repo_path);
 	free(cwd);
@@ -7747,6 +8034,11 @@ done:
 		    got_repo_pack_fds_close(pack_fds);
 		if (error == NULL)
 			error = pack_err;
+	}
+	if (tog_io != NULL) {
+		io_err = tog_io_close(tog_io);
+		if (error == NULL)
+			error = io_err;
 	}
 	tog_free_refs();
 	return error;
@@ -8495,12 +8787,13 @@ usage_ref(void)
 static const struct got_error *
 cmd_ref(int argc, char *argv[])
 {
-	const struct got_error *error;
+	const struct got_error *io_err, *error;
 	struct got_repository *repo = NULL;
 	struct got_worktree *worktree = NULL;
 	char *cwd = NULL, *repo_path = NULL;
 	int ch;
 	struct tog_view *view;
+	struct tog_io *tog_io = NULL;
 	int *pack_fds = NULL;
 
 	while ((ch = getopt(argc, argv, "r:")) != -1) {
@@ -8549,7 +8842,9 @@ cmd_ref(int argc, char *argv[])
 	if (error != NULL)
 		goto done;
 
-	init_curses();
+	error = init_curses(&tog_io);
+	if (error)
+		goto done;
 
 	error = apply_unveil(got_repo_get_path(repo), NULL);
 	if (error)
@@ -8574,7 +8869,7 @@ cmd_ref(int argc, char *argv[])
 		got_worktree_close(worktree);
 		worktree = NULL;
 	}
-	error = view_loop(view);
+	error = view_loop(view, tog_io);
 done:
 	free(repo_path);
 	free(cwd);
@@ -8588,6 +8883,11 @@ done:
 		    got_repo_pack_fds_close(pack_fds);
 		if (error == NULL)
 			error = pack_err;
+	}
+	if (tog_io != NULL) {
+		io_err = tog_io_close(tog_io);
+		if (error == NULL)
+			error = io_err;
 	}
 	tog_free_refs();
 	return error;
@@ -9467,7 +9767,7 @@ main(int argc, char *argv[])
 
 	setlocale(LC_CTYPE, "");
 
-#ifndef PROFILE
+#if !defined(PROFILE) && !defined(TOG_REGRESS)
 	if (pledge("stdio rpath wpath cpath flock proc tty exec sendfd unveil",
 	    NULL) == -1)
 		err(1, "pledge");