code: plan9front

Download patch

ref: 31913a8524edf97ad79f9f895d6ac5761b9b2fd7
parent: c3ba64f6935322f09b6de5c2285544fd471c605d
author: Ori Bernstein <ori@eigenstate.org>
date: Sun Feb 19 15:44:56 EST 2023

mkfiles: add 'mk test' support

9front has several tests scattered throughout the source,
as well as more tests in an external 'regress' repository.
Many of these tests are broken, because there is no easy
way to build and track all of them.

This pulls in several tests from different sources, deletes
the broken tests, tests with missing data, and adds a single
command that can be run from the root of the src directory
to test our system.

The hope is that as we develop new code, we add more tests,
and eventually start running the tests on every commit.
Please enter the commit message for your changes. Lines starting

diff: cannot open b/sys/src/cmd/git/test//null: file does not exist: 'b/sys/src/cmd/git/test//null' diff: cannot open b/sys/src/cmd/test/patch//null: file does not exist: 'b/sys/src/cmd/test/patch//null' diff: cannot open b/sys/src/cmd/test//null: file does not exist: 'b/sys/src/cmd/test//null'
--- a/acme/mkfile
+++ b/acme/mkfile
@@ -1,9 +1,9 @@
 </$objtype/mkfile
 
 none:VQ:
-	echo mk all, install, clean, nuke, installall, update
+	echo mk all, install, clean, nuke, installall, update, test
 
-all install clean nuke installall update:V:
+all install clean nuke installall update test:V:
 	@{cd bin/source; mk $target}
 	@{cd news/src; mk $target}
 	@{cd wiki/src; mk $target}
--- a/sys/lib/git/common.rc
+++ b/sys/lib/git/common.rc
@@ -84,10 +84,11 @@
 		base=/dev/null
 	if(! test -f $theirs)
 		theirs=/dev/null
