[pacman-dev,4/4] add basic makepkg test

Message ID 20170225172118.5780-5-andrew.gregory.8@gmail.com
State New
Headers show
Series
  • makepkg test suite
Related show

Commit Message

Andrew Gregory Feb. 25, 2017, 5:21 p.m. UTC
---
 Makefile.am                         |   6 +-
 configure.ac                        |   2 +
 test/makepkg/Makefile.am            |   9 +++
 test/makepkg/README                 |  15 ++++
 test/makepkg/test_functions.sh      | 143 ++++++++++++++++++++++++++++++++++++
 test/makepkg/tests/Makefile.am      |   7 ++
 test/makepkg/tests/TESTS            |   4 +
 test/makepkg/tests/dbfiles.sh       |  35 +++++++++
 test/makepkg/tests/dotfiles.sh      |  33 +++++++++
 test/makepkg/tests/pkgbuild.sh      |  53 +++++++++++++
 test/makepkg/tests/util-pkgbuild.sh |  35 +++++++++
 11 files changed, 341 insertions(+), 1 deletion(-)
 create mode 100644 test/makepkg/Makefile.am
 create mode 100644 test/makepkg/README
 create mode 100644 test/makepkg/test_functions.sh
 create mode 100644 test/makepkg/tests/Makefile.am
 create mode 100644 test/makepkg/tests/TESTS
 create mode 100755 test/makepkg/tests/dbfiles.sh
 create mode 100755 test/makepkg/tests/dotfiles.sh
 create mode 100755 test/makepkg/tests/pkgbuild.sh
 create mode 100755 test/makepkg/tests/util-pkgbuild.sh

Comments

