"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Mark Jamsek <mark@jamsek.com>
Subject:
tog test harness
To:
Game of Trees <gameoftrees@openbsd.org>
Date:
Wed, 12 Apr 2023 23:28:10 +1000

Download raw body.

Thread
The below diff implements the framework for tog automated tests.

I started this way back at g2k22 but out of frustration stopped working
on it when I got home. I was motivated to resume work recently when
I broke 'tog diff' :)

The implementation looks very different and _much_ simpler than the
first couple designs.

The regress/tog/log.sh test script demonstrates how tests are driven.
In brief, we feed an instruction per line, which is usually a 1:1
mapping to an actual tog key binding; for example:

KEY_ENTER
S
+
SCREENDUMP

Related envvars notwithstanding, and assuming this is a 'tog log' test,
just by being familiar with tog we know what view this generates:

  open diff view of the first commit
  toggle horizontal split
  increase the split size

We can also setup the environment when initialising each test by
defining LINES and COLUMNS to tweak the terminal size. This is good for
testing various combinations but also simplifies writing new test cases
because we can limit the screen estate to what we are targeting.

The implementation is simple: we read each line and check against
certain curses key defines or just take the first character, and pass
the result to the view's input handler. When the SCREENDUMP instruction
is received, we write the visible screen to file, which is defined via
the TOG_SCR_DUMP envvar. This is then diffed against the expected
output.

I think this approach makes writing tests really simple. We build a view
exactly like we use tog. And use the existing infrastructure to handle
input and generate views. So we only needed a little code to capture
screen dumps. This is much simpler than the previous designs. The first
implemented a vi command mode type interface which required a lot of
unnecessary parsing code, and the second injected input to
a non-canonical terminal mode that read each byte with no timeout, which
required saving, modifying, and restoring the original terminal state.

One caveat I want to highlight: testing requires a regress build
which is basically a profile build (i.e., *unpledged*):

  $ make tog-regress
  $ make install
  $ make -C regress/tog

This is because I couldn't find the set of promises that works with this
mode of curses io; I'm not sure it can be done.

Also, I've added a kanji test case ripped from stsp's ja.git repo. This
means we have embedded utf8 glyphs. It's only test scripts so perhaps
this is ok? I think it is an important apsect to test in tog, and using
the approach we took in tog.c would obfuscate the tests. But there are
certainly other approaches we could take, and I am not averse to
exploring them if we want to avoid embedded utf8.

In any case, I think the diff is ready for wider testing, and it will be
good to start growing tog's test coverage :)

diff refs/heads/main refs/heads/tt
commit - cc88020e952af813c1e01b91ab6516969562e972
commit + b0b5605a95386ed8ada5f82a545debf8f519434c
blob - 86e0c721884cc636b289ca3fd5d77a75bedc9f8f
blob + 72bace2b2e1f257595de3351ff479827283fa69f
--- Makefile
+++ Makefile
@@ -60,4 +60,7 @@ server-regress:
 server-regress:
 	${MAKE} -C regress/gotd
 
+tog-regress:
+	${MAKE} TOG_REGRESS=1
+
 .include <bsd.subdir.mk>
blob - 2328859beb94e97d832f30c6cbb5eb26ac6d3b0e
blob + 618f75b58a5e3c7455952c6678e8793cdef05cae
--- README
+++ README
@@ -52,6 +52,19 @@ Man page files in the Got source tree can be viewed wi
  $ 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 @@ Some areas of code, such as the tog UI, are not covere
 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 - 89e400f4320fe358711e282ef7a72ddfa0b30e80
blob + 246c94d7499f25bf057c19817889f00c83453008
--- tog/Makefile
+++ tog/Makefile
@@ -30,6 +30,10 @@ realinstall:
 NOMAN = Yes
 .endif
 
+.if defined(TOG_REGRESS)
+CPPFLAGS += -DTOG_REGRESS
+.endif
+
 realinstall:
 	${INSTALL} ${INSTALL_COPY} -o ${BINOWN} -g ${BINGRP} \
 	-m ${BINMODE} ${PROG} ${BINDIR}/${PROG}
blob - 43007666e1ffbbce67f7375be748a75aadbb074a
blob + 18e7c012afeb4bad36f2fa0f3059432461446a50
--- tog/tog.c
+++ tog/tog.c
@@ -611,6 +611,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.
  *
@@ -1413,6 +1424,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.
  */
@@ -1489,9 +1616,48 @@ static const struct got_error *
 		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;
@@ -1527,11 +1693,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
@@ -1721,6 +1892,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;
@@ -1744,9 +1918,26 @@ view_loop(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;
@@ -1778,7 +1969,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;
 
@@ -3965,10 +4157,69 @@ static void
 	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
 	 * broken cleanup() signal handler.
@@ -3979,7 +4230,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();
@@ -3991,6 +4249,8 @@ init_curses(void)
 		start_color();
 		use_default_colors();
 	}
+
+	return NULL;
 }
 
 static const struct got_error *
@@ -4029,7 +4289,7 @@ cmd_log(int argc, char *argv[])
 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;
@@ -4039,6 +4299,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) {
@@ -4098,7 +4359,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);
@@ -4145,7 +4408,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);
@@ -4167,6 +4430,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;
 }
@@ -5536,7 +5804,7 @@ cmd_diff(int argc, char *argv[])
 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;
@@ -5547,6 +5815,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) {
@@ -5614,7 +5883,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)
@@ -5643,7 +5914,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);
@@ -5662,6 +5933,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;
 }
@@ -6635,7 +6911,7 @@ cmd_blame(int argc, char *argv[])
 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;
@@ -6645,6 +6921,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) {
@@ -6701,7 +6978,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)
@@ -6750,7 +7029,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);
@@ -6772,6 +7051,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;
 }
@@ -7604,7 +7888,7 @@ cmd_tree(int argc, char *argv[])
 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;
@@ -7616,6 +7900,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) {
@@ -7672,7 +7957,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)
@@ -7725,7 +8012,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);
@@ -7744,6 +8031,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;
 }
@@ -8491,12 +8783,13 @@ cmd_ref(int argc, char *argv[])
 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) {
@@ -8545,7 +8838,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)
@@ -8570,7 +8865,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);
@@ -8585,6 +8880,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;
 }
@@ -9463,7 +9763,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");

-- 
Mark Jamsek <fnc.bsdbox.org|got.bsdbox.org>
GPG: F2FF 13DE 6A06 C471 CA80  E6E2 2930 DC66 86EE CF68