-	if(! ape/diff3 -3 -m $ours $base $theirs > $tmp)
-		echo merge needed: $out >[1=2]
 
 	if(mergeperm $ours $base $theirs){
+		mkdir -p `{basename -d $tmp}
+		if(! ape/diff3 -3 -m $ours $base $theirs > $tmp)
+			echo merge needed: $out >[1=2]
 		mv $tmp $out
 		git/add $out
 		chmod $mergedperms $out
--- a/sys/src/ape/mkfile
+++ b/sys/src/ape/mkfile
@@ -27,3 +27,6 @@
 9src.%:V:
 	cd 9src
 	mk $stem
+
+test:VQ:
+	# nothing
--- a/sys/src/cmd/audio/flacdec/mkfile
+++ b/sys/src/cmd/audio/flacdec/mkfile
@@ -4,7 +4,7 @@
 TARGET=flacdec
 
 CC=pcc
-CFLAGS=-I. -I../libFLAC -I../libFLAC/FLAC -D_POSIX_SOURCE -D_BSD_EXTENSION -DPlan9 -c
+CFLAGS=-I. -I../libFLAC -I../libFLAC/FLAC -D_POSIX_SOURCE -D_BSD_EXTENSION -DPlan9
 
 %.$O: %.c
 	$CC $CFLAGS -c $stem.c
@@ -19,3 +19,6 @@
 
 install:V: $O.$TARGET
 	cp $O.$TARGET $BIN/$TARGET
+
+test:VQ:
+	# nothing
--- a/sys/src/cmd/audio/flacenc/mkfile
+++ b/sys/src/cmd/audio/flacenc/mkfile
@@ -19,3 +19,6 @@
 
 install:V: $O.$TARGET
 	cp $O.$TARGET $BIN/$TARGET
+
+test:VQ:
+	# nothing
--- a/sys/src/cmd/audio/mkfile
+++ b/sys/src/cmd/audio/mkfile
@@ -60,3 +60,9 @@
 		cd $i
 		mk clean
 	}
+
+test:V:
+	for (i in $DIRS) @{
+		cd $i
+		mk test
+	}
--- a/sys/src/cmd/audio/mp3enc/mkfile
+++ b/sys/src/cmd/audio/mp3enc/mkfile
@@ -59,7 +59,3 @@
 
 testcase.new.mp3: testcase.wav $O.out
 	$O.out --nores -h testcase.wav testcase.new.mp3
-
-test:V: testcase.new.mp3
-	cmp -l testcase.new.mp3 testcase.mp3 | wc -l
-	rm testcase.new.mp3
--- a/sys/src/cmd/audio/oggdec/mkfile
+++ b/sys/src/cmd/audio/oggdec/mkfile
@@ -19,3 +19,6 @@
 
 install:V: $O.$TARGET
 	cp $O.$TARGET $BIN/$TARGET
+
+test:VQ:
+	# nothing
--- a/sys/src/cmd/audio/oggenc/mkfile
+++ b/sys/src/cmd/audio/oggenc/mkfile
@@ -19,3 +19,6 @@
 
 install:V: $O.$TARGET
 	cp $O.$TARGET $BIN/$TARGET
+
+test:VQ:
+	# nothing
--- a/sys/src/cmd/bzip2/mkfile
+++ b/sys/src/cmd/bzip2/mkfile
@@ -18,20 +18,6 @@
 
 CFLAGS=$CFLAGS -p -DPLAN9 -Ilib
 
-test:V: $O.bzip2 $O.bunzip2
-	./$O.bzip2 -1  < sample1.ref > sample1.rb2
-	./$O.bzip2 -2  < sample2.ref > sample2.rb2
-	./$O.bzip2 -3  < sample3.ref > sample3.rb2
-	./$O.bunzip2  < sample1.bz2 > sample1.tst
-	./$O.bunzip2  < sample2.bz2 > sample2.tst
-	./$O.bunzip2 < sample3.bz2 > sample3.tst
-	cmp sample1.bz2 sample1.rb2 
-	cmp sample2.bz2 sample2.rb2
-	cmp sample3.bz2 sample3.rb2
-	cmp sample1.tst sample1.ref
-	cmp sample2.tst sample2.ref
-	cmp sample3.tst sample3.ref
-
 bzip2recover.$O: bzip2recover.c
 	pcc -D_POSIX_SOURCE -D_BSD_EXTENSION -c bzip2recover.c
 
--- a/sys/src/cmd/cwfs/mkfile
+++ b/sys/src/cmd/cwfs/mkfile
@@ -13,3 +13,6 @@
 	cd cwfs64x && mk $stem
 
 cleanall:V: clean emelie.clean fs64.clean cwfs64.clean cwfs64x.clean
+
+test:VQ:
+	# nothing
--- /dev/null
+++ b/sys/src/cmd/git/test/add.expected
@@ -1,0 +1,9 @@
+A a
+A b
+A dir/a
+A dir/b
+A extra/a
+A extra/b
+A more/a
+A more/b
+A more/c
--- /dev/null
+++ b/sys/src/cmd/git/test/add.rc
@@ -1,0 +1,34 @@
+#!/bin/rc
+
+. util.rc
+
+rm -fr scratch
+mkdir -p scratch/repo
+
+echo @@ abs paths @@
+@{
+	rfork ne
+	cd scratch/repo
+	pwd=`{pwd}
+	$G/init
+	mkdir dir
+	mkdir another
+	mkdir more
+	mkdir extra
+	touch a b c
+	touch dir/^(a b c)
+	touch another/^(a b c)
+	touch more/^(a b c)
+	touch extra/^(a b c)
+	$G/add a
+	$G/add $pwd/b
+	$G/add dir/a
+	$G/add $pwd/dir/b
+	$G/add more
+	@{cd more && $G/add ../extra/a}
+	@{cd more && $G/add $pwd/extra/b}
+	$G/$O.fs
+	$G/$O.walk > ../added
+}
+
+diff -c scratch/added add.expected >/dev/null || die wrong files
--- /dev/null
+++ b/sys/src/cmd/git/test/basic.rc
@@ -1,0 +1,67 @@
+#!/bin/rc
+
+. util.rc
+
+wrkdir=`{pwd}
+rm -fr scratch
+mkdir -p scratch/upstream
+
+echo @@ version1 @@
+@{
+	cd scratch/upstream
+	q $G/init
+	echo version1 > file.txt
+	q $G/add file.txt
+	q $G/commit -m version1 file.txt
+}
+
+@{
+	cd scratch
+	$G/clone $wrkdir/scratch/upstream downstream
+}
+
+diff -c scratch/upstream/file.txt scratch/downstream/file.txt || die mismatch
+
+echo @@ version2 @@
+@{
+	cd scratch/upstream
+	echo version2 > file.txt
+	q $G/commit -m version2 file.txt
+}
+@{
+	cd scratch/downstream
+	q $G/pull
+}
+q diff -c scratch/upstream/file.txt scratch/downstream/file.txt || die mismatch
+
+echo @@ version3 @@
+@{
+	cd scratch/upstream
+	echo version3 > file2.txt
+	$G/add file2.txt
+	q $G/commit -m version3 file2.txt
+}
+@{
+	cd scratch/downstream
+	q $G/pull
+}
+q diff -c scratch/upstream/file.txt scratch/downstream/file.txt || die mismatch
+q diff -c scratch/upstream/file2.txt scratch/downstream/file2.txt || die mismatch
+
+echo @@ version4 @@
+@{
+	cd scratch/upstream
+	echo version4 > file.txt
+	$G/rm file2.txt
+	rm file2.txt
+	q $G/commit -m version4 file.txt file2.txt
+}
+
+@{
+	cd scratch/downstream
+	q $G/pull
+}
+q diff -c scratch/upstream/file.txt scratch/downstream/file.txt || die mismatch
+! test -e scratch/upstream/file2.txt || die mismatch
+! test -e scratch/downstream/file2.txt || die mismatch
+
--- /dev/null
+++ b/sys/src/cmd/git/test/export.rc
@@ -1,0 +1,40 @@
+#!/bin/rc
+
+. util.rc
+
+rm -fr scratch
+mkdir -p scratch
+cd scratch
+
+# setup test repo
+@{
+	rfork ne
+	q $G/init a
+
+	cd a
+	echo hello > a
+	echo goodbye > b
+	q $G/add a b
+	q $G/commit -m v1 .
+	cd ..
+	pwd
+	q $G/clone `{pwd}^/a b
+}
+
+echo @@ export and apply @@
+@{
+	rfork ne
+	@{
+		cd b
+		echo hihi > b
+		q $G/commit -m export1 b
+		$G/export > ../export1.patch
+	}
+
+	@{
+		cd a
+		q $G/import ../export1.patch
+	}
+}
+
+~ `{cd a && $G/$O.query HEAD} `{cd b && $G/$O.query HEAD} || die 'mismatched export'
--- /dev/null
+++ b/sys/src/cmd/git/test/lca.rc
@@ -1,0 +1,68 @@
+#!/bin/rc
+
+. util.rc
+
+rm -fr scratch
+mkdir -p scratch
+cd scratch
+
+echo @@ test lca @@
+@{
+	q $G/init a
+
+	cd a
+
+	echo 'first' > f
+	q $G/add f
+	q $G/commit -m base f
+	r=`{$G/$O.query HEAD}
+
+	echo 0 > f
+	q $G/commit -m a.0 .
+	a=`{$G/$O.query HEAD}
+
+	for(i in `{seq 10}){
+		echo $i > f
+		q $G/commit -m a.$i .
+	}
+
+	q $G/branch -nb $r merge
+	echo x > f
+	q $G/commit -m b.0 .
+	b=`{$G/$O.query HEAD}
+
+	qq $G/merge front
+	q $G/commit -m merge .
+	m=`{$G/$O.query HEAD}
+
+~ `{$G/$O.query $a $m @} $a || die lca a-m
+~ `{$G/$O.query $a $b @} $r || die lca a-b
+~ `{$G/$O.query $a $r @} $r || die lca a-r
+}
+
+#       a
+#       ↓
+# b→c→d→e→f
+#
+# date order (oldest to newest): f d c b e a
+echo @@ test lca rebase @@
+@{
+	q $G/init b
+	cd b
+
+	touch f
+
+	fn commit {
+		$G/$O.save -n regress -e regress $* f
+	}
+
+	f=`{commit -m f -d 1}
+	e=`{commit -m e -d 5 -p $f}
+	d=`{commit -m d -d 2 -p $e}
+	c=`{commit -m c -d 3 -p $d}
+	b=`{commit -m b -d 4 -p $c}
+	a=`{commit -m a -d 6 -p $e}
+
+~ `{$G/$O.query $a $b @} $e || die lca a-b
+~ `{$G/$O.query $b $a @} $e || die lca b-a
+}
--- /dev/null
+++ b/sys/src/cmd/git/test/merge.rc
@@ -1,0 +1,92 @@
+#!/bin/rc
+
+. util.rc
+
+rm -fr scratch
+mkdir -p scratch
+cd scratch
+c='foo
+bar
+baz
+'
+
+# setup test repo
+@{
+	rfork ne
+	q $G/init a
+
+	cd a
+	echo hello > a
+	echo goodbye > b
+	echo -n $c > c
+	chmod +x a
+	q $G/add a b c
+	q $G/commit -m v1 .
+	cd ..
+	pwd
+	q $G/clone `{pwd}^/a b
+}
+
+echo @@ merge different files @@
+@{
+	rfork ne
+	@{
+		cd a
+		echo x > a
+		q $G/commit -m diverge1a a
+	}
+	@{
+		cd b
+		echo y > b
+		q $G/commit -m diverge1b b
+	}
+
+	@{
+		cd b
+		qq $G/pull
+		$G/merge origin/front || status=''
+		q $G/commit -m merged .
+	}
+}
+
+flag +x
+~ `{cat b/a} x || die merge 1.a
+~ `{cat b/b} y || die merge 1.b
+~ `''{cat b/c} $c || die merge 1.c
+test -x b/a || die merge preserve exec
+! test -x b/b || die merge preserve nonexec b
+! test -x b/c || die merge preserve nonexec c
+
+echo @@ merge concurent edits @@
+@{
+	rfork ne
+	@{
+		cd a
+		chmod -x a
+		chmod +x b
+		echo quux >>c
+		q $G/commit -m diverge2a a b c
+	}
+	@{
+		cd b
+		sed s/foo/FOO/ <c >c.new
+		mv c.new c
+		q $G/commit -m diverge2b c
+
+		qq $G/pull
+		qq $G/merge origin/front
+		q $G/commit -m merge c
+	}
+}
+
+c='FOO
+bar
+baz
+quux
+'
+~ `{cat b/a} x || die	# commit from a
+~ `{cat b/b} y || die # commit from b
+~ `''{cat b/c} $c || {diff -u b/c <{echo $c}; die merge 1.c}
+! test -x b/a || die merge remove exec
+test -x b/b || die merge add exec
+! test -x b/c || die merge preserve nonexec c
--- /dev/null
+++ b/sys/src/cmd/git/test/mkfile
@@ -1,0 +1,11 @@
+</$objtype/mkfile
+
+TEST=\
+	add\
+	basic\
+	export\
+	lca\
+	merge\
+	range
+
+</sys/src/cmd/mktest
--- /dev/null
+++ b/sys/src/cmd/git/test/range.rc
@@ -1,0 +1,75 @@
+#!/bin/rc
+
+. util.rc
+
+rm -fr scratch
+mkdir -p scratch
+cd scratch
+
+fn commit {
+	$G/$O.save -n regress -e regress $* f
+}
+
+# h→g→f
+# ↓   ↓
+# e→d→c→b→a
+echo @@ test range @@
+@{
+	q $G/init a
+	cd a
+	touch f
+
+	a=`{commit -m a}
+	b=`{commit -m b -p $a}
+	c=`{commit -m c -p $b}
+	d=`{commit -m d -p $c}
+	e=`{commit -m e -p $d}
+	f=`{commit -m f -p $c}
+	g=`{commit -m g -p $f}
+	h=`{commit -m h -p $e -p $g}
+	map='
+		s/^'$a'$/a/
+		s/^'$b'$/b/
+		s/^'$c'$/c/
+		s/^'$d'$/d/
+		s/^'$e'$/e/
+		s/^'$f'$/f/
+		s/^'$g'$/g/
+		s/^'$h'$/h/
+	'
+
+	diff -u <{echo d; echo e; echo g; echo h} \
+		<{$G/$O.query $f..$h | sed -e $map} || die range
+}
+
+#       b
+#      ↙ ↖
+# f←e←d   a
+#      ↖ ↙
+#       c
+echo @@ test range 2 @@
+@{
+	q $G/init b
+	cd b
+	touch f
+
+	a=`{commit -m a}
+	b=`{commit -m b -p $a}
+	c=`{commit -m c -p $a}
+	d=`{commit -m d -p $b -p $c}
+	e=`{commit -m e -p $d}
+	f=`{commit -m f -p $e}
+	map='
+		s/^'$a'$/a/
+		s/^'$b'$/b/
+		s/^'$c'$/c/
+		s/^'$d'$/d/
+		s/^'$e'$/e/
+		s/^'$f'$/f/
+	'
+
+	diff -u <{echo c; echo d; echo e; echo f} \
+		<{$G/$O.query $b..$f | sed -e $map} || die range
+	diff -u <{echo b; echo d; echo e; echo f} \
+		<{$G/$O.query $c..$f | sed -e $map} || die range
+}
--- /dev/null
+++ b/sys/src/cmd/git/test/util.rc
@@ -1,0 +1,17 @@
+fn q {
+	{$* > /tmp/out.$pid && rm /tmp/out.$pid} || cat /tmp/out
+}
+fn qq {
+	$* >/dev/null >[2]/dev/null
+}
+
+fn die {
+	st=$status
+	if(! ~ $st ''){
+		*=($* : $st)
+		echo $"*
+		exit $"*
+	}
+}
+
+G=`{cleanname `{pwd}^/..}
--- a/sys/src/cmd/ktrans/mkfile
+++ b/sys/src/cmd/ktrans/mkfile
@@ -11,6 +11,3 @@
 
 $O.test: test.$O
 	$LD $LDFLAGS -o $target $prereq
-
-test:V: $O.test $O.out
-	$O.test $O.out
--- a/sys/src/cmd/mkfile
+++ b/sys/src/cmd/mkfile
@@ -72,3 +72,6 @@
 
 nuke:V: cleancmds nuke.dirs
 	rm -f *.acid
+
+test:VQ: test.dirs
+	@{cd test && mk $MKFLAGS test}
--- a/sys/src/cmd/mklib
+++ b/sys/src/cmd/mklib
@@ -46,3 +46,9 @@
 
 update:V:
 	update $UPDATEFLAGS $UPDATE
+
+test:VQ: $LIB
+	if(test -d ./test)
+		cd test && mk $MKFLAGS test
+	if not
+		status=()
--- a/sys/src/cmd/mkmany
+++ b/sys/src/cmd/mkmany
@@ -79,3 +79,10 @@
 	cp $stem.man $MAN/$stem
 
 man:V:	$MANFILES
+
+test:VQ: $PROGS
+	if(test -d ./test)
+		cd test && mk $MKFLAGS test
+	if not
+		status=()
+
--- a/sys/src/cmd/mkone
+++ b/sys/src/cmd/mkone
@@ -60,3 +60,12 @@
 	cp $prereq $target
 
 man:V:    $MAN/$TARG
+
+test:QV: $O.out $TESTDEP
+	if(test -d ./test){
+		pwd
+		@{cd ./test && mk $MKFLAGS test}
+	}
+	if not
+		status=()
+
--- a/sys/src/cmd/mksyslib
+++ b/sys/src/cmd/mksyslib
@@ -41,3 +41,9 @@
 
 update:V:
 	update $UPDATEFLAGS $UPDATE
+
+test:VQ:
+	if(test -d ./test)
+		cd test && mk $MKFLAGS test
+	if not
+		status=()
--- /dev/null
+++ b/sys/src/cmd/mktest
@@ -1,0 +1,41 @@
+test:VQ:
+	echo $t
+	for(t in $TEST) @{
+		if(test -d $t)
+			@{cd $t && mk $MKFLAGS test}
+		if not
+			mk $t.test
+	}
+
+all:VQ:
+	# nothing
+
+%.test:V: $O.%
+	./$O.$stem
+
+%.test:V: %.rc
+	./$stem.rc
+
+%.$O:	$HFILES		# don't combine with following %.$O rules
+
+%.$O:	%.c
+	$CC $CFLAGS $stem.c
+
+%.$O:	%.s
+	$AS $AFLAGS $stem.s
+
+$O.%:V:	%.$O $OFILES $LIB
+	$LD $LDFLAGS -o $target $prereq
+
+# [$OS].??* avoids file names like 9.h
+nuke:V:
+	rm -f *.[$OS] y.tab.? lex.yy.c y.debug y.output [$OS].??* *.acid $TARG $CLEANFILES
+
+clean:V:
+	for(t in $TEST)
+		if(test -d $t)
+			@{cd $t && mk $MKFLAGS clean}
+	rm -f *.[$OS] *.a[$OS] y.tab.? lex.yy.c y.debug y.output [$OS].??* $TARG $CLEANFILES
+
+%.clean:V:
+	rm -f $stem.[$OS] [$OS].$stem $stem.acid $stem
--- a/sys/src/cmd/nusb/mkfile
+++ b/sys/src/cmd/nusb/mkfile
@@ -36,3 +36,8 @@
 		echo update $i
 		cd $i && mk 'UPDATEFLAGS='$"UPDATEFLAGS update
 	}
+
+test:
+	for(d in $DIRS) @{
+		cd $d && mk $MKFLAGS test
+	}
--- a/sys/src/cmd/postscript/mkfile
+++ b/sys/src/cmd/postscript/mkfile
@@ -48,3 +48,6 @@
 		cd $i;
 		mk $target
 	}
+
+test:VQ:
+	# nothing
--- a/sys/src/cmd/spell/mkfile
+++ b/sys/src/cmd/spell/mkfile
@@ -39,8 +39,3 @@
 
 clean:V:
 	rm -f *.[$OS] [$OS].out [$OS].pcode y.tab.? y.debug y.output $TARG
-
-test:V:	$O.out brspell
-	time ./$O.out -b -f brspell </dev/null >y
-	time ./$O.out -b -f brspell <x >z
-	cmp y z
--- /dev/null
+++ b/sys/src/cmd/test/date.rc
@@ -1,0 +1,91 @@
+#!/bin/rc
+
+rfork en
+cat /adm/timezone/GMT > /env/timezone
+
+nl='
+'
+
+fn check {
+	r=`$nl{../$O.seconds $1}
+	if(! ~ $r $2){
+		echo "$"r"
+		echo "$"2"
+		echo $status
+		>[1=2] echo fail: $1: got $r expected $2
+		exit 'fail'
+	}
+}
+
+# examples from manpage, and shuffles
+rfork ne
+check '23 may 2011' 			1306108800
+check 'may 23 2011' 			1306108800
+check 'may 2011 23' 			1306108800
+check '23 2011 may' 			1306108800
+check '2011 may 23'			1306108800
+check '2011 23 may'			1306108800
+
+# now with timezones
+check '23 may 2011 edt'			1306123200
+check '23 may 2011 gmt'			1306108800
+
+# If the tz is present, the results should stay
+# the same if we change zones.
+@{
+	rfork en
+	cat /adm/timezone/US_Pacific >/env/timezone
+	check '23 may 2011 edt'			1306123200
+	check '23 may 2011 gmt'			1306108800
+}
+
+# now with all variations on times.
+check 'may 23 2011 0'			1306108800
+check 'may 23 2011 0:1'			1306108860
+check 'may 23 2011 0:1:2'		1306108862
+
+# now with times and timezones
+check '23 may 2011 edt'			1306123200
+check '23 may 2011 gmt'			1306108800
+
+# formats from ../$O.date(1)
+check 'Sun, 14 Jun 2020 22:08:48 -0700'	1592197728
+check 'Sun, 14 Jun 2020 -0700'		1592118000
+check '2020-06-14'			1592092800
+check '2020-06-14T22:14:17-07:00'	1592198057
+
+# colloquial american format (eww)
+check '06/14/2020'			1592092800
+check '06/01/2020'			1590969600
+
+
+# Arizona has no DST
+@{
+	rfork en
+	cat /adm/timezone/US_Arizona >/env/timezone
+	check 'Mon, Jun 21 17:38:02 MST 2020' 1592786282
+}
+
+# CET is a timezone with no hard-coded
+# timezone name -- it should round trip
+@{
+	rfork en
+	cat /adm/timezone/CET >/env/timezone
+	tm=1592782682
+	ds=`{../$O.date $tm}
+	r=`{../$O.seconds $"ds}
+	if(! ~ $tm $r)
+		>[1=2] echo fail: CET: got $r expected $tm
+}
+
+# The other EST should also round trip.
+@{
+	rfork en
+	cat /adm/timezone/Australia_ACT >/env/timezone
+	tm=1592782682
+	ds=`$nl{../$O.date $tm}
+	r=`$nl{../$O.seconds $ds}
+	if(! ~ $tm $r)
+		>[1=2] echo fail: Austraila_ACT: got $r expected $tm
+}
+exit ''
--- /dev/null
+++ b/sys/src/cmd/test/mkfile
@@ -1,0 +1,9 @@
+</$objtype/mkfile
+
+TEST=\
+	date\
+	patch\
+	ramfs\
+	test\
+
+</sys/src/cmd/mktest
--- /dev/null
+++ b/sys/src/cmd/test/patch/basic.expected
@@ -1,0 +1,100 @@
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
+98
+99
+100
--- /dev/null
+++ b/sys/src/cmd/test/patch/basic.in
@@ -1,0 +1,93 @@
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+13
+12
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
+98
--- /dev/null
+++ b/sys/src/cmd/test/patch/basic.patch
@@ -1,0 +1,42 @@
+--- basic.in
++++ basic.out
+@@ -1,3 +1,5 @@
++1
++2
+ 3
+ 4
+ 5
+@@ -10,8 +12,8 @@
+ 12
+ 13
+ 14
+-13
+-12
++15
++16
+ 17
+ 18
+ 19
+@@ -35,6 +37,8 @@
+ 37
+ 38
+ 39
++40
++41
+ 42
+ 43
+ 44
+@@ -80,6 +84,7 @@
+ 84
+ 85
+ 86
++87
+ 88
+ 89
+ 90
+@@ -91,3 +96,5 @@
+ 96
+ 97
+ 98
++99
++100
--- /dev/null
+++ b/sys/src/cmd/test/patch/create.expected
@@ -1,0 +1,12 @@
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
--- /dev/null
+++ b/sys/src/cmd/test/patch/create.patch
@@ -1,0 +1,15 @@
+--- /dev/null
++++ create.out
+@@ -1,0 +1,12 @@
++1
++2
++3
++4
++5
++6
++7
++8
++9
++10
++11
++12
--- /dev/null
+++ b/sys/src/cmd/test/patch/delete.expected
@@ -1,0 +1,10 @@
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
--- /dev/null
+++ b/sys/src/cmd/test/patch/delete.patch
@@ -1,0 +1,15 @@
+--- delete.out
++++ /dev/null
+@@ -1,12 +1,0 @@
+-1
+-2
+-3
+-4
+-5
+-6
+-7
+-8
+-9
+-10
+-11
+-12
--- /dev/null
+++ b/sys/src/cmd/test/patch/header.expected
@@ -1,0 +1,100 @@
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+87
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
+98
+99
+100
--- /dev/null
+++ b/sys/src/cmd/test/patch/header.in
@@ -1,0 +1,93 @@
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+13
+12
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
+98
--- /dev/null
+++ b/sys/src/cmd/test/patch/header.patch
@@ -1,0 +1,54 @@
+diff should handle headers just fine, so
+this file contains a few lines of header
+and footer, with a couple of false starts,
+consisting of lines starting with
+--- some words
+but no immediately following 
++++ words
+lines
+
+--- header.in
++++ header.out
+@@ -1,3 +1,5 @@
++1
++2
+ 3
+ 4
+ 5
+@@ -10,8 +12,8 @@
+ 12
+ 13
+ 14
+-13
+-12
++15
++16
+ 17
+ 18
+ 19
+@@ -35,6 +37,8 @@
+ 37
+ 38
+ 39
++40
++41
+ 42
+ 43
+ 44
+@@ -80,6 +84,7 @@
+ 84
+ 85
+ 86
++87
+ 88
+ 89
+ 90
+@@ -91,3 +96,5 @@
+ 96
+ 97
+ 98
++99
++100
+-- signature footer: should not get picked up
+and here is the footer that should
+also be ignored.
--- /dev/null
+++ b/sys/src/cmd/test/patch/mkfile
@@ -1,0 +1,5 @@
+</$objtype/mkfile
+
+TEST=patch
+
+</sys/src/cmd/mktest
--- /dev/null
+++ b/sys/src/cmd/test/patch/multifile.patch
@@ -1,0 +1,89 @@
+--- multifile2.in
++++ multifile2.out
+@@ -1,21 +1,19 @@
+-77777
+ 77778
+ 77779
+ 77780
+ 77781
+ 77782
+-77783
+-77784
+-77785
+-77786
+-77787
+-77788
+ 77789
++77788
++77787
++77786
++77785
++77784
++77783
+ 77790
+ 77791
+ 77792
+ 77793
+ 77794
+-77795
+ 77796
+ 77797
+
+of course, each patch can have its own comment
+lines mixed in, so we should handle these.
+--- multifile1.in
++++ multifile1.out
+@@ -14,16 +14,6 @@
+ 12
+ 17
+ 18
+-19
+-20
+-21
+-22
+-23
+-24
+-25
+-26
+-27
+-28
+ 29
+ 30
+ 31
+@@ -32,20 +22,6 @@
+ 34
+ 35
+ 36
+-37
+-38
+-39
+-42
+-43
+-44
+-45
+-46
+-47
+-48
+-49
+-50
+-51
+-52
+ 53
+ 54
+ 55
+@@ -73,11 +49,11 @@
+ 77
+ 78
+ 79
+-80
+-81
+-82
+-83
+ 84
++83
++82
++81
++80
+ 85
+ 86
+ 88
--- /dev/null
+++ b/sys/src/cmd/test/patch/multifile1.expected
@@ -1,0 +1,69 @@
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+13
+12
+17
+18
+29
+30
+31
+32
+33
+34
+35
+36
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+84
+83
+82
+81
+80
+85
+86
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
+98
--- /dev/null
+++ b/sys/src/cmd/test/patch/multifile1.in
@@ -1,0 +1,93 @@
+3
+4
+5
+6
+7
+8
+9
+10
+11
+12
+13
+14
+13
+12
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
+52
+53
+54
+55
+56
+57
+58
+59
+60
+61
+62
+63
+64
+65
+66
+67
+68
+69
+70
+71
+72
+73
+74
+75
+76
+77
+78
+79
+80
+81
+82
+83
+84
+85
+86
+88
+89
+90
+91
+92
+93
+94
+95
+96
+97
+98
--- /dev/null
+++ b/sys/src/cmd/test/patch/multifile2.expected
@@ -1,0 +1,19 @@
+77778
+77779
+77780
+77781
+77782
+77789
+77788
+77787
+77786
+77785
+77784
+77783
+77790
+77791
+77792
+77793
+77794
+77796
+77797
--- /dev/null
+++ b/sys/src/cmd/test/patch/multifile2.in
@@ -1,0 +1,21 @@
+77777
+77778
+77779
+77780
+77781
+77782
+77783
+77784
+77785
+77786
+77787
+77788
+77789
+77790
+77791
+77792
+77793
+77794
+77795
+77796
+77797
--- /dev/null
+++ b/sys/src/cmd/test/patch/nop.expected
@@ -1,0 +1,10 @@
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
--- /dev/null
+++ b/sys/src/cmd/test/patch/patch.rc
@@ -1,0 +1,39 @@
+#!/bin/rc -e
+
+fn check{
+	if(! cmp $1 $2){
+		>[2=1] echo fail: $1 $2
+		>[2=1] diff -u $1 $2
+		exit mismatch
+	}
+	status=()
+}
+
+fn checkpatch{
+	rm -f $1.out
+	../../$O.patch $1.patch
+	check $1.out $1.expected
+}
+
+checkpatch basic
+checkpatch header
+checkpatch create
+
+seq 12 > delete.out
+../../$O.patch delete.patch
+test ! -f delete.out
+
+rm -f multifile^(1 2)^.out
+chmod 640 multifile1.in
+chmod 400 multifile2.in
+../../$O.patch multifile.patch
+check multifile1.out multifile1.expected
+check multifile2.out multifile2.expected
+
+if(! ~ `{walk -ex multifile1.out} --rw-r-----)
+	exit misperm1
+if(! ~ `{walk -ex multifile2.out} --rw-------)
+	exit misperm2
+
+
+status=()
--- /dev/null
+++ b/sys/src/cmd/test/ramfs.rc
@@ -1,0 +1,40 @@
+#!/bin/rc
+rfork e
+
+attach='
+Tversion 8192 9P2000
+Rversion 8192 9P2000
+Tattach 1 -1 '^$user^' ''''
+Rattach {0,0,d}
+Twalk 1 2
+Rwalk
+'
+
+fn assert{
+	aux/9pcon -ac '../6.ramfs -i' >/dev/null || exit $status
+}
+
+
+# attach
+assert <<.
+$attach
+.
+
+# create/write/read
+assert <<.
+$attach
+Tcreate 2 testfile 777 2
+Rcreate {1,0,0} 0
+Twrite 2 0 hello
+Rwrite 5
+Tclunk 2
+Rclunk
+Twalk 1 2 testfile
+Rwalk {1,2,0}
+Topen 2 0
+Ropen {1,2,0} 0
+Tread 2 0 5
+Rread hello
+Tclunk 2
+Rclunk
+.
--- /dev/null
+++ b/sys/src/cmd/test/test.rc
@@ -1,0 +1,135 @@
+#!/bin/rc
+
+rfork e
+
+ERROR=0
+FAILED=0
+
+fn t{
+	# $1 -> exit code
+	# $2 -> $test expression
+
+	expect=$1
+	shift
+
+	# check for syntax errors
+	syntax=`{../$O.test $* >[2=1]}
+	if(~ $"syntax ''){
+		switch($expect){
+		case 0
+			if(! ../$O.test $*) failed $expect $*
+		case 1
+			if(../$O.test $*) failed $expect $*
+		}
+	}
+	if not
+		error
+}
+
+fn error{
+	echo
+	echo '	'^$"syntax
+	ERROR=`{echo $ERROR 1 + p | dc}
+}
+
+fn failed{
+	echo -n $1^': test' $*(2-)^'^:	failed'
+	echo '	failed'
+	FAILED=`{echo $FAILED 1 + p | dc}
+}
+
+
+t 0 b '=' b
+t 1 b '!=' b
+t 0 '(' b '=' b ')'
+t 1 ! '(' b '=' b ')'
+t 1 ! -f /bin/rc
+
+t 0 -h '=' -h
+t 0 -o '=' -o
+#t 1 -f '=' h
+t 1 -h '=' f
+t 1 -o '=' f
+t 1 f '=' -o
+
+t 0 '(' -h '=' -h ')'
+t 1 '(' a '=' -h ')'
+#t 1 '(' -f '=' h ')'
+t 0 -h '=' -h -o a
+t 0 '(' -h '=' -h ')' -o 1
+t 0 -h '=' -h -o -h '=' -h
+t 0 '(' -h '=' -h ')' -o '(' -h '=' -h ')'
+t 0 roedelheim '=' roedelheim
+t 1 potsdam '=' berlin-dahlem
+
+t 0 -d /
+t 0 -d / -a a '!=' b
+t 1 -z -z
+t 0 -n -n
+
+t 0 0
+t 0 '(' 0 ')'
+t 0 '-E'
+t 0 -X -a -X
+t 0 -XXX
+t 0 '(' -E ')'
+t 0 true -o X
+t 0 true -o -X
+t 0 '(' '(' '(' a '=' a ')' -o 1 ')' -a 1 ')'
+#t 1 -h /
+t 0 -r /
+t 1 -w /dev/zero
+t 0 -w /dev/null
+t 1 -x /dev/null
+t 0 -x /bin/rc
+#t 0 -c /dev/null
+#t 0 -b /dev/fd0a -o -b /dev/rfd0a -o true
+t 0 -f /adm/users
+t 0 -s /adm/users
+
+t 1 ! '(' 700 -le 1000 -a -n 1 -a 20 '=' 20 ')'
+t 0 100 -eq 100
+t 0 100 -lt 200
+t 1 1000 -lt 200
+t 0 1000 -gt 200
+t 0 1000 -ge 200
+t 0 1000 -ge 1000
+
+t 1 2 -ne 2
+t 0 0 -eq 0
+t 1 -5 -eq 5
+t 0 '(' 0 -eq 0 ')'
+t 1 1 -eq 0 -o a '=' a -a 1 -eq 0 -o a '=' aa
+#t 0 '" +123 " -eq 123'
+#t 1 '"-123  " -gt " -1"'
+t 0 123 -gt -123
+t 0 -0 -eq +0
+t 1 +0 -gt 0
+t 0 0 -eq 0
+t 0 0000 -eq -0
+t 0 -1 -gt -2
+t 1 1 -gt 2
+t 1 4294967296 -eq 0
+t 0 12345678901234567890 -eq +12345678901234567890
+
+t 1 '' -o ''
+t 1 '' -a ''
+t 1 a -a ''
+t 0 a -a ! ''
+t 1 ''
+t 0 ! ''
+
+t 0 1 -eq 1
+t 1 ! 1 -eq 1
+t 0 ! ! 1 -eq 1
+t 1 ! ! ! 1 -eq 1
+t 1 ! '(' XXX -o XXX ')'
+t 0 ! '(' ! '(' XXX -o XXX ')' ')'
+
+if(! ~ $ERROR 0 || ! ~ $FAILED 0){
+	echo
+	echo 'Syntax errors:' $ERROR 'Failed:' $FAILED
+}
+if(! ~ $ERROR 0 || ! ~ $FAILED 0)
+	exit 'failures'
+exit ''
--- /dev/null
+++ b/sys/src/cmd/test/zones.rc
@@ -1,0 +1,19 @@
+#!/bin/rc
+
+rfork en
+
+msg=()
+for(f in /adm/timezone/*){
+	if(! ~ $f /adm/timezone/README){
+		cat $f >/env/timezone
+		tm=`{../6.date -f'WW, DD MMM YYYY hh:mm:ss Z'}
+		x=`{../6.date -n}
+		y=`{../6.seconds $"tm}
+		if(! ~ $x $y){
+			echo $f $tm $x $y are not equal
+			msg=($msg $f)
+		}
+	}
+}
+
+exit $"msg
--- a/sys/src/cmd/upas/binscripts/mkfile
+++ b/sys/src/cmd/upas/binscripts/mkfile
@@ -27,4 +27,7 @@
 $BIN/%: %.rc
 	cp $stem.rc $BIN/$stem
 
+test:VQ:
+	# nothing
+
 <../mkupas
--- a/sys/src/cmd/upas/mkfile
+++ b/sys/src/cmd/upas/mkfile
@@ -43,3 +43,14 @@
 		cd $i
 		mk safeinstallall
 	}
+
+test:V:
+	for (i in $LIBS) @{
+		cd $i
+		mk test
+	}
+	for (i in $PROGS) @{
+		cd $i
+		mk test
+	}
+
--- a/sys/src/cmd/vl/mkfile
+++ b/sys/src/cmd/vl/mkfile
@@ -28,9 +28,3 @@
 
 x:V:	$O.out
 	$O.out -la -o/dev/null x.v
-
-test:V:	$O.out
-	rm -f xxx
-	mv $O.out xxx
-	./xxx $OFILES
-	cmp $O.out xxx
--- a/sys/src/libc/mkfile
+++ b/sys/src/libc/mkfile
@@ -49,3 +49,6 @@
 		mk $MKFLAGS install
 	}
 	rm -f */*.[012456789kqvxz]
+
+test:V:
+	cd test && mk $MKFLAGS test
--- /dev/null
+++ b/sys/src/libc/test/date.c
@@ -1,0 +1,377 @@
+#include <u.h>
+#include <libc.h>
+
+int failed;
+
+/*
+ * For debugging
+ */
+void
+printtm(Tm *tm)
+{
+	fprint(2, "sec=%d min=%d hour=%d mday=%d mon=%d"
+		" year=%d day=%d yday=%d zone=%s tzoff=%d\n",
+		tm->sec, /* seconds (range 0..59) */
+		tm->min, /* minutes (0..59) */
+		tm->hour,     /* hours (0..23) */
+		tm->mday,     /* day of the month (1..31) */
+		tm->mon, /* month of the year (0..11) */
+		tm->year,     /* year A.D. - 1900 */
+		tm->wday,     /* day of week (0..6, Sunday = 0) */
+		tm->yday,     /* day of year (0..365) */
+		tm->zone,   /* time zone name */
+		tm->tzoff);    /* time   zone delta from GMT */
+}
+
+void
+fail(char *fmt, ...)
+{
+	va_list ap;
+
+	va_start(ap, fmt);
+	fprint(2, "failed: ");
+	vfprint(2, fmt, ap);
+	va_end(ap);
+	failed++;
+}
+
+void
+testtm(char *s, int year, int mon, int mday, int hour, int min, int sec, int nsec, Tm *tm){
+	if(tm->year != year-1900) fail("%s wrong year expected=%d actual=%d\n", s, year, tm->year);
+	if(tm->mon != mon)	fail("%s wrong month expected=%d actual=%d\n", s, mon, tm->mon);
+	if(tm->mday != mday)	fail("%s wrong mday expected=%d actual=%d\n", s, mday, tm->mday);
+	if(tm->hour != hour)	fail("%s wrong hour expected=%d actual=%d\n", s, hour, tm->hour);
+	if(tm->min != min)	fail("%s wrong min expected=%d actual=%d\n", s, min, tm->min);
+	if(tm->sec != sec)	fail("%s wrong sec expected=%d actual=%d\n", s, sec, tm->sec);
+	if(tm->nsec != nsec)	fail("%s wrong nsec expected=%d actual=%d\n", s, nsec, tm->nsec);
+}
+
+void
+rangechk(vlong sec, char *s, vlong val, vlong lo, vlong hi)
+{
+	if(val < lo || val > hi){
+		fprint(2, "%lld: %s: expected %lld <= %lld <= %lld", sec, s, lo, val, hi);
+		failed++;
+	}
+}
+
+void
+main(int, char **)
+{
+	Tm tm, tt;
+	Tzone *gmt, *us_arizona, *us_eastern, *us_central;
+	Tm here, there;
+	Tzone *zl, *zp;
+	char buf[128], buf1[128];
+	int i, h;
+	long s;
+
+	tmfmtinstall();
+	if((gmt = tzload("GMT")) == nil)
+		sysfatal("nil gmt: %r\n");
+	if((us_arizona = tzload("US_Arizona")) == nil)
+		sysfatal("nil us_arizona: %r\n");
+	if((us_eastern = tzload("US_Eastern")) == nil)
+		sysfatal("nil us_eastern: %r\n");
+	if((us_central = tzload("US_Central")) == nil)
+		sysfatal("get zone: %r\n");
+
+	if((zl = tzload("local")) == nil)
+	     sysfatal("load zone: %r\n");
+	if((zp = tzload("US_Pacific")) == nil)
+	     sysfatal("load zone: %r\n");
+	if(tmnow(&here, zl) == nil)
+	     sysfatal("get time: %r\n");
+	if(tmtime(&there, tmnorm(&here), zp) == nil)
+	     sysfatal("shift time: %r\n");
+
+	for(i = 0; i < 1600826575; i += 3613){
+		tmtime(&tm, i, nil);
+		rangechk(i, "nsec", tm.nsec, 0, 1e9);
+		rangechk(i, "sec", tm.sec, 0, 59);
+		rangechk(i, "min", tm.min, 0, 59);
+		rangechk(i, "hour", tm.hour, 0, 23);
+		rangechk(i, "mday", tm.mday, 1, 31);
+		rangechk(i, "mon", tm.mon, 0, 11);
+		rangechk(i, "year", tm.year, 69 ,120);
+		rangechk(i, "wday", tm.wday, 0, 6);
+		rangechk(i, "yday", tm.yday, 0, 365);
+	}
+
+	tmtime(&tm, 1586574870, gmt);
+	testtm("tmtime-gmt", 2020, 3, 11, 3, 14, 30, 0, &tm);
+	tmtime(&tm, 1586574870, us_arizona);
+	testtm("tmtime-az", 2020, 3, 10, 20, 14, 30, 0, &tm);
+
+	tmtime(&tm, 0, gmt);
+	testtm("tmtime-0-gmt", 1970, 0, 1, 0, 0, 0, 0, &tm);
+	tmnorm(&tm);
+	testtm("tmnorm-0-gmt", 1970, 0, 1, 0, 0, 0, 0, &tm);
+
+	tmtime(&tm, 84061, gmt);
+	testtm("tmtime-near0-gmt", 1970, 0, 1, 23, 21, 1, 0, &tm);
+	tmnorm(&tm);
+	testtm("tmnorm-near0-gmt", 1970, 0, 1, 23, 21, 1, 0, &tm);
+
+	tmtime(&tm, 1586574870, us_arizona);
+	testtm("tmtime-recent-az", 2020, 3, 10, 20, 14, 30, 0, &tm);
+	tmnorm(&tm);
+	testtm("tmnorm-recent-az", 2020, 3, 10, 20, 14, 30, 0, &tm);
+
+	tmtime(&tm, 1586574870, us_eastern);
+	testtm("tmtime-recent-est", 2020, 3, 10, 23, 14, 30, 0, &tm);
+	tmnorm(&tm);
+	testtm("tmnorm-recent-est", 2020, 3, 10, 23, 14, 30, 0, &tm);
+
+	if(tmparse(&tm, "hhmm", "1600", gmt, nil) == nil)
+		sysfatal("failed parse: %r\n");
+	testtm("hhmm", 1970, 0, 1, 16, 0, 0, 0, &tm);
+
+	if(tmparse(&tm, "YYYY-MM-DD hh:mm:ss Z", "1969-12-31 16:00:00 -0800", nil, nil) == nil)
+		fail("parse failed: %r\n");
+	if(tmnorm(&tm) != 0)
+		fail("wrong result: %lld != 0\n", tmnorm(&tm));
+
+	if(tmparse(&tm, "YYYY MM DD", "1990,01,03", nil, nil) == nil)
+		fail("comma parse failed");
+	if(tmnorm(&tm) != 631324800)
+		fail("wrong result");
+	if(tmparse(&tm, "YYYY MM DD", "1990 ,\t01,03", nil, nil) == nil)
+		fail("comma parse failed");
+	if(tmnorm(&tm) != 631324800)
+		fail("wrong result");
+
+	if(tmparse(&tm, "YYYY MM DD hh:mm:ss", "1969 12 31 16:00:00", gmt, nil) == nil)
+		sysfatal("failed parse: %r\n");
+	testtm("parse-notz1", 1969, 11, 31, 16, 0, 0, 0, &tm);
+
+	if(tmparse(&tm, "YYYY MM DD hh:mm:ss", "1970 01 01 04:00:00", gmt, nil) == nil)
+	fail("failed parse: %r\n");
+	testtm("parse-notz2", 1970, 0, 1, 4, 0, 0, 0, &tm);
+
+	if(tmparse(&tm, "YYYY MM DD", "1970 01 01", gmt, nil) == nil)
+	fail("failed parse: %r\n");
+	testtm("parse-notz3", 1970, 0, 1, 0, 0, 0, 0, &tm);
+
+	if(tmparse(&tm, "YYYY MMMM DD WWW hh:mm:ss", "2020 April 10 Friday 16:04:00", gmt, nil) == nil)
+		sysfatal("failed parse: %r\n");
+	testtm("parse-notz4", 2020, 3, 10, 16, 4, 0, 0, &tm);
+
+	if(tmparse(&tm, "MM DD hh:mm:ss", "12 31 16:00:00", gmt, nil) == nil)
+		sysfatal("failed parse: %r\n");
+	testtm("parse-notz5", 1970, 11, 31, 16, 0, 0, 0, &tm);
+
+	if(tmparse(&tm, "MM DD h:mm:ss", "12 31 4:00:00", gmt, nil) == nil)
+		sysfatal("failed parse: %r\n");
+	testtm("parse-mmdd-hms", 1970, 11, 31, 4, 0, 0, 0, &tm);
+	if(tm.tzoff != 0) print("%d wrong tzoff expected=%d actual=%d\n", 6, 0, tm.tzoff);
+
+	if(tmparse(&tm, "YYYY MM DD hh:mm:ss", "2020 04 10 23:14:30", us_eastern, nil) == nil)
+		fail("failed parse: %r\n");
+	testtm("parse-est", 2020, 3, 10, 23, 14, 30, 0, &tm);
+	tmtime(&tm, tmnorm(&tm), nil);
+
+	if(tmparse(&tm, "YYYY MM DD hh:mm:ss", "2020 04 10 20:14:30", us_arizona, nil) == nil)
+		fail("failed parse: %r\n");
+	testtm("parse-az", 2020, 3, 10, 20, 14, 30, 0, &tm);
+
+	if(tmparse(&tm, "YYYY MM DD hh:mm:ss ZZZ", "2020 04 10 20:14:30 EST", us_arizona, nil) == nil)
+		fail("failed parse: %r\n");
+	testtm("parse-tz1", 2020, 3, 10, 17, 14, 30, 0, &tm);
+
+	if(tmparse(&tm, "YYYY MM DD hh:mm:ss Z", "2020 04 10 20:14:30 -0400", nil, nil) == nil)
+		fail("failed parse: %r\n");
+	testtm("parse-tz2", 2020, 3, 10, 20, 14, 30, 0, &tm);
+	snprint(buf, sizeof(buf), "%τ", tmfmt(&tm, "YYYY MM DD hh:mm:ss Z"));
+	if(strcmp(buf, "2020 04 10 20:14:30 -0400") != 0)
+		fail("failed format: %s != 2020 04 10 20:14:30 -0400", buf);
+
+	if(tmparse(&tm, "YYYY MM DD hh:mm:ss.ttt", "2020 04 10 20:14:30.207", nil, nil) == nil)
+		fail("failed parse: %r\n");
+	testtm("parse-milliseconds", 2020, 3, 10, 20, 14, 30, 207*1000*1000, &tm);
+
+	if(tmparse(&tm, "YYYY MM DD hh:mm:ss.nnn", "2020 04 10 20:14:30.207", nil, nil) == nil)
+		fail("failed parse: %r\n");
+	testtm("parse-nanoseconds", 2020, 3, 10, 20, 14, 30, 207, &tm);
+
+	if(tmparse(&tm, "YYYY MM DD hh:mm:ss.nnn", "2020 04 10 20:14:30.999", nil, nil) == nil)
+		fail("failed parse: %r\n");
+	testtm("parse-nnn2", 2020, 3, 10, 20, 14, 30, 999, &tm);
+	snprint(buf, sizeof(buf), "%τ", tmfmt(&tm, "YYYY MM DD hh:mm:ss.nnn"));
+	if(strcmp(buf, "2020 04 10 20:14:30.999") != 0)
+		fail("failed format: %s != 2020 04 10 20:14:30.999", buf);
+
+	if(tmparse(&tm, "YYYY MM DD hh:mm:ss.ttt", "2020 04 10 20:14:30.999", nil, nil) == nil)
+		fail("failed parse: %r\n");
+	testtm("parse-nnn2", 2020, 3, 10, 20, 14, 30, 999*1000*1000, &tm);
+	snprint(buf, sizeof(buf), "%τ", tmfmt(&tm, "YYYY MM DD hh:mm:ss.ttt"));
+	if(strcmp(buf, "2020 04 10 20:14:30.999") != 0)
+		fail("failed format: %s != 2020 04 10 20:14:30.999", buf);
+
+	/* edge case: leap year feb 29 */
+	if(tmparse(&tm, "YYYY MM DD hh:mm:ss Z", "2020 02 29 20:14:30 -0400", nil, nil) == nil)
+		fail("failed leap year feb 29: %r\n");
+	testtm("parse-leapfeb", 2020, 1, 29, 20, 14, 30, 0, &tm);
+	if(tmparse(&tm, "YYYY MM DD hh:mm:ss Z", "2021 02 29 20:14:30 -0400", nil, nil) != nil)
+
+		fail("incorrectly accepted non-leap year feb 29\n");
+	/* stray spaces */
+	if(tmparse(&tm, "YYYY MM DD hh:mm:ss Z", "   2020 02 29 20:14:30 -0400   ", nil, nil) == nil)
+		fail("failed leap year feb 29: %r\n");
+
+	/* lots of round trips: Jan 1960 => Jun 2020, in almost-11 day increments */
+	for(i = -315619200; i < 1592179806; i += 23*3600 + 1732){
+		if(tmtime(&tm, i, nil) == nil)
+			fail("load time %d\n", i);
+		if(tmnorm(&tm) != i)
+			fail("wrong load time: %d\n", i);
+		if(snprint(buf, sizeof(buf), "%τ", tmfmt(&tm, "WW MMM DD hh:mm:ss Z YYYY")) == -1)
+			fail("format: %r\n");
+		if(tmparse(&tt, "WW MMM DD hh:mm:ss Z YYYY", buf, nil, nil) == nil)
+			fail("parse: %r\n");
+		if(tmnorm(&tm) != tmnorm(&tt))
+			fail("parse: wrong time (%lld != %lld)\n", tmnorm(&tm), tmnorm(&tt));
+	}
+
+	/* lots of round trips: Jan 1960 => Jun 2020, in almost-dailyincrements, now with timezone */
+	for(i = -315619200; i < 1592179806; i += 23*3600 + 1732){
+		if(tmtime(&tm, i, us_eastern) == nil)
+			fail("load time %d\n", i);
+		if(tmnorm(&tm) != i)
+			fail("wrong load time: %d\n", i);
+		if(snprint(buf, sizeof(buf), "%τ", tmfmt(&tm, "WW MMM DD hh:mm:ss Z YYYY")) == -1)
+			fail("format: %r\n");
+		if(tmparse(&tt, "WW MMM DD hh:mm:ss Z YYYY", buf, us_arizona, nil) == nil)
+			fail("parse: %r\n");
+		if(tmnorm(&tm) != tmnorm(&tt))
+			fail("parse: wrong time (%lld != %lld)\n", tmnorm(&tm), tmnorm(&tt));
+		tm = tt;
+		tmnorm(&tm);
+		testtm("norm-rt", tt.year + 1900, tt.mon, tt.mday, tt.hour, tt.min, tt.sec, 0, &tm);
+	}
+
+	if(tmtime(&tm, -624623143, nil) == nil)
+		fail("tmtime: %r");
+	if(snprint(buf, sizeof(buf), "%τ", tmfmt(&tm, "WW, DD MMM YYYY hh:mm:ss Z")) == -1)
+		fail("format: %r");
+	if(strcmp(buf, "Fri, 17 Mar 1950 13:34:17 +0000") != 0)
+		fail("wrong output: %s\n", buf);
+	if(tmtime(&tm, -624623143, us_eastern) == nil)
+		fail("tmtime: %r");
+	if(snprint(buf, sizeof(buf), "%τ", tmfmt(&tm, "WW, DD MMM YYYY hh:mm:ss Z")) == -1)
+		fail("format: %r");
+	if(strcmp(buf, "Fri, 17 Mar 1950 08:34:17 -0500") != 0)
+		fail("wrong output: %s\n", buf);
+
+	/* AM and PM parsing */
+	for(i = 0; i < 24; i++){
+		h = i % 12;
+		if(h == 0)
+			h = 12;
+		snprint(buf, sizeof(buf), "2021 02 01 %d:14:30 -0400", i);
+		snprint(buf1, sizeof(buf1), "2021 02 01 %d:14:30 -0400 %s", h, (i < 12) ? "AM" : "PM");
+		if(tmparse(&tm, "YYYY MM DD hh:mm:ss Z", buf, nil, nil) == nil)
+			fail("parse: %r\n");
+		if(tmparse(&tt, "YYYY MM DD hh:mm:ss Z A", buf1, nil, nil) == nil)
+			fail("parse: %r\n");
+		if(tmnorm(&tm) != tmnorm(&tt))
+			print("bad am/pm parsed: %s != %s (%lld != %lld)\n", buf, buf1, tmnorm(&tm), tmnorm(&tt));
+	}
+
+	/* Time zone boundaries: entering DST */
+	if(tmtime(&tm, 1520733600, us_eastern) == nil)
+		fail("tmtime: tz boundary");
+	if(snprint(buf, sizeof(buf), "%τ", tmfmt(&tm, nil)) == -1)
+		fail("format: %r");
+	memset(&tm, 0, sizeof(tm));
+	if(tmparse(&tm, "WW MMM D hh:mm:ss ZZZ YYYY", buf, nil, nil) == nil)
+		fail("parse: %r\n");
+	if(tmnorm(&tm) != 1520733600)
+		fail("round trip timezone: %lld != 1520733600\n", tmnorm(&tm));
+
+	/* Time zone boundaries: leaving DST */
+	if(tmtime(&tm, 1541296800, us_eastern) == nil)
+		fail("tmtime: tz boundary");
+	if(snprint(buf, sizeof(buf), "%τ", tmfmt(&tm, nil)) == -1)
+		fail("format: %r\n");
+	memset(&tm, 0, sizeof(tm));
+	if(tmparse(&tm, "WW MMM D hh:mm:ss ZZZ YYYY", buf, nil, nil) == nil)
+		fail("parse: %r");
+	if(tmnorm(&tm) != 1541296800)
+		fail("round trip timezone: %lld != 1541296800\n", tmnorm(&tm));
+	
+
+	/* flexible date parsing */
+	if(tmparse(&tm, "?YYYY ?MM DD hh:mm:ss ?ZZZ", "89 04 10 20:14:30 -0400", nil, nil) == nil)
+		fail("failed parse: %r\n");
+	testtm("flexdates", 1989, 3, 10, 20, 14, 30, 0, &tm);
+	char **d, *flexdates[] = {
+		"1920 4 10 20:14:30 -0400",
+		"1920 04 10 20:14:30 -0400",
+		"1920 Apr 10 20:14:30 -0400",
+		"1920 Apr 10 20:14:30 -04:00",
+		"1920 Apr 10 20:14:30 -04:00",
+		"1920 Apr 10 20:14:30 -04:00",
+		"1920 April 10 20:14:30 EDT",
+		"20 April 10 20:14:30 EDT",
+		nil,
+	};
+	for(d = flexdates; *d; d++){
+		if(tmparse(&tm, "?YYYY ?MM DD hh:mm:ss ?ZZZ", *d, nil, nil) == nil)
+			fail("failed parse: %r\n");
+		testtm("flexdates", 1920, 3, 10, 20, 14, 30, 0, &tm);
+	}
+
+	/* Fuzzy zone */
+	if(tmparse(&tm, "?YYYY ?MM DD hh:mm:ss ?ZZZ", "2020 04 10 20:14:30 NOPE", nil, nil) == nil)
+		fail("failed parse: %r\n");
+	testtm("fuzzy-nonzone", 2020, 3, 10, 20, 14, 30, 0, &tm);
+
+	/* test tmnorm() offset */
+	memset(&tm, 0, sizeof(Tm));
+	tm.year = 120;
+	tm.sec=0;
+	tm.min=0;
+	tm.hour=0;
+	tm.mday=22;
+	tm.mon=5;
+	tm.tz=us_central;
+	tmnorm(&tm);
+	if(tmnorm(&tm) != 1592802000)
+		fail("tmnorm is not using the daylight savings time offset. %lld != 1592809200\n", tmnorm(&tm));
+
+	memset(&tm, 0, sizeof(Tm));
+	if(tmnow(&tm, us_central) == nil)
+		fail("tmnow(): %r");
+	tm.year = 120;
+	tm.sec=0;
+	tm.min=0;
+	tm.hour=0;
+	tm.mday=22;
+	tm.mon=5;
+	tm.tz=us_central;
+	s = tmnorm(&tm);
+	if(s != 1592802000)
+		fail("tmnorm is not using the daylight savings time offset. %lld != 1592809200\n", s);
+	tm.year = 120;
+	tm.sec=0;
+	tm.min=0;
+	tm.hour=0;
+	tm.mday=22;
+	tm.mon=0;
+	s = tmnorm(&tm);
+	if(s != 1579672800)
+		fail("tmnorm is not using the daylight savings time offset. %lld != 1579672800\n", s);
+	tm.tz=us_eastern;
+	s = tmnorm(&tm);
+	if(s != 1579669200)
+		fail("tmnorm converted to us_eastern. %lld != 1579669200\n", s);
+	tm.tz=us_central;
+	s = tmnorm(&tm);
+	if(s != 1579672800)
+		fail("tmnorm converted back to us_central. %lld != 1579672800\n", s);
+
+	if(failed)
+		exits("test failed");
+	exits(nil);
+}
--- /dev/null
+++ b/sys/src/libc/test/mkfile
@@ -1,0 +1,8 @@
+</$objtype/mkfile
+
+TEST=\
+	date\
+	pow\
+	strchr\
+
+</sys/src/cmd/mktest
--- /dev/null
+++ b/sys/src/libc/test/pow.c
@@ -1,0 +1,69 @@
+#include <u.h>
+#include <libc.h>
+
+/* We try to match the results specified by posix */
+void
+main(void)
+{
+	/*
+	 * For any value of y (including NaN), 
+	 *	if x is +1, 1.0 shall be returned.
+	 */
+	assert(pow(1.0, 132234.3) == 1.0);
+	assert(pow(1.0, NaN()) == 1.0);
+
+	/*
+	 * For any value of x (including NaN),
+	 *	if y is ±0, 1.0 shall be returned.
+	 */
+	assert(pow(10213.7, 0.0) == 1.0);
+	assert(pow(NaN(), 0.0) == 1.0);
+
+	/*
+	 *  If x or y is a NaN, a NaN shall be returned (unless
+	 * specified elsewhere in this description).
+	 */
+	assert(isNaN(pow(NaN(), NaN())));
+	assert(isNaN(pow(NaN(), 42.42)));
+	assert(isNaN(pow(42.42, NaN())));
+
+	/*
+	 * For any odd integer value of y > 0,
+	 *	if x is ±0, ±0 shall be returned.
+	 */
+	assert(pow(0.0, 1.0) == 0.0);
+	assert(pow(0.0, 39.0) == 0.0);
+	assert(pow(-0.0, 1.0) == -0.0);
+
+	/*
+	 * For y > 0 and not an odd integer,
+	 *	if x is ±0, +0 shall be returned.
+	 */
+	assert(pow(0.0, 2.0) == 0.0);
+	assert(pow(0.0, 34.0) == 0.0);
+	assert(pow(-0.0, 22.0) == 0.0);
+
+	/* If x is -1, and y is ±Inf, 1.0 shall be returned. */
+	assert(pow(-1.0, Inf(1)) == 1.0);
+	/* For |x| < 1, if y is -Inf, +Inf shall be returned. */
+	assert(isInf(pow(0.9, Inf(-1)), 1));
+	/* For |x| > 1, if y is -Inf, +0 shall be returned. */
+	assert(pow(1.1, Inf(-1)) == 0);
+	/* For |x| < 1, if y is +Inf, +0 shall be returned. */
+	assert(pow(0.9, Inf(1)) == 0.0);
+	/* For |x| > 1, if y is +Inf, +Inf shall be returned. */
+	assert(isInf(pow(1.1, Inf(1)), 1));
+	/* For y an odd integer < 0, if x is -Inf, -0 shall be returned. */
+	assert(pow(-7, Inf(-1)) == -0.0);
+	/* For y < 0 and not an odd integer, if x is -Inf, +0 shall be returned. */
+	assert(pow(Inf(-1), -0.3) == 0);
+	/* For y an odd integer > 0, if x is -Inf, -Inf shall be returned. */
+	assert(isInf(pow(Inf(-1), 7), -1));
+	/* For y > 0 and not an odd integer, if x is -Inf, +Inf shall be returned. */
+	assert(isInf(pow(Inf(-1), 19123.25324), 1));
+	/* For y < 0, if x is +Inf, +0 shall be returned. */
+	assert(pow(Inf(1), -1.3) == 0.0);
+	/* For y > 0, if x is +Inf, +Inf shall be returned. */
+	assert(isInf(pow(Inf(1), 1.7), 1));
+	exits(nil);
+}
--- /dev/null
+++ b/sys/src/libc/test/strchr.c
@@ -1,0 +1,47 @@
+#include <u.h>
+#include <libc.h>
+void
+chars(void)
+{
+	char *z = "";
+	char *v = "foo bar ss";
+	char *e;
+
+	e = strchr(z, 0);
+	assert(e == z);
+	e = strchr(z, 'z');
+	assert(e == nil);
+	e = strchr(v, L'z');
+	assert(e == nil);
+	e = strchr(v, L'a');
+	assert(e == v+5);
+	e = strchr(v, 0);
+	assert(e == v+10);
+}
+
+void
+runes(void)
+{
+	Rune *z = L"";
+	Rune *c = L"foo βαρ ß";
+	Rune *e;
+
+	e = runestrchr(z, 0);
+	assert(e == z);
+	e = runestrchr(z, L'z');
+	assert(e == nil);
+	e = runestrchr(c, L'z');
+	assert(e == nil);
+	e = runestrchr(c, L'α');
+	assert(e == c+5);
+	e = runestrchr(c, 0);
+	assert(e == c+9);
+}
+
+void
+main(void)
+{
+	chars();
+	runes();
+	exits(nil);
+}
--- a/sys/src/libmp/mkfile
+++ b/sys/src/libmp/mkfile
@@ -33,6 +33,11 @@
 	}
 	rm -f */*.[$OS]
 
+test:VQ:
+	cd test && mk test
+	
+
+# really dev tools more than tests
 test.$O: test.c /$objtype/include/u.h /sys/include/mp.h port/dat.h
 	$CC -Iport test.c
 
--- a/sys/src/libmp/test/main.c
+++ b/sys/src/libmp/test/main.c
@@ -50,4 +50,5 @@
 	
 	convtests();
 	tests();
+	exits(nil);
 }
--- a/sys/src/libmp/test/mkfile
+++ b/sys/src/libmp/test/mkfile
@@ -1,8 +1,9 @@
 </$objtype/mkfile
 
-TARG=test
+TEST=\
+	main
+
 OFILES=\
-	main.$O\
 	ld.$O\
 	gen.tab.$O\
 	convtest.$O\
@@ -11,7 +12,7 @@
 
 CLEANFILES=gen.tab.c
 
-</sys/src/cmd/mkone
+</sys/src/cmd/mktest
 
 gen.tab.c:D: testgen.rc
 	rc testgen.rc >gen.tab.c
--- a/sys/src/libsec/mkfile
+++ b/sys/src/libsec/mkfile
@@ -39,3 +39,6 @@
 		mk $MKFLAGS install
 	}
 	rm -f */*.[$OS]
+
+test:VQ:
+	# nothing
--- a/sys/src/libstdio/mkfile
+++ b/sys/src/libstdio/mkfile
@@ -64,6 +64,3 @@
 </sys/src/cmd/mksyslib
 # this code really can't handle any flow-analysis warnings
 CFLAGS=
-
-test:V:	$OFILES
-	ar vu libstdio.a $OFILES
--- a/sys/src/mkfile
+++ b/sys/src/mkfile
@@ -50,7 +50,7 @@
 none:VQ:
 	echo mk all, install, clean, nuke, release, kernels, or libs
 
-all install clean nuke:VQ:
+all install clean nuke test:VQ:
 	date
 	for (i in $LIBS $SUBSYS $CMDS) @{
 		cd $i