brainpower Feb. 25, 2017, 7:09 p.m. UTC | #1
On 25.02.2017 18:21, Andrew Gregory wrote:
> ---
>  Makefile.am                         |   6 +-
>  configure.ac                        |   2 +
>  test/makepkg/Makefile.am            |   9 +++
>  test/makepkg/README                 |  15 ++++
>  test/makepkg/test_functions.sh      | 143 ++++++++++++++++++++++++++++++++++++
>  test/makepkg/tests/Makefile.am      |   7 ++
>  test/makepkg/tests/TESTS            |   4 +
>  test/makepkg/tests/dbfiles.sh       |  35 +++++++++
>  test/makepkg/tests/dotfiles.sh      |  33 +++++++++
>  test/makepkg/tests/pkgbuild.sh      |  53 +++++++++++++
>  test/makepkg/tests/util-pkgbuild.sh |  35 +++++++++
>  11 files changed, 341 insertions(+), 1 deletion(-)
>  create mode 100644 test/makepkg/Makefile.am
>  create mode 100644 test/makepkg/README
>  create mode 100644 test/makepkg/test_functions.sh
>  create mode 100644 test/makepkg/tests/Makefile.am
>  create mode 100644 test/makepkg/tests/TESTS
>  create mode 100755 test/makepkg/tests/dbfiles.sh
>  create mode 100755 test/makepkg/tests/dotfiles.sh
>  create mode 100755 test/makepkg/tests/pkgbuild.sh
>  create mode 100755 test/makepkg/tests/util-pkgbuild.sh
> 
> diff --git a/Makefile.am b/Makefile.am
> index 67ffc6b4..7b877a6c 100644
> --- a/Makefile.am
> +++ b/Makefile.am
> @@ -1,4 +1,4 @@
> -SUBDIRS = lib/libalpm src/util src/pacman scripts etc test/pacman test/util test/scripts
> +SUBDIRS = lib/libalpm src/util src/pacman scripts etc test/makepkg test/pacman test/util test/scripts
>  if WANT_DOC
>  SUBDIRS += doc
>  endif
> @@ -26,11 +26,15 @@ dist_pkgdata_DATA = \
>  $(top_srcdir)/test/pacman/tests/TESTS: $(wildcard test/pacman/tests/*.py)
>  	@printf "TESTS += %s\n" $^ | LC_ALL=C sort -u > "$@"
>  
> +$(top_srcdir)/test/makepkg/tests/TESTS: $(wildcard test/makepkg/tests/*.sh)
> +	@printf "TESTS += %s\n" $^ | LC_ALL=C sort -u > "$@"
> +
>  TESTS =  test/scripts/parseopts_test.sh \
>  				 test/scripts/human_to_size_test.sh \
>  				 test/scripts/makepkg-template_test.sh \
>  				 test/scripts/pacman-db-upgrade-v9.py \
>  				 test/util/vercmptest.sh
> +include $(top_srcdir)/test/makepkg/tests/TESTS
>  include $(top_srcdir)/test/pacman/tests/TESTS
>  
>  TEST_SUITE_LOG = test/test-suite.log
> diff --git a/configure.ac b/configure.ac
> index 10e4415c..d7e2c9ff 100644
> --- a/configure.ac
> +++ b/configure.ac
> @@ -530,6 +530,8 @@ scripts/Makefile
>  scripts/po/Makefile.in
>  doc/Makefile
>  etc/Makefile
> +test/makepkg/Makefile
> +test/makepkg/tests/Makefile
>  test/pacman/Makefile
>  test/pacman/tests/Makefile
>  test/scripts/Makefile
> diff --git a/test/makepkg/Makefile.am b/test/makepkg/Makefile.am
> new file mode 100644
> index 00000000..835030c0
> --- /dev/null
> +++ b/test/makepkg/Makefile.am
> @@ -0,0 +1,9 @@
> +SUBDIRS = tests
> +
> +check_SCRIPTS = test_functions.sh
> +
> +noinst_SCRIPTS = $(check_SCRIPTS)
> +
> +EXTRA_DIST = $(check_SCRIPTS)
> +
> +# vim:set noet:
> diff --git a/test/makepkg/README b/test/makepkg/README
> new file mode 100644
> index 00000000..70c6872b
> --- /dev/null
> +++ b/test/makepkg/README
> @@ -0,0 +1,15 @@
> +README
> +======
> +
> +Running Tests
> +-------------
> +
> +Environment Variables
> +---------------------
> +
> +PMTEST_SCRIPT_DIR - location of the makepkg executable; defaults to "$(dirname "$0")/../../../scripts)"
> +PMTEST_LIBMAKEPKG_DIR - base directory of the makepkg library; defaults to "$PMTEST_SCRIPT_DIR/libmakepkg"
> +VERBOSE - enable additional test output for debugging
> +
> +Adding New Tests
> +----------------
> diff --git a/test/makepkg/test_functions.sh b/test/makepkg/test_functions.sh
> new file mode 100644
> index 00000000..a6f07bcd
> --- /dev/null
> +++ b/test/makepkg/test_functions.sh
> @@ -0,0 +1,143 @@
> +# basic setup to run before every test
> +# tap_init
> +tap_init() {
> +	set +e
> +	set -u
> +	set -o pipefail
> +}
> +
> +# wrapper around tap_bail that immediately causes the test to exit non-zero
> +# tap_xbail $reason...
> +tap_xbail() {
> +	tap_bail "$@"
> +	exit 1;
> +}
> +
> +# read from stdin and reprint as diagnostic messages if VERBOSE is set and
> +# non-zero, otherwise, discard
> +# $command |& tap_filter
> +tap_filter() {
> +	local v=${VERBOSE:-0}
> +	if (( $v )); then
> +		while IFS= read line; do
> +			tap_diag "$line"
> +		done
> +	else
> +		while IFS= read line; do
> +			:
> +		done
> +	fi
> +}
> +
> +# locate the script that should be tested
> +locate_bin() {
> +	local scriptdir="${PMTEST_SCRIPT_DIR:-"$(dirname "$0")/../../../scripts"}"
> +	local script="$(realpath "${1:-"$scriptdir/makepkg-wrapper"}")"
> +	if ! type -p "$script" &>/dev/null; then
> +		tap_xbail "makepkg executable (%s) could not be located" "${script}"
> +		exit 1

Isn't this exit unnecessary if xbail already exits?


> +	fi
> +	printf "%s" "$script"
> +}
> +
> +# locate an source libmakepkg files
> +source_libmakepkg_file() {
> +	local file=$1; shift 1
> +	local scriptdir="${PMTEST_SCRIPT_DIR:-"$(dirname "$0")/../../../scripts"}"
> +	local libdir="${PMTEST_LIBMAKEPKG_DIR:-"$scriptdir/libmakepkg"}"
> +	source "$(realpath "$libdir/$file")"
> +}
> +
> +# eval a piece of code and test the return value
> +# tap_eval $code $test_name...
> +tap_eval() {
> +	local t=$1; shift 1
> +	eval "$t"
> +	tap_ok $? "$@"
> +}
> +
> +# extract ls-style information about a file:
> +# mode nhardlinks user group size month date time/year filename
> +_ar_stat() {
> +	local ar=$1 path=$2; shift 2
> +	bsdtar --fast-read -tvf "$ar" "$@" "$path"  2>/dev/null
> +}
> +
> +# same as _ar_stat but with numeric owner ids
> +_ar_nstat() {
> +	local ar=$1 path=$2; shift 2
> +	_ar_stat "$ar" "$path" --numeric-owner "$@"
> +}
> +
> +# check the owner of a given file, owner may be a numeric id or user name
> +# tap_ar_is_owner $path_to_archive $file $expected_owner $test_name...
> +tap_ar_is_owner() {
> +	local ar=$1 path=$2 expect=$3; shift 3
> +	local statfun="_ar_stat" owner unused
> +	[[ $expect =~ ^[0-9]+$ ]] && statfun="_ar_nstat"
> +	if ! read -r unused unused owner unused < <($statfun "$ar" "$path"); then
> +		tap_ok 1 "$@"
> +		tap_diag "         got: invalid path"
> +		tap_diag "    expected: '%s'" "$expect"
> +	elif [[ $owner != $expect ]]; then
> +		tap_ok 1 "$@"
> +		tap_diag "         got: '%s'" "$owner"
> +		tap_diag "    expected: '%s'" "$expect"
> +	else
> +		tap_ok 0 "$@"
> +	fi
> +}
> +
> +# check the group of a given file, group may be a numeric id or user name
> +# tap_ar_is_group $path_to_archive $file $expected_group $test_name...
> +tap_ar_is_group() {
> +	local ar=$1 path=$2 expect=$3; shift 3
> +	local statfun="_ar_stat" group unused
> +	[[ $expect =~ ^[0-9]+$ ]] && statfun="_ar_nstat"
> +	if ! read -r unused unused unused group unused < <($statfun "$ar" "$path"); then
> +		tap_ok 1 "$@"
> +		tap_diag "         got: invalid path"
> +		tap_diag "    expected: '%s'" "$expect"
> +	elif [[ $group != $expect ]]; then
> +		tap_ok 1 "$@"
> +		tap_diag "         got: '%s'" "$group"
> +		tap_diag "    expected: '%s'" "$expect"
> +	else
> +		tap_ok 0 "$@"
> +	fi
> +}
> +
> +# check if a path within an archive refers to a file
> +# tap_ar_is_file $path_to_archive $file $test_name...
> +tap_ar_is_file() {
> +	local ar=$1 path=$2; shift 2
> +	local stat="$(_ar_stat "$ar" "$path")"
> +	if [[ ${stat:0:1} != '-' ]]; then
> +		tap_ok 1 "$@"
> +		tap_diag "         got: not a file"
> +		tap_diag "    expected: '%s'" "$path"
> +	else
> +		tap_ok 0 "$@"
> +	fi
> +}
> +
> +# check if a path within an archive refers to a symbolic link
> +# tap_ar_is_link $path_to_archive $file $expected_destination $test_name...
> +tap_ar_is_link() {
> +	local ar=$1 path=$2 dest=$3; shift 3
> +	local stat="$(_ar_stat "$ar" "$path")"
> +	if [[ ${stat:0:1} != 'l' ]]; then
> +		tap_ok 1 "$@"
> +		tap_diag "         got: not a link"
> +		tap_diag "    expected: '%s'" "$dest"
> +	elif [[ ${stat##*$path -> } != $dest ]]; then
> +		tap_ok 1 "$@"
> +		tap_diag "         got: '%s'" "${stat##*$path -> }"
> +		tap_diag "    expected: '%s'" "$dest"
> +	else
> +		tap_ok 0 "$@"
> +	fi
> +}
> +
> +source "$(dirname "$0")"/../../tap.sh || exit 1
> +tap_init
> diff --git a/test/makepkg/tests/Makefile.am b/test/makepkg/tests/Makefile.am
> new file mode 100644
> index 00000000..fb59d11c
> --- /dev/null
> +++ b/test/makepkg/tests/Makefile.am
> @@ -0,0 +1,7 @@
> +check_SCRIPTS = $(wildcard *.sh)
> +
> +noinst_SCRIPTS = $(check_SCRIPTS)
> +
> +EXTRA_DIST = $(check_SCRIPTS)
> +
> +# vim:set noet:
> diff --git a/test/makepkg/tests/TESTS b/test/makepkg/tests/TESTS
> new file mode 100644
> index 00000000..f167e594
> --- /dev/null
> +++ b/test/makepkg/tests/TESTS
> @@ -0,0 +1,4 @@
> +TESTS += test/makepkg/tests/dbfiles.sh
> +TESTS += test/makepkg/tests/dotfiles.sh
> +TESTS += test/makepkg/tests/pkgbuild.sh
> +TESTS += test/makepkg/tests/util-pkgbuild.sh
> diff --git a/test/makepkg/tests/dbfiles.sh b/test/makepkg/tests/dbfiles.sh
> new file mode 100755
> index 00000000..72e196ef
> --- /dev/null
> +++ b/test/makepkg/tests/dbfiles.sh
> @@ -0,0 +1,35 @@
> +#!/bin/bash
> +
> +source "$(dirname "$0")"/../test_functions.sh || exit 1
> +
> +script="$(locate_bin "${1:-}")"
> +
> +TMPDIR="$(mktemp -d --tmpdir "${0##*/}.XXXXXX")"
> +trap "rm -rf '${TMPDIR}'" EXIT TERM
> +
> +tap_note "check that required metadata files are created"
> +tap_note "testing '%s'" "$script"
> +tap_note "using test dir '%s'" "$TMPDIR"
> +
> +(
> +	set -e
> +	cd "$TMPDIR"
> +	cat >PKGBUILD <<-'PKGBUILD'
> +		pkgname=foo
> +		pkgver=1
> +		pkgrel=1
> +		arch=(any)
> +		PKGBUILD
> +	MAKEPKG_CONF="/dev/null" PKGEXT=".pkg.tar" $script
> +) |& tap_filter
> +[[ $? -eq 0 ]] || tap_xbail "test setup failed"
> +
> +pkgfile="$TMPDIR/foo-1-1-any.pkg.tar"

Shouldn't this, like the test below, check if foo-1-1-any.pkg.tar actually exists?
Or does the test below run earlier than this, so it's tested already and can be assumed to be created?

Is it the same for all other tests below, which don't check if $pkgfile exists?

> +
> +tap_plan 10
> +for f in .BUILDINFO .PKGINFO .MTREE; do
> +	tap_ar_is_file "$pkgfile" "$f" "pkg contains %s" "$f"
> +	tap_ar_is_owner "$pkgfile" "$f" "0" "%s owner is root" "$f"
> +	tap_ar_is_group "$pkgfile" "$f" "0" "%s group is root" "$f"
> +done
> +tap_is_int "$(bsdtar -tf "$pkgfile" | wc -l)" 3 "pkg only contains known metainfo files"
> diff --git a/test/makepkg/tests/dotfiles.sh b/test/makepkg/tests/dotfiles.sh
> new file mode 100755
> index 00000000..9fa81258
> --- /dev/null
> +++ b/test/makepkg/tests/dotfiles.sh
> @@ -0,0 +1,33 @@
> +#!/bin/bash
> +
> +source "$(dirname "$0")"/../test_functions.sh || exit 1
> +
> +script="$(locate_bin "${1:-}")"
> +
> +TMPDIR="$(mktemp -d --tmpdir "${0##*/}.XXXXXX")"
> +[[ ${KEEPFILES:-0} == 0 ]] || trap "rm -rf '${TMPDIR}'" EXIT TERM
> +
> +tap_note "test that dotfiles don't make it into the package root"
> +tap_note "testing '%s'" "$script"
> +tap_note "using test dir '%s'" "$TMPDIR"
> +
> +output="$(
> +	set -e
> +	cd "$TMPDIR"
> +	cat >PKGBUILD <<-'PKGBUILD'
> +		pkgname=foo
> +		pkgver=1
> +		pkgrel=1
> +		arch=(any)
> +		package() {
> +			touch "$pkgdir"/.dotfile
> +		}
> +		PKGBUILD
> +	MAKEPKG_CONF="/dev/null" PKGEXT=".pkg.tar" $script 2>&1
> +)"
> +ret=$?
> +
> +tap_plan 3
> +tap_eval "[[ '$ret' -ne 0 ]]"  "makepkg exited non-zero"
> +tap_eval "[[ ! -f '$TMPDIR/foo-1-1-any.pkg.tar' ]]" "no package was built"
> +tap_eval "[[ '$output' = *'Dotfile found in package root'* ]]" "error message references dotfile"
> diff --git a/test/makepkg/tests/pkgbuild.sh b/test/makepkg/tests/pkgbuild.sh
> new file mode 100755
> index 00000000..5dc8d46f
> --- /dev/null
> +++ b/test/makepkg/tests/pkgbuild.sh
> @@ -0,0 +1,53 @@
> +#!/bin/bash
> +
> +source "$(dirname "$0")"/../test_functions.sh || exit 1
> +
> +script="$(locate_bin "${1:-}")"
> +
> +TMPDIR="$(mktemp -d --tmpdir "${0##*/}.XXXXXX")"
> +trap "rm -rf '${TMPDIR}'" EXIT TERM
> +
> +tap_note "basic package building test"
> +tap_note "testing '%s'" "$script"
> +tap_note "using test dir '%s'" "$TMPDIR"
> +
> +(
> +	set -e
> +	cd "$TMPDIR"
> +	cat >PKGBUILD <<-'PKGBUILD'
> +		pkgname=foo
> +		pkgver=1
> +		pkgrel=1
> +		arch=(any)
> +		package() {
> +			touch "$pkgdir/!first"
> +			touch "$pkgdir/target"
> +			ln -s target "$pkgdir/link"
> +			mkdir "$pkgdir/dir"
> +			touch "$pkgdir/dir/.dotfile"
> +		}
> +		PKGBUILD
> +	MAKEPKG_CONF="/dev/null" PKGEXT=".pkg.tar" $script
> +) |& tap_filter
> +[[ $? -eq 0 ]] || tap_xbail "test setup failed"
> +
> +pkgfile="$TMPDIR/foo-1-1-any.pkg.tar"
> +
> +tap_plan 10
> +tap_ar_is_file "$pkgfile" "!first" "pkg contains !first"
> +tap_ar_is_file "$pkgfile" "target" "pkg contains target"
> +tap_ar_is_file "$pkgfile" "dir/.dotfile" "pkg contains dir/.dotfile"
> +tap_ar_is_link "$pkgfile" "link" "target" "pkg contains link to target"
> +tap_ar_is_owner "$pkgfile" "target" "0" "target owner is root"
> +tap_ar_is_group "$pkgfile" "target" "0" "target group is root"
> +
> +tap_eval "! bsdtar -tf '$pkgfile' | grep -qE '^\\.?\\.?/'" \
> +	"package paths are relative without leading dot dirs"
> +tap_eval "bsdtar -tf '$pkgfile' | grep -v '^\\.' | LANG=C sort -Cu" \
> +	"package files are sorted"
> +tap_eval "bsdtar -tf '$pkgfile' | LANG=C sort | LANG=C sort -Cu" \
> +	"package files are unique"
> +tap_eval "bsdtar -tf '$pkgfile' | head -n1 | grep -q '^\\.'" \
> +	"db files are placed at the beginning of the package"
> +
> +tap_finish
> diff --git a/test/makepkg/tests/util-pkgbuild.sh b/test/makepkg/tests/util-pkgbuild.sh
> new file mode 100755
> index 00000000..3fd970c8
> --- /dev/null
> +++ b/test/makepkg/tests/util-pkgbuild.sh
> @@ -0,0 +1,35 @@
> +#!/bin/bash
> +
> +source "$(dirname "$0")"/../test_functions.sh || exit 1
> +
> +tap_note "testing libmakepkg/util/pkgbuild.sh"
> +
> +source_libmakepkg_file 'util/pkgbuild.sh'
> +
> +test_foo() {
> +	myarray=(foo bar)
> +	myarray+=(baz)
> +	#myarray+=(this should be ignored)
> +	myscalar=baz
> +	myscalar=quux
> +	#myscalar=ignored
> +}
> +
> +declare -a oarray
> +declare oscalar
> +
> +tap_plan 9
> +
> +tap_eval 'have_function test_foo' 'detected existing function test_foo'
> +tap_eval '! have_function test_bar' 'detected missing function test_bar'
> +
> +tap_eval 'extract_function_variable test_foo myarray 1 oarray' 'extract array variable'
> +tap_is_int "${#oarray[@]}" 3 'extracted array length'
> +tap_is_str "${oarray[0]}" 'foo' 'extracted array contents'
> +tap_is_str "${oarray[1]}" 'bar' 'extracted array contents'
> +tap_is_str "${oarray[2]}" 'baz' 'extracted array contents'
> +
> +tap_eval 'extract_function_variable test_foo myscalar 0 oscalar' 'extract scalar variable'
> +tap_is_str "$oscalar" 'quux' 'extracted scalar value'
> +
> +tap_finish
>
Allan McRae March 2, 2017, 4:39 a.m. UTC | #2
On 26/02/17 03:21, Andrew Gregory wrote:
> ---
>  Makefile.am                         |   6 +-
>  configure.ac                        |   2 +
>  test/makepkg/Makefile.am            |   9 +++
>  test/makepkg/README                 |  15 ++++
>  test/makepkg/test_functions.sh      | 143 ++++++++++++++++++++++++++++++++++++
>  test/makepkg/tests/Makefile.am      |   7 ++
>  test/makepkg/tests/TESTS            |   4 +
>  test/makepkg/tests/dbfiles.sh       |  35 +++++++++
>  test/makepkg/tests/dotfiles.sh      |  33 +++++++++
>  test/makepkg/tests/pkgbuild.sh      |  53 +++++++++++++
>  test/makepkg/tests/util-pkgbuild.sh |  35 +++++++++
>  11 files changed, 341 insertions(+), 1 deletion(-)
>  create mode 100644 test/makepkg/Makefile.am
>  create mode 100644 test/makepkg/README
>  create mode 100644 test/makepkg/test_functions.sh
>  create mode 100644 test/makepkg/tests/Makefile.am
>  create mode 100644 test/makepkg/tests/TESTS
>  create mode 100755 test/makepkg/tests/dbfiles.sh
>  create mode 100755 test/makepkg/tests/dotfiles.sh
>  create mode 100755 test/makepkg/tests/pkgbuild.sh
>  create mode 100755 test/makepkg/tests/util-pkgbuild.sh
> 

Looks good to me.  Tests are a bit more complicated to make than pacman
ones, but I will manage!

Try not to use realpath in the script.  This is not portable (there were
issues with BSD/OSX from memory).

Allan

Patch

diff --git a/Makefile.am b/Makefile.am
index 67ffc6b4..7b877a6c 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1,4 +1,4 @@ 
-SUBDIRS = lib/libalpm src/util src/pacman scripts etc test/pacman test/util test/scripts
+SUBDIRS = lib/libalpm src/util src/pacman scripts etc test/makepkg test/pacman test/util test/scripts
 if WANT_DOC
 SUBDIRS += doc
 endif
@@ -26,11 +26,15 @@  dist_pkgdata_DATA = \
 $(top_srcdir)/test/pacman/tests/TESTS: $(wildcard test/pacman/tests/*.py)
 	@printf "TESTS += %s\n" $^ | LC_ALL=C sort -u > "$@"
 
+$(top_srcdir)/test/makepkg/tests/TESTS: $(wildcard test/makepkg/tests/*.sh)
+	@printf "TESTS += %s\n" $^ | LC_ALL=C sort -u > "$@"
+
 TESTS =  test/scripts/parseopts_test.sh \
 				 test/scripts/human_to_size_test.sh \
 				 test/scripts/makepkg-template_test.sh \
 				 test/scripts/pacman-db-upgrade-v9.py \
 				 test/util/vercmptest.sh
+include $(top_srcdir)/test/makepkg/tests/TESTS
 include $(top_srcdir)/test/pacman/tests/TESTS
 
 TEST_SUITE_LOG = test/test-suite.log
diff --git a/configure.ac b/configure.ac
index 10e4415c..d7e2c9ff 100644
--- a/configure.ac
+++ b/configure.ac
@@ -530,6 +530,8 @@  scripts/Makefile
 scripts/po/Makefile.in
 doc/Makefile
 etc/Makefile
+test/makepkg/Makefile
+test/makepkg/tests/Makefile
 test/pacman/Makefile
 test/pacman/tests/Makefile
 test/scripts/Makefile
diff --git a/test/makepkg/Makefile.am b/test/makepkg/Makefile.am
new file mode 100644
index 00000000..835030c0
--- /dev/null
+++ b/test/makepkg/Makefile.am
@@ -0,0 +1,9 @@ 
+SUBDIRS = tests
+
+check_SCRIPTS = test_functions.sh
+
+noinst_SCRIPTS = $(check_SCRIPTS)
+
+EXTRA_DIST = $(check_SCRIPTS)
+
+# vim:set noet:
diff --git a/test/makepkg/README b/test/makepkg/README
new file mode 100644
index 00000000..70c6872b
--- /dev/null
+++ b/test/makepkg/README
@@ -0,0 +1,15 @@ 
+README
+======
+
+Running Tests
+-------------
+
+Environment Variables
+---------------------
+
+PMTEST_SCRIPT_DIR - location of the makepkg executable; defaults to "$(dirname "$0")/../../../scripts)"
+PMTEST_LIBMAKEPKG_DIR - base directory of the makepkg library; defaults to "$PMTEST_SCRIPT_DIR/libmakepkg"
+VERBOSE - enable additional test output for debugging
+
+Adding New Tests
+----------------
diff --git a/test/makepkg/test_functions.sh b/test/makepkg/test_functions.sh
new file mode 100644
index 00000000..a6f07bcd
--- /dev/null
+++ b/test/makepkg/test_functions.sh
@@ -0,0 +1,143 @@ 
+# basic setup to run before every test
+# tap_init
+tap_init() {
+	set +e
+	set -u
+	set -o pipefail
+}
+
+# wrapper around tap_bail that immediately causes the test to exit non-zero
+# tap_xbail $reason...
+tap_xbail() {
+	tap_bail "$@"
+	exit 1;
+}
+
+# read from stdin and reprint as diagnostic messages if VERBOSE is set and
+# non-zero, otherwise, discard
+# $command |& tap_filter
+tap_filter() {
+	local v=${VERBOSE:-0}
+	if (( $v )); then
+		while IFS= read line; do
+			tap_diag "$line"
+		done
+	else
+		while IFS= read line; do
+			:
+		done
+	fi
+}
+
+# locate the script that should be tested
+locate_bin() {
+	local scriptdir="${PMTEST_SCRIPT_DIR:-"$(dirname "$0")/../../../scripts"}"
+	local script="$(realpath "${1:-"$scriptdir/makepkg-wrapper"}")"
+	if ! type -p "$script" &>/dev/null; then
+		tap_xbail "makepkg executable (%s) could not be located" "${script}"
+		exit 1
+	fi
+	printf "%s" "$script"
+}
+
+# locate an source libmakepkg files
+source_libmakepkg_file() {
+	local file=$1; shift 1
+	local scriptdir="${PMTEST_SCRIPT_DIR:-"$(dirname "$0")/../../../scripts"}"
+	local libdir="${PMTEST_LIBMAKEPKG_DIR:-"$scriptdir/libmakepkg"}"
+	source "$(realpath "$libdir/$file")"
+}
+
+# eval a piece of code and test the return value
+# tap_eval $code $test_name...
+tap_eval() {
+	local t=$1; shift 1
+	eval "$t"
+	tap_ok $? "$@"
+}
+
+# extract ls-style information about a file:
+# mode nhardlinks user group size month date time/year filename
+_ar_stat() {
+	local ar=$1 path=$2; shift 2
+	bsdtar --fast-read -tvf "$ar" "$@" "$path"  2>/dev/null
+}
+
+# same as _ar_stat but with numeric owner ids
+_ar_nstat() {
+	local ar=$1 path=$2; shift 2
+	_ar_stat "$ar" "$path" --numeric-owner "$@"
+}
+
+# check the owner of a given file, owner may be a numeric id or user name
+# tap_ar_is_owner $path_to_archive $file $expected_owner $test_name...
+tap_ar_is_owner() {
+	local ar=$1 path=$2 expect=$3; shift 3
+	local statfun="_ar_stat" owner unused
+	[[ $expect =~ ^[0-9]+$ ]] && statfun="_ar_nstat"
+	if ! read -r unused unused owner unused < <($statfun "$ar" "$path"); then
+		tap_ok 1 "$@"
+		tap_diag "         got: invalid path"
+		tap_diag "    expected: '%s'" "$expect"
+	elif [[ $owner != $expect ]]; then
+		tap_ok 1 "$@"
+		tap_diag "         got: '%s'" "$owner"
+		tap_diag "    expected: '%s'" "$expect"
+	else
+		tap_ok 0 "$@"
+	fi
+}
+
+# check the group of a given file, group may be a numeric id or user name
+# tap_ar_is_group $path_to_archive $file $expected_group $test_name...
+tap_ar_is_group() {
+	local ar=$1 path=$2 expect=$3; shift 3
+	local statfun="_ar_stat" group unused
+	[[ $expect =~ ^[0-9]+$ ]] && statfun="_ar_nstat"
+	if ! read -r unused unused unused group unused < <($statfun "$ar" "$path"); then
+		tap_ok 1 "$@"
+		tap_diag "         got: invalid path"
+		tap_diag "    expected: '%s'" "$expect"
+	elif [[ $group != $expect ]]; then
+		tap_ok 1 "$@"
+		tap_diag "         got: '%s'" "$group"
+		tap_diag "    expected: '%s'" "$expect"
+	else
+		tap_ok 0 "$@"
+	fi
+}
+
+# check if a path within an archive refers to a file
+# tap_ar_is_file $path_to_archive $file $test_name...
+tap_ar_is_file() {
+	local ar=$1 path=$2; shift 2
+	local stat="$(_ar_stat "$ar" "$path")"
+	if [[ ${stat:0:1} != '-' ]]; then
+		tap_ok 1 "$@"
+		tap_diag "         got: not a file"
+		tap_diag "    expected: '%s'" "$path"
+	else
+		tap_ok 0 "$@"
+	fi
+}
+
+# check if a path within an archive refers to a symbolic link
+# tap_ar_is_link $path_to_archive $file $expected_destination $test_name...
+tap_ar_is_link() {
+	local ar=$1 path=$2 dest=$3; shift 3
+	local stat="$(_ar_stat "$ar" "$path")"
+	if [[ ${stat:0:1} != 'l' ]]; then
+		tap_ok 1 "$@"
+		tap_diag "         got: not a link"
+		tap_diag "    expected: '%s'" "$dest"
+	elif [[ ${stat##*$path -> } != $dest ]]; then
+		tap_ok 1 "$@"
+		tap_diag "         got: '%s'" "${stat##*$path -> }"
+		tap_diag "    expected: '%s'" "$dest"
+	else
+		tap_ok 0 "$@"
+	fi
+}
+
+source "$(dirname "$0")"/../../tap.sh || exit 1
+tap_init
diff --git a/test/makepkg/tests/Makefile.am b/test/makepkg/tests/Makefile.am
new file mode 100644
index 00000000..fb59d11c
--- /dev/null
+++ b/test/makepkg/tests/Makefile.am
@@ -0,0 +1,7 @@ 
+check_SCRIPTS = $(wildcard *.sh)
+
+noinst_SCRIPTS = $(check_SCRIPTS)
+
+EXTRA_DIST = $(check_SCRIPTS)
+
+# vim:set noet:
diff --git a/test/makepkg/tests/TESTS b/test/makepkg/tests/TESTS
new file mode 100644
index 00000000..f167e594
--- /dev/null
+++ b/test/makepkg/tests/TESTS
@@ -0,0 +1,4 @@ 
+TESTS += test/makepkg/tests/dbfiles.sh
+TESTS += test/makepkg/tests/dotfiles.sh
+TESTS += test/makepkg/tests/pkgbuild.sh
+TESTS += test/makepkg/tests/util-pkgbuild.sh
diff --git a/test/makepkg/tests/dbfiles.sh b/test/makepkg/tests/dbfiles.sh
new file mode 100755
index 00000000..72e196ef
--- /dev/null
+++ b/test/makepkg/tests/dbfiles.sh
@@ -0,0 +1,35 @@ 
+#!/bin/bash
+
+source "$(dirname "$0")"/../test_functions.sh || exit 1
+
+script="$(locate_bin "${1:-}")"
+
+TMPDIR="$(mktemp -d --tmpdir "${0##*/}.XXXXXX")"
+trap "rm -rf '${TMPDIR}'" EXIT TERM
+
+tap_note "check that required metadata files are created"
+tap_note "testing '%s'" "$script"
+tap_note "using test dir '%s'" "$TMPDIR"
+
+(
+	set -e
+	cd "$TMPDIR"
+	cat >PKGBUILD <<-'PKGBUILD'
+		pkgname=foo
+		pkgver=1
+		pkgrel=1
+		arch=(any)
+		PKGBUILD
+	MAKEPKG_CONF="/dev/null" PKGEXT=".pkg.tar" $script
+) |& tap_filter
+[[ $? -eq 0 ]] || tap_xbail "test setup failed"
+
+pkgfile="$TMPDIR/foo-1-1-any.pkg.tar"
+
+tap_plan 10
+for f in .BUILDINFO .PKGINFO .MTREE; do
+	tap_ar_is_file "$pkgfile" "$f" "pkg contains %s" "$f"
+	tap_ar_is_owner "$pkgfile" "$f" "0" "%s owner is root" "$f"
+	tap_ar_is_group "$pkgfile" "$f" "0" "%s group is root" "$f"
+done
+tap_is_int "$(bsdtar -tf "$pkgfile" | wc -l)" 3 "pkg only contains known metainfo files"
diff --git a/test/makepkg/tests/dotfiles.sh b/test/makepkg/tests/dotfiles.sh
new file mode 100755
index 00000000..9fa81258
--- /dev/null
+++ b/test/makepkg/tests/dotfiles.sh
@@ -0,0 +1,33 @@ 
+#!/bin/bash
+
+source "$(dirname "$0")"/../test_functions.sh || exit 1
+
+script="$(locate_bin "${1:-}")"
+
+TMPDIR="$(mktemp -d --tmpdir "${0##*/}.XXXXXX")"
+[[ ${KEEPFILES:-0} == 0 ]] || trap "rm -rf '${TMPDIR}'" EXIT TERM
+
+tap_note "test that dotfiles don't make it into the package root"
+tap_note "testing '%s'" "$script"
+tap_note "using test dir '%s'" "$TMPDIR"
+
+output="$(
+	set -e
+	cd "$TMPDIR"
+	cat >PKGBUILD <<-'PKGBUILD'
+		pkgname=foo
+		pkgver=1
+		pkgrel=1
+		arch=(any)
+		package() {
+			touch "$pkgdir"/.dotfile
+		}
+		PKGBUILD
+	MAKEPKG_CONF="/dev/null" PKGEXT=".pkg.tar" $script 2>&1
+)"
+ret=$?
+
+tap_plan 3
+tap_eval "[[ '$ret' -ne 0 ]]"  "makepkg exited non-zero"
+tap_eval "[[ ! -f '$TMPDIR/foo-1-1-any.pkg.tar' ]]" "no package was built"
+tap_eval "[[ '$output' = *'Dotfile found in package root'* ]]" "error message references dotfile"
diff --git a/test/makepkg/tests/pkgbuild.sh b/test/makepkg/tests/pkgbuild.sh
new file mode 100755
index 00000000..5dc8d46f
--- /dev/null
+++ b/test/makepkg/tests/pkgbuild.sh
@@ -0,0 +1,53 @@ 
+#!/bin/bash
+
+source "$(dirname "$0")"/../test_functions.sh || exit 1
+
+script="$(locate_bin "${1:-}")"
+
+TMPDIR="$(mktemp -d --tmpdir "${0##*/}.XXXXXX")"
+trap "rm -rf '${TMPDIR}'" EXIT TERM
+
+tap_note "basic package building test"
+tap_note "testing '%s'" "$script"
+tap_note "using test dir '%s'" "$TMPDIR"
+
+(
+	set -e
+	cd "$TMPDIR"
+	cat >PKGBUILD <<-'PKGBUILD'
+		pkgname=foo
+		pkgver=1
+		pkgrel=1
+		arch=(any)
+		package() {
+			touch "$pkgdir/!first"
+			touch "$pkgdir/target"
+			ln -s target "$pkgdir/link"
+			mkdir "$pkgdir/dir"
+			touch "$pkgdir/dir/.dotfile"
+		}
+		PKGBUILD
+	MAKEPKG_CONF="/dev/null" PKGEXT=".pkg.tar" $script
+) |& tap_filter
+[[ $? -eq 0 ]] || tap_xbail "test setup failed"
+
+pkgfile="$TMPDIR/foo-1-1-any.pkg.tar"
+
+tap_plan 10
+tap_ar_is_file "$pkgfile" "!first" "pkg contains !first"
+tap_ar_is_file "$pkgfile" "target" "pkg contains target"
+tap_ar_is_file "$pkgfile" "dir/.dotfile" "pkg contains dir/.dotfile"
+tap_ar_is_link "$pkgfile" "link" "target" "pkg contains link to target"
+tap_ar_is_owner "$pkgfile" "target" "0" "target owner is root"
+tap_ar_is_group "$pkgfile" "target" "0" "target group is root"
+
+tap_eval "! bsdtar -tf '$pkgfile' | grep -qE '^\\.?\\.?/'" \
+	"package paths are relative without leading dot dirs"
+tap_eval "bsdtar -tf '$pkgfile' | grep -v '^\\.' | LANG=C sort -Cu" \
+	"package files are sorted"
+tap_eval "bsdtar -tf '$pkgfile' | LANG=C sort | LANG=C sort -Cu" \
+	"package files are unique"
+tap_eval "bsdtar -tf '$pkgfile' | head -n1 | grep -q '^\\.'" \
+	"db files are placed at the beginning of the package"
+
+tap_finish
diff --git a/test/makepkg/tests/util-pkgbuild.sh b/test/makepkg/tests/util-pkgbuild.sh
new file mode 100755
index 00000000..3fd970c8
--- /dev/null
+++ b/test/makepkg/tests/util-pkgbuild.sh
@@ -0,0 +1,35 @@ 
+#!/bin/bash
+
+source "$(dirname "$0")"/../test_functions.sh || exit 1
+
+tap_note "testing libmakepkg/util/pkgbuild.sh"
+
+source_libmakepkg_file 'util/pkgbuild.sh'
+
+test_foo() {
+	myarray=(foo bar)
+	myarray+=(baz)
+	#myarray+=(this should be ignored)
+	myscalar=baz
+	myscalar=quux
+	#myscalar=ignored
+}
+
+declare -a oarray
+declare oscalar
+
+tap_plan 9
+
+tap_eval 'have_function test_foo' 'detected existing function test_foo'
+tap_eval '! have_function test_bar' 'detected missing function test_bar'
+
+tap_eval 'extract_function_variable test_foo myarray 1 oarray' 'extract array variable'
+tap_is_int "${#oarray[@]}" 3 'extracted array length'
+tap_is_str "${oarray[0]}" 'foo' 'extracted array contents'
+tap_is_str "${oarray[1]}" 'bar' 'extracted array contents'
+tap_is_str "${oarray[2]}" 'baz' 'extracted array contents'
+
+tap_eval 'extract_function_variable test_foo myscalar 0 oscalar' 'extract scalar variable'
+tap_is_str "$oscalar" 'quux' 'extracted scalar value'
+
+tap_finish