diff mbox

[pacman-dev,WIP] add alpm tests

Message ID 20170423230530.25945-1-andrew.gregory.8@gmail.com
State Under Review
Headers show

Commit Message

Andrew Gregory April 23, 2017, 11:05 p.m. UTC
---

For alpm tests, we use compiled tests using pactest.c to setup package/db files
and temporary working environments, ptserve.c to run an http server for sync
packages, and tap.c to generate the test output.  Everything is already
working, but I would appreciate feedback on the following in particular:

1. autotools - I have no idea if I got all of the autotools nonsense correct.

2. pactest.c API - Being C programs these are always going to be more verbose
   and have more boilerplate than our python test suite, but any suggestions
   for how to improve the pactest.c API to make writing tests easier are
   welcome.

3. git submodules - tap.c, pactest.c, and ptserve.c are projects that I'm
   managing externally, just like tap.sh.  This patch just includes static
   copies of them; should we use git submodules instead?

 Makefile.am                                    |   7 +-
 configure.ac                                   |   2 +
 test/alpm/Makefile.am                          |  11 +
 test/alpm/README                               | 108 ++++++
 test/alpm/alpmtest.h                           |  14 +
 test/alpm/pactest.c                            | 458 +++++++++++++++++++++++++
 test/alpm/ptrun                                |  58 ++++
 test/alpm/ptserve.c                            | 403 ++++++++++++++++++++++
 test/alpm/tap.c                                | 305 ++++++++++++++++
 test/alpm/tests/.gitignore                     |   3 +
 test/alpm/tests/Makefile.am                    |  21 ++
 test/alpm/tests/TESTS                          |   3 +
 test/alpm/tests/add_remove.c                   |  56 +++
 test/alpm/tests/cached_part_file.c             |  65 ++++
 test/alpm/tests/part_file_in_secondary_cache.c | 102 ++++++
 15 files changed, 1615 insertions(+), 1 deletion(-)
 create mode 100644 test/alpm/Makefile.am
 create mode 100644 test/alpm/README
 create mode 100644 test/alpm/alpmtest.h
 create mode 100644 test/alpm/pactest.c
 create mode 100755 test/alpm/ptrun
 create mode 100644 test/alpm/ptserve.c
 create mode 100644 test/alpm/tap.c
 create mode 100644 test/alpm/tests/.gitignore
 create mode 100644 test/alpm/tests/Makefile.am
 create mode 100644 test/alpm/tests/TESTS
 create mode 100644 test/alpm/tests/add_remove.c
 create mode 100644 test/alpm/tests/cached_part_file.c
 create mode 100644 test/alpm/tests/part_file_in_secondary_cache.c
diff mbox

Patch

diff --git a/Makefile.am b/Makefile.am
index 67ffc6b4..20544242 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/pacman test/util test/scripts test/alpm
 if WANT_DOC
 SUBDIRS += doc
 endif
@@ -32,6 +32,7 @@  TESTS =  test/scripts/parseopts_test.sh \
 				 test/scripts/pacman-db-upgrade-v9.py \
 				 test/util/vercmptest.sh
 include $(top_srcdir)/test/pacman/tests/TESTS
+include $(top_srcdir)/test/alpm/tests/TESTS
 
 TEST_SUITE_LOG = test/test-suite.log
 TEST_EXTENSIONS = .py
@@ -50,6 +51,10 @@  AM_PY_LOG_FLAGS = \
 		--ldconfig $(LDCONFIG) \
 		--bindir $(top_builddir)/src/pacman \
 		--bindir $(top_builddir)/scripts
+T_LOG_DRIVER = env AM_TAP_AWK='$(AWK)' $(SHELL) \
+								 $(top_srcdir)/build-aux/tap-driver.sh
+T_LOG_COMPILER = $(top_srcdir)/test/alpm/ptrun
+AM_T_LOG_FLAGS = -rc
 
 # create the pacman DB, cache, makepkg-template and system hook directories upon install
 install-data-local:
diff --git a/configure.ac b/configure.ac
index 57f068d3..514a8dd6 100644
--- a/configure.ac
+++ b/configure.ac
@@ -526,6 +526,8 @@  scripts/Makefile
 scripts/po/Makefile.in
 doc/Makefile
 etc/Makefile
+test/alpm/Makefile
+test/alpm/tests/Makefile
 test/pacman/Makefile
 test/pacman/tests/Makefile
 test/scripts/Makefile
diff --git a/test/alpm/Makefile.am b/test/alpm/Makefile.am
new file mode 100644
index 00000000..7f0a4ca0
--- /dev/null
+++ b/test/alpm/Makefile.am
@@ -0,0 +1,11 @@ 
+SUBDIRS = tests
+
+check_SCRIPTS = ptrun
+
+noinst_SCRIPTS = $(check_SCRIPTS)
+
+EXTRA_DIST = \
+	README \
+	$(check_SCRIPTS)
+
+# vim:set noet:
diff --git a/test/alpm/README b/test/alpm/README
new file mode 100644
index 00000000..3762545d
--- /dev/null
+++ b/test/alpm/README
@@ -0,0 +1,108 @@ 
+Running Tests
+=============
+
+Tests are normally run using `make check` from the project root.  Individual
+tests are located under 'tests/' and can be run directly.  `ptrun` is a wrapper
+script provided as a convenience for modifying how tests are run (using
+fakeroot, fakechroot, valgrind, etc.).
+
+Requirements
+------------
+
+All requirements are optional unless otherwise noted.  Tests that depend on any
+missing requirements will be skipped.
+
+* pthreads
+* fakechroot
+* fakeroot
+
+Writing Tests
+=============
+
+Libraries
+---------
+
+Tests should `#include "../alpmtest.h"`.  This will bring in libalpm,
+pactest.c, ptserve.c, tap.c, and stdio libraries.
+
+Test Naming
+-----------
+
+Tests designed to check a particular function or data type under multiple
+conditions should be named after the function or data type being tested (e.g.
+`alpm_filelist.c`).  Otherwise, tests should be named according to the
+particular condition being tested, avoiding an `alpm` prefix if possible (e.g.
+`cached_part_file.c`).
+
+Test Initialization
+-------------------
+
+Any functions called during test setup that could fail should be wrapped in
+`ASSERT`.
+
+Any calls that could fail should be wrapped in either an `ASSERT` or
+a `tap_*` function.
+
+Do not rely on pactest.c default settings.  If a test requires a particular
+option, set it manually in the test.
+
+If a test program is being run in an environment where it is impossible for the
+tests to properly run (e.g. due to insufficient permissions or missing
+features) the test should be skipped using `tap_skip_all` and return 0.  Tests
+which should be able to run but fail to should bail out and return 99.  In
+particular, tests which rely on install scriptlets or ldconfig should check for
+`chroot` permissions with the `CAN_CHROOT` macro and tests which rely on
+setting file ownership should check `CAN_CHOWN` before running.
+
+Test Cleanup
+------------
+
+Tests should ensure that all memory is freed after a successful run.  Memory
+leaks are acceptable if the test fails, but any `pt_env_t` or `pt_serve_t`
+resources must be freed regardless of test failure.  The easiest way to
+accomplish this is to register a `cleanup` function using `atexit`.
+
+Return Values
+-------------
+
+ 0   - success or test skipped (returned by `tap_finish`)
+ 1   - failed test(s) or bad/missing test plan (returned by `tap_finish`)
+ 77  - reserved*
+ 99  - hard error** (set by `ASSERT`)
+ 123 - valgrind error (set by ptrun)
+
+* Automake uses an exit status of 77 for simple tests to indicate that a test
+  was skipped.  For TAP-based tests an exit status of 77 counts as failure, so
+  skipped tests should call `tap_skip_all` and return 0.  Avoiding 77 as an
+  exit status prevents confusion between a failed or skipped test if a test is
+  accidentally run as a simple test.
+
+** For compatibility with Automake's simple test runner.
+
+Template
+--------
+
+.. code:: c
+
+   #include "../alpmtest.h"
+   
+   /* <describe what is being tested> */
+   
+   pt_env_t *pt = NULL;
+   
+   void cleanup(void) {
+   	pt_cleanup(pt);
+   }
+   
+   int main(void) {
+    ASSERT(atexit(cleanup) == 0);
+
+    <setup test>
+
+    tap_plan(<number of tests>);
+    <perform test>
+
+   	return tap_finish();
+   }
+
+.. vim: set ft=rst:
diff --git a/test/alpm/alpmtest.h b/test/alpm/alpmtest.h
new file mode 100644
index 00000000..3df772ab
--- /dev/null
+++ b/test/alpm/alpmtest.h
@@ -0,0 +1,14 @@ 
+#include "pactest.c"
+#include "ptserve.c"
+#include "tap.c"
+#include <alpm.h>
+#include <stdio.h>
+
+#define ASSERT(x) \
+    if(!(x)) { \
+        tap_bail("ASSERT FAILED %s line %d: '%s'", __FILE__, __LINE__, #x); \
+        exit(1); \
+    }
+
+#define CAN_CHROOT chroot("/") == 0
+#define CAN_CHOWN geteuid() == 0
diff --git a/test/alpm/pactest.c b/test/alpm/pactest.c
new file mode 100644
index 00000000..28fdb14e
--- /dev/null
+++ b/test/alpm/pactest.c
@@ -0,0 +1,458 @@ 
+/*
+ * Copyright 2015 Andrew Gregory <andrew.gregory.8@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ *
+ * Project URL: http://github.com/andrewgregory/pactest.c
+ */
+
+#ifndef PACTEST_C
+#define PACTEST_C
+
+#define PACTEST_C_VERSION "0.1"
+
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <alpm.h>
+#include <archive.h>
+#include <archive_entry.h>
+
+typedef struct pt_pkg_t {
+    alpm_list_t *backup;
+    alpm_list_t *conflicts;
+    alpm_list_t *depends;
+    alpm_list_t *groups;
+    alpm_list_t *licenses;
+    alpm_list_t *optdepends;
+    alpm_list_t *makedepends;
+    alpm_list_t *checkdepends;
+    alpm_list_t *provides;
+    alpm_list_t *replaces;
+    char *arch;
+    char *base;
+    char *builddate;
+    char *desc;
+    char *csize;
+    char *isize;
+    char *name;
+    char *packager;
+    char *url;
+    char *version;
+    char *filename;
+} pt_pkg_t;
+
+typedef struct pt_db_t {
+    char *name;
+    alpm_list_t *pkgs;
+} pt_db_t;
+
+typedef struct pt_env_t {
+    char *root;
+    char *dbpath;
+    int rootfd;
+    int dbpathfd;
+    alpm_list_t *pkgs;
+    alpm_list_t *dbs;
+} pt_env_t;
+
+/************************************
+ * utility functions
+ ************************************/
+
+char *pt_path(pt_env_t *pt, const char *path) {
+    static char abspath[PATH_MAX];
+    if(snprintf(abspath, PATH_MAX, "%s/%s", pt->root, path) >= PATH_MAX) {
+        return NULL;
+    } else {
+        return abspath;
+    }
+}
+
+static int _pt_rmrfat(int dd, const char *path) {
+    struct stat sbuf;
+    if(fstatat(dd, path, &sbuf, AT_SYMLINK_NOFOLLOW) != 0) {
+        return errno == ENOENT ? 0 : -1;
+    } else if(S_ISDIR(sbuf.st_mode)) {
+        int fd;
+        DIR *d;
+        struct dirent ent, *ctx;
+
+        if((fd = openat(dd, path, O_DIRECTORY)) < 0) { return -1; }
+        if((d = fdopendir(fd)) == NULL) { close(fd); return -1; }
+
+        while(readdir_r(d, &ent, &ctx) == 0 && ctx != NULL) {
+            if(strcmp(ent.d_name, ".") == 0) { continue; }
+            if(strcmp(ent.d_name, "..") == 0) { continue; }
+            if(_pt_rmrfat(fd, ent.d_name) != 0) { break; }
+        }
+        closedir(d);
+        return ctx == NULL ? unlinkat(dd, path, AT_REMOVEDIR) : -1;
+    } else {
+        return unlinkat(dd, path, 0);
+    }
+}
+
+static int _pt_mkpdirat(int dd, const char *path, mode_t mode) {
+    char *c, *pcopy = strdup(path);
+    if(pcopy == NULL) { return -1; }
+    for(c = pcopy; *c == '/'; c++); /* skip leading '/' */
+    while((c = strchr(c, '/'))) {
+        *c = '\0';
+        if(mkdirat(dd, pcopy, mode) != 0 && errno != EEXIST) {
+            free(pcopy);
+            return -1;
+        }
+        for(*(c++) = '/'; *c == '/'; c++); /* restore and skip '/' */
+    }
+    free(pcopy);
+    return 0;
+}
+
+int pt_mkdirat(int dd, const char *path, mode_t mode) {
+    if(_pt_mkpdirat(dd, path, mode) != 0) { return -1; }
+    return mkdirat(dd, path, mode);
+}
+
+/************************************
+ * package functions
+ ************************************/
+
+void pt_pkg_free(pt_pkg_t *pkg) {
+    if(pkg == NULL) { return; }
+    FREELIST(pkg->backup);
+    FREELIST(pkg->conflicts);
+    FREELIST(pkg->depends);
+    FREELIST(pkg->groups);
+    FREELIST(pkg->licenses);
+    FREELIST(pkg->optdepends);
+    FREELIST(pkg->provides);
+    FREELIST(pkg->replaces);
+    free(pkg->arch);
+    free(pkg->base);
+    free(pkg->builddate);
+    free(pkg->desc);
+    free(pkg->csize);
+    free(pkg->isize);
+    free(pkg->name);
+    free(pkg->packager);
+    free(pkg->url);
+    free(pkg->version);
+    free(pkg->filename);
+    free(pkg);
+}
+
+pt_pkg_t *pt_pkg_new(pt_env_t *pt, const char *pkgname, const char *pkgver) {
+    pt_pkg_t *pkg = NULL;
+#define _PT_ASSERT(x) if(!(x)) { pt_pkg_free(pkg); return NULL; }
+    _PT_ASSERT(pkg = calloc(sizeof(pt_pkg_t), 1));
+    _PT_ASSERT(pkg->name = strdup(pkgname));
+    _PT_ASSERT(pkg->version = strdup(pkgver));
+    _PT_ASSERT(pt == NULL || alpm_list_append(&pt->pkgs, pkg));
+#undef _PT_ASSERT
+    return pkg;
+}
+
+static int _pt_fwrite_pkgentry(FILE *f, const char *section, const char *value) {
+    if(value && fprintf(f, "%s = %s\n", section, value) < 0) { return -1; }
+    return 0;
+}
+
+static int _pt_fwrite_pkglist(FILE *f, const char *section, alpm_list_t *values) {
+    while(values) {
+        if(_pt_fwrite_pkgentry(f, section, values->data) < 0) { return -1; }
+        values = values->next;
+    }
+    return 0;
+}
+
+
+int _pt_pkg_write_archive(pt_pkg_t *pkg, struct archive *a) {
+    FILE *contents;
+    struct archive_entry *e;
+    char *buf = NULL;
+    size_t buflen = 0;
+
+    if((contents = open_memstream(&buf, &buflen)) == NULL) { return -1; }
+
+#define _PT_ASSERT(x) if(!(x)) { fclose(contents); free(buf); return -1; }
+    _PT_ASSERT( _pt_fwrite_pkgentry(contents, "pkgname", pkg->name) == 0 );
+    _PT_ASSERT( _pt_fwrite_pkgentry(contents, "pkgver", pkg->version) == 0 );
+    _PT_ASSERT( _pt_fwrite_pkgentry(contents, "pkgdesc", pkg->desc) == 0 );
+    _PT_ASSERT( _pt_fwrite_pkgentry(contents, "url", pkg->url) == 0 );
+    _PT_ASSERT( _pt_fwrite_pkgentry(contents, "builddate", pkg->builddate) == 0 );
+    _PT_ASSERT( _pt_fwrite_pkgentry(contents, "packager", pkg->packager) == 0 );
+    _PT_ASSERT( _pt_fwrite_pkgentry(contents, "arch", pkg->arch) == 0 );
+    _PT_ASSERT( _pt_fwrite_pkgentry(contents, "size", pkg->isize) == 0 );
+    _PT_ASSERT( _pt_fwrite_pkglist(contents, "group", pkg->groups) == 0 );
+    _PT_ASSERT( _pt_fwrite_pkglist(contents, "license", pkg->licenses) == 0 );
+    _PT_ASSERT( _pt_fwrite_pkglist(contents, "depend", pkg->depends) == 0 );
+    _PT_ASSERT( _pt_fwrite_pkglist(contents, "optdepend", pkg->optdepends) == 0 );
+    _PT_ASSERT( _pt_fwrite_pkglist(contents, "conflict", pkg->conflicts) == 0 );
+    _PT_ASSERT( _pt_fwrite_pkglist(contents, "replaces", pkg->replaces) == 0 );
+    _PT_ASSERT( _pt_fwrite_pkglist(contents, "provides", pkg->provides) == 0 );
+    _PT_ASSERT( _pt_fwrite_pkglist(contents, "backup", pkg->backup) == 0 );
+    _PT_ASSERT( fclose(contents) == 0 );
+#undef _PT_ASSERT
+
+#define _PT_ASSERT(x) if(!(x)) { free(buf); archive_entry_free(e); return -1; }
+    _PT_ASSERT( e = archive_entry_new() );
+
+    archive_entry_set_pathname(e, ".PKGINFO");
+    archive_entry_set_perm(e, 0644);
+    archive_entry_set_filetype(e, AE_IFREG);
+    archive_entry_set_size(e, buflen);
+
+    _PT_ASSERT( archive_write_header(a, e) == ARCHIVE_OK );
+    _PT_ASSERT( archive_write_data(a, buf, buflen) != -1 );
+#undef _PT_ASSERT
+
+    free(buf);
+    archive_entry_free(e);
+
+    return 0;
+}
+
+int pt_pkg_writeat(int dd, const char *path, pt_pkg_t *pkg) {
+    struct archive *a;
+    char *c;
+    int fd;
+
+    if(_pt_mkpdirat(dd, path, 0700) != 0) { return -1; }
+    if((fd = openat(dd, path, O_CREAT | O_WRONLY, 0644)) < 0) { return -1; }
+
+    if((a = archive_write_new()) == NULL) { close(fd); return -1; }
+
+#define _PT_ASSERT(x) if(!(x)) { close(fd); archive_write_free(a); return -1; }
+
+    if((c = strrchr(path, '.'))) {
+        if(strcmp(c, ".bz2") == 0) {
+            _PT_ASSERT( archive_write_add_filter_bzip2(a) == ARCHIVE_OK );
+        } else if(strcmp(c, ".gz") == 0) {
+            _PT_ASSERT( archive_write_add_filter_gzip(a) == ARCHIVE_OK );
+        } else if(strcmp(c, ".xz") == 0) {
+            _PT_ASSERT( archive_write_add_filter_xz(a) == ARCHIVE_OK );
+        } else if(strcmp(c, ".lz") == 0) {
+            _PT_ASSERT( archive_write_add_filter_lzip(a) == ARCHIVE_OK );
+        } else if(strcmp(c, ".Z") == 0) {
+            _PT_ASSERT( archive_write_add_filter_compress(a) == ARCHIVE_OK );
+        }
+    }
+
+    _PT_ASSERT( archive_write_set_format_ustar(a) == ARCHIVE_OK );
+    _PT_ASSERT( archive_write_open_fd(a, fd) == ARCHIVE_OK );
+    _PT_ASSERT( _pt_pkg_write_archive(pkg, a) == 0 );
+
+#undef _PT_ASSERT
+
+    archive_write_free(a);
+    close(fd);
+    return 0;
+}
+
+/************************************
+ * database functions
+ ************************************/
+
+void pt_db_free(pt_db_t *db) {
+    if(db == NULL) { return; }
+    alpm_list_free(db->pkgs);
+    free(db->name);
+    free(db);
+}
+
+pt_db_t *pt_db_new(pt_env_t *pt, const char *dbname) {
+    pt_db_t *db = NULL;
+#define _PT_ASSERT(x) if(!(x)) { pt_db_free(db); return NULL; }
+    _PT_ASSERT(db = calloc(sizeof(pt_db_t), 1));
+    _PT_ASSERT(db->name = strdup(dbname));
+    _PT_ASSERT(pt == NULL || alpm_list_append(&pt->dbs, db));
+#undef _PT_ASSERT
+    return db;
+}
+
+int pt_db_add_pkg(pt_db_t *db, pt_pkg_t *pkg) {
+    return alpm_list_append(&db->pkgs, pkg) ? 1 : 0;
+}
+
+int _pt_fwrite_dbheader(FILE *f, const char *header) {
+    return fprintf(f, "%%" "%s" "%%" "\n", header);
+}
+
+void _pt_fwrite_dbentry(FILE *f, const char *section, const char *value) {
+    if(value == NULL) { return; }
+    _pt_fwrite_dbheader(f, section);
+    fprintf(f, "%s\n\n", value);
+}
+
+void _pt_fwrite_dblist(FILE *f, const char *section, alpm_list_t *values) {
+    _pt_fwrite_dbheader(f, section);
+    while(values) {
+        fprintf(f, "%s\n", (char *) values->data);
+        values = values->next;
+    }
+    fputc('\n', f);
+}
+
+int pt_db_writeat(int dd, const char *path, pt_db_t *db) {
+    alpm_list_t *i;
+    struct archive *a = archive_write_new();
+    struct archive_entry *e = archive_entry_new();
+    int fd = openat(dd, path, O_CREAT | O_WRONLY, 0644);
+
+    archive_write_set_format_ustar(a);
+    archive_write_open_fd(a, fd);
+    for(i = db->pkgs; i; i = i->next) {
+        pt_pkg_t *pkg = i->data;
+        size_t buflen = 0;
+        char *buf, ppath[PATH_MAX];
+        FILE *f;
+
+        sprintf(ppath, "%s-%s/", pkg->name, pkg->version);
+        archive_entry_clear(e);
+        archive_entry_set_pathname(e, ppath);
+        archive_entry_set_filetype(e, AE_IFDIR);
+        archive_entry_set_perm(e, 0755);
+        archive_write_header(a, e);
+
+        f = open_memstream(&buf, &buflen);
+        _pt_fwrite_dblist(f, "DEPENDS", pkg->depends);
+        _pt_fwrite_dblist(f, "CONFLICTS", pkg->conflicts);
+        _pt_fwrite_dblist(f, "PROVIDES", pkg->provides);
+        _pt_fwrite_dblist(f, "OPTDEPENDS", pkg->optdepends);
+        _pt_fwrite_dblist(f, "MAKEDEPENDS", pkg->makedepends);
+        _pt_fwrite_dblist(f, "CHECKDEPENDS", pkg->checkdepends);
+        fclose(f);
+
+        sprintf(ppath, "%s-%s/depends", pkg->name, pkg->version);
+        archive_entry_clear(e);
+        archive_entry_set_pathname(e, ppath);
+        archive_entry_set_filetype(e, AE_IFREG);
+        archive_entry_set_perm(e, 0644);
+        archive_entry_set_size(e, buflen);
+        archive_write_header(a, e);
+        archive_write_data(a, buf, buflen);
+        free(buf);
+
+        f = open_memstream(&buf, &buflen);
+        _pt_fwrite_dbentry(f, "FILENAME", pkg->filename);
+        _pt_fwrite_dbentry(f, "NAME", pkg->name);
+        _pt_fwrite_dbentry(f, "ARCH", pkg->arch);
+        _pt_fwrite_dbentry(f, "BASE", pkg->base);
+        _pt_fwrite_dbentry(f, "VERSION", pkg->version);
+        _pt_fwrite_dbentry(f, "DESC", pkg->desc);
+        _pt_fwrite_dbentry(f, "CSIZE", pkg->csize);
+        _pt_fwrite_dbentry(f, "ISIZE", pkg->isize);
+        _pt_fwrite_dblist(f, "GROUPS", pkg->groups);
+        fclose(f);
+
+        sprintf(ppath, "%s-%s/desc", pkg->name, pkg->version);
+        archive_entry_clear(e);
+        archive_entry_set_pathname(e, ppath);
+        archive_entry_set_filetype(e, AE_IFREG);
+        archive_entry_set_perm(e, 0644);
+        archive_entry_set_size(e, buflen);
+        archive_write_header(a, e);
+        archive_write_data(a, buf, buflen);
+        free(buf);
+    }
+
+    archive_entry_free(e);
+    archive_write_free(a);
+    close(fd);
+    return 0;
+}
+
+/************************************
+ * pactest functions
+ ************************************/
+
+void pt_cleanup(pt_env_t *pt) {
+    if(pt == NULL) { return; }
+
+    alpm_list_free_inner(pt->pkgs, (alpm_list_fn_free) pt_pkg_free);
+    alpm_list_free(pt->pkgs);
+    alpm_list_free_inner(pt->dbs, (alpm_list_fn_free) pt_db_free);
+    alpm_list_free(pt->dbs);
+
+    close(pt->rootfd);
+    close(pt->dbpathfd);
+
+    _pt_rmrfat(AT_FDCWD, pt->root);
+
+    free(pt->root);
+    free(pt->dbpath);
+    free(pt);
+}
+
+pt_env_t *pt_init(const char *dbpath) {
+    pt_env_t *pt = NULL;
+    char *tmpdir = getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp";
+    char *template = "pactest-XXXXXX";
+    size_t rootlen = strlen(tmpdir) + strlen("/") + strlen(template);
+    if(dbpath == NULL) { dbpath = "var/lib/pacman"; }
+#define _PT_ASSERT(x) if(!(x)) { pt_cleanup(pt); return NULL; }
+    _PT_ASSERT(pt = calloc(sizeof(pt_env_t), 1));
+    _PT_ASSERT(pt->root = malloc(rootlen + 1));
+    _PT_ASSERT(sprintf(pt->root, "%s/%s", tmpdir, template) > 0);
+    _PT_ASSERT(mkdtemp(pt->root) != NULL);
+    _PT_ASSERT((pt->rootfd = open(pt->root, O_DIRECTORY)) >= 0);
+    _PT_ASSERT(pt->dbpath = strdup(pt_path(pt, dbpath)));
+    _PT_ASSERT(pt_mkdirat(pt->rootfd, dbpath, 0700) == 0);
+    _PT_ASSERT((pt->dbpathfd = open(pt->dbpath, O_DIRECTORY)) >= 0);
+#undef _PT_ASSERT
+    return pt;
+}
+
+int pt_install_db(pt_env_t *pt, pt_db_t *db) {
+    char path[PATH_MAX];
+    pt_mkdirat(pt->dbpathfd, "sync", 0755);
+    sprintf(path, "sync/%s.db", db->name);
+    return pt_db_writeat(pt->dbpathfd, path, db);
+}
+
+char *pt_vasprintf(const char *fmt, va_list args) {
+    va_list arg_cp;
+    size_t len;
+    char *ret;
+    va_copy(arg_cp, args);
+    len = vsnprintf(NULL, 0, fmt, arg_cp);
+    va_end(arg_cp);
+    if((ret = malloc(len + 2)) != NULL) { vsprintf(ret, fmt, args); }
+    return ret;
+}
+
+char *pt_asprintf(const char *fmt, ...) {
+    va_list args;
+    char *ret;
+    va_start(args, fmt);
+    ret = pt_vasprintf(fmt, args);
+    va_end(args);
+    return ret;
+}
+
+#endif /* PACTEST_C */
diff --git a/test/alpm/ptrun b/test/alpm/ptrun
new file mode 100755
index 00000000..dcdc9e55
--- /dev/null
+++ b/test/alpm/ptrun
@@ -0,0 +1,58 @@ 
+#!/bin/bash
+
+fakechroot=0
+fakeroot=0
+gdb=0
+libtool=0
+valgrind=0
+cmd=()
+
+extend() {
+    if which "$1" &>/dev/null; then
+        cmd+=("$@")
+    else
+        # bailing out would be counted as a failure by test harnesses,
+        # ignore missing programs so that tests can be gracefully skipped
+        printf "warning: command '$1' not found\n" >&2
+    fi
+}
+
+usage() {
+    printf "ptrun - run an executable pacman test\n"
+    printf "usage: ptrun [options] <testfile> [test-options]\n"
+    printf "\n"
+    printf "Options:\n"
+    printf "   -c   fakechroot\n"
+    printf "   -d   enable alpm debug log\n"
+    printf "   -r   fakeroot\n"
+    printf "   -g   gdb (implies -l)\n"
+    printf "   -h   display help\n"
+    printf "   -l   libtool execute\n"
+    printf "   -s   preserve test environment\n"
+    printf "   -v   valgrind (implies -l)\n"
+}
+
+while getopts cdghlrsv name; do
+  case $name in
+    c) fakechroot=1;;
+    d) export PT_DEBUG=1;;
+    g) libtool=1; gdb=1;;
+    h) usage; exit;;
+    l) libtool=1;;
+    r) fakeroot=1;;
+    s) export PT_KEEP_ROOT=1;;
+    v) libtool=1; valgrind=1;;
+  esac
+done
+
+[ $fakechroot -eq 1 ] && extend fakechroot
+[ $fakeroot -eq 1 ] && extend fakeroot
+[ $libtool -eq 1 ] && extend libtool execute
+[ $gdb -eq 1 ] && extend gdb
+[ $valgrind -eq 1 ] && extend valgrind --quiet --leak-check=full \
+    --gen-suppressions=yes --error-exitcode=123 \
+    --suppressions="$(dirname "$0")/../../valgrind.supp"
+
+while (( --OPTIND > 0 )); do shift; done # remove our options from the stack
+
+"${cmd[@]}" "$@"
diff --git a/test/alpm/ptserve.c b/test/alpm/ptserve.c
new file mode 100644
index 00000000..049abda0
--- /dev/null
+++ b/test/alpm/ptserve.c
@@ -0,0 +1,403 @@ 
+/*
+ * Copyright 2015 Andrew Gregory <andrew.gregory.8@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ *
+ * Project URL: http://github.com/andrewgregory/pactest.c
+ */
+
+#ifndef PTSERVE_C
+#define PTSERVE_C
+
+#define PTSERVE_C_VERSION 0.1
+
+#include <arpa/inet.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/wait.h>
+
+#include <alpm.h>
+
+typedef struct ptserve_message_t {
+	struct ptserve_t *ptserve;
+	char *method;
+	char *path;
+	char *protocol;
+	alpm_list_t *headers;
+	int socket_fd;
+} ptserve_message_t;
+
+typedef void (ptserve_response_cb_t)(ptserve_message_t *request);
+
+typedef struct ptserve_t {
+	ptserve_response_cb_t *response_cb;
+	uint16_t port;
+	char *url;
+	void *data;
+	int rootfd;
+	int sd_server;
+	pid_t _pid;
+	pthread_t _tid;
+} ptserve_t;
+
+/*****************************************************************************
+ * utilities
+ ****************************************************************************/
+
+static int _vasprintf(char **strp, const char *fmt, va_list args) {
+	va_list arg_cp;
+	size_t len;
+	va_copy(arg_cp, args);
+	len = vsnprintf(NULL, 0, fmt, arg_cp);
+	va_end(arg_cp);
+	if((*strp = malloc(len + 2)) != NULL) { return vsprintf(*strp, fmt, args); }
+	else { return -1; }
+}
+
+static int _asprintf(char **strp, const char *fmt, ...) {
+	va_list args;
+	int ret;
+	va_start(args, fmt);
+	ret = _vasprintf(strp, fmt, args);
+	va_end(args);
+	return ret;
+}
+
+static ssize_t _send(int socket, const void *buf, size_t len) {
+	return send(socket, buf, len, MSG_NOSIGNAL);
+}
+
+static ssize_t _sendf(int socket, const char *fmt, ...) {
+	ssize_t ret;
+	char *buf = NULL;
+	int blen = 0;
+	va_list args;
+	va_start(args, fmt);
+	blen = _vasprintf(&buf, fmt, args);
+	va_end(args);
+	ret = _send(socket, buf, blen);
+	free(buf);
+	return ret;
+}
+
+static ssize_t _dgetdelim(int fd, char *buf, ssize_t bufsiz, char *delim) {
+	char *d = delim, *b = buf;
+	while(1) {
+		ssize_t ret = read(fd, b, 1);
+		if(ret == 0) { *b = '\0'; return b - buf; }
+		if(ret == -1) { return -1; }
+		if(*d && *b == *d) {
+			if(*(++d) == '\0') {
+				b -= strlen(delim) - 1;
+				*b = '\0';
+				return b - buf;
+			}
+		} else {
+			d = delim;
+		}
+		if(++b - buf >= bufsiz - 1) { return -1; }
+	}
+}
+
+/*****************************************************************************
+ * http message
+ ****************************************************************************/
+
+#define PTSERVE_HDR_MAX 1024
+ptserve_message_t *ptserve_message_new(ptserve_t *server, int socket_fd) {
+	ptserve_message_t *msg = calloc(sizeof(ptserve_message_t), 1);
+	char line[PTSERVE_HDR_MAX];
+
+	_dgetdelim(socket_fd, line, PTSERVE_HDR_MAX, " ");
+	msg->method = strdup(line);
+	_dgetdelim(socket_fd, line, PTSERVE_HDR_MAX, " ");
+	msg->path = strdup(line);
+	_dgetdelim(socket_fd, line, PTSERVE_HDR_MAX, "\r\n");
+	msg->protocol = strdup(line);
+
+	while(_dgetdelim(socket_fd, line, PTSERVE_HDR_MAX, "\r\n") > 0) {
+		msg->headers = alpm_list_add(msg->headers, strdup(line));
+	}
+
+	msg->ptserve = server;
+	msg->socket_fd = socket_fd;
+
+	return msg;
+}
+
+void ptserve_message_free(ptserve_message_t *msg) {
+	if(msg == NULL) { return; }
+	free(msg->method);
+	free(msg->path);
+	free(msg->protocol);
+	FREELIST(msg->headers);
+	if(msg->socket_fd >= 0) { close(msg->socket_fd); }
+	free(msg);
+}
+
+const char *ptserve_message_get_header(ptserve_message_t *msg, const char *hdr) {
+	alpm_list_t *i;
+	size_t hlen = strlen(hdr);
+	for(i = msg->headers; i; i = alpm_list_next(i)) {
+		const char *mhdr = i->data;
+		if(strncasecmp(mhdr, hdr, hlen) == 0 && strncmp(mhdr + hlen, ": ", 2) == 0) {
+			return mhdr + hlen + 2;
+		}
+	}
+	return NULL;
+}
+
+void ptserve_message_rm_header(ptserve_message_t *msg, const char *hdr) {
+	alpm_list_t *i;
+	size_t hlen = strlen(hdr);
+	for(i = msg->headers; i; i = i->next) {
+		char *oldheader = i->data;
+		if(strncasecmp(i->data, hdr, hlen) == 0 && oldheader[hlen] == ':') {
+			msg->headers = alpm_list_remove_item(msg->headers, i);
+			free(i->data);
+			free(i);
+			return;
+		}
+	}
+}
+
+int ptserve_message_set_header(ptserve_message_t *message,
+		const char *header, const char *value) {
+	alpm_list_t *i;
+	char *newheader;
+	size_t hlen = strlen(header);
+
+	if(_asprintf(&newheader, "%s: %s", header, value) == -1) { return 0; }
+
+	/* look for an existing header */
+	for(i = message->headers; i; i = i->next) {
+		char *oldheader = i->data;
+		if(strncasecmp(i->data, header, hlen) == 0 && oldheader[hlen] == ':') {
+			free(i->data);
+			i->data = newheader;
+			return 1;
+		}
+	}
+
+	message->headers = alpm_list_add(message->headers, newheader);
+	return 1;
+}
+
+/*****************************************************************************
+ * ptserve
+ ****************************************************************************/
+
+ptserve_t *ptserve_new() {
+	ptserve_t *ptserve = calloc(sizeof(ptserve_t), 1);
+	if(ptserve == NULL) { return NULL; }
+	ptserve->rootfd = AT_FDCWD;
+	ptserve->sd_server = -1;
+	ptserve->_tid = -1;
+	return ptserve;
+}
+
+void ptserve_free(ptserve_t *ptserve) {
+	if(ptserve == NULL) { return; }
+	if(ptserve->_pid > 0) {
+		kill(ptserve->_pid, SIGTERM);
+		waitpid(ptserve->_pid, NULL, 0);
+	} else if(ptserve->_tid != -1) {
+		pthread_cancel(ptserve->_tid);
+		/* pthread_kill(ptserve->_tid, SIGINT); */
+		/* pthread_join(ptserve->_tid, NULL); */
+	}
+	free(ptserve->url);
+	free(ptserve);
+}
+
+void ptserve_listen(ptserve_t *ptserve) {
+	struct sockaddr_in sin;
+	socklen_t addrlen = sizeof(sin);
+
+	if(ptserve->sd_server >= 0) { return; } /* already listening */
+
+	memset(&sin, 0, sizeof(sin));
+	sin.sin_family = AF_INET;
+	sin.sin_addr.s_addr = htonl(INADDR_ANY);
+	sin.sin_port = htons(0);
+
+	ptserve->sd_server = socket(PF_INET, SOCK_STREAM, 0);
+	bind(ptserve->sd_server, (struct sockaddr*) &sin, sizeof(sin));
+	getsockname(ptserve->sd_server, (struct sockaddr*) &sin, &addrlen);
+
+	listen(ptserve->sd_server, SOMAXCONN);
+
+	ptserve->port = ntohs(sin.sin_port);
+	_asprintf(&(ptserve->url), "http://127.0.0.1:%d", ptserve->port);
+}
+
+int ptserve_accept(ptserve_t *ptserve) {
+	return accept(ptserve->sd_server, 0, 0);
+}
+
+void *ptserve_serve(ptserve_t *ptserve) {
+	int session_fd;
+	ptserve_listen(ptserve);
+	while((session_fd = ptserve_accept(ptserve)) >= 0) {
+		ptserve_message_t *msg = ptserve_message_new(ptserve, session_fd);
+		ptserve->response_cb(msg);
+		ptserve_message_free(msg);
+	}
+	return NULL;
+}
+
+/*****************************************************************************
+ * ptserve helpers
+ ****************************************************************************/
+
+void ptserve_send_file(int socket, int rootfd, const char *path) {
+	struct stat sbuf;
+	ssize_t nread;
+	char buf[128];
+	int fd = openat(rootfd, path, O_RDONLY);
+	fstat(fd, &sbuf);
+	_sendf(socket, "HTTP/1.1 200 OK\r\n");
+	_sendf(socket, "Content-Length: %zd\r\n", sbuf.st_size);
+	_sendf(socket, "\r\n");
+	while((nread = read(fd, buf, 128)) > 0 && _send(socket, buf, nread));
+	close(fd);
+}
+
+void ptserve_send_range(int socket, int rootfd, const char *path, off_t start, off_t len) {
+	struct stat sbuf;
+	ssize_t nread;
+	char buf[128];
+	int fd = openat(rootfd, path, O_RDONLY);
+	lseek(fd, start, SEEK_SET);
+	fstat(fd, &sbuf);
+	if(len > sbuf.st_size - start) { len = sbuf.st_size - start; }
+	_sendf(socket, "HTTP/1.1 200 OK\r\n");
+	_sendf(socket, "Content-Length: %zd\r\n", len);
+	_sendf(socket, "Content-Range: bytes %zd-%zd/%zd\r\n",
+			start, start + len, sbuf.st_size);
+	_sendf(socket, "\r\n");
+	while((nread = read(fd, buf, 128)) > 0 && _send(socket, buf, nread));
+	close(fd);
+}
+
+void ptserve_send_str(int socket, const char *body) {
+	size_t blen = strlen(body);
+	_sendf(socket, "HTTP/1.1 200 OK\r\n");
+	_sendf(socket, "Content-Length: %zd\r\n", blen);
+	_sendf(socket, "\r\n");
+	_send(socket, body, blen);
+}
+
+void ptserve_cb_dir(ptserve_message_t *request) {
+	char *c, *path = request->path;
+	const char *range_hdr;
+	/* strip protocol and location if present */
+	if((c = strstr(path, "://")) != NULL) {
+		path = c + 3;
+		if((c = strchr(path, '/')) != NULL) {
+			path = c + 1;
+		} else {
+			path = "/";
+		}
+	}
+	/* strip leading '/' */
+	if(path[0] == '/') { path++; }
+	if(range_hdr = ptserve_message_get_header(request, "Range")) {
+		off_t start = 0, len = 0;
+		sscanf(range_hdr, "Range: bytes=%li-%li", &start, &len);
+		ptserve_send_range(request->socket_fd, request->ptserve->rootfd, path, start, len);
+	} else {
+		ptserve_send_file(request->socket_fd, request->ptserve->rootfd, path);
+	}
+}
+
+ptserve_t *ptserve_serve_cbat(int fd, ptserve_response_cb_t *cb, void *data) {
+	ptserve_t *ptserve = ptserve_new();
+	if(ptserve == NULL) {
+		free(ptserve);
+		return NULL;
+	}
+	ptserve->rootfd = fd;
+	ptserve->response_cb = cb;
+	ptserve->data = data;
+	ptserve_serve(ptserve);
+	return ptserve;
+}
+
+ptserve_t *ptserve_serve_cb(ptserve_response_cb_t *cb, void *data) {
+	return ptserve_serve_cbat(AT_FDCWD, cb, data);
+}
+
+ptserve_t *ptserve_serve_dirat(int fd, const char *path) {
+	ptserve_t *ptserve = ptserve_new();
+	int rootfd = openat(fd, path, O_RDONLY | O_DIRECTORY);
+	if(ptserve == NULL || (ptserve->rootfd = rootfd) < 0) {
+		free(ptserve);
+		return NULL;
+	}
+	ptserve->response_cb = ptserve_cb_dir;
+	ptserve_listen(ptserve);
+	/* pthread_create(&ptserve->_tid, NULL, (void* (*)(void*)) ptserve_serve, ptserve); */
+	/* pthread_detach(ptserve->_tid); */
+	ptserve->_pid = fork();
+	if(ptserve->_pid == 0) {
+		ptserve_serve(ptserve);
+	}
+	return ptserve;
+}
+
+ptserve_t *ptserve_serve_dir(const char *path) {
+	return ptserve_serve_dirat(AT_FDCWD, path);
+}
+
+/*****************************************************************************
+ * tests
+ ****************************************************************************/
+
+void ptserve_set_proxy(ptserve_t *ptserve) {
+	setenv("http_proxy", ptserve->url, 1);
+}
+#if 0
+int main(int argc, char *argv[]) {
+	ptserve_t *ptserve = ptserve_serve_cbat(AT_FDCWD, ptserve_cb_dir, NULL);
+	ptserve_listen(ptserve);
+	printf("listening on port %d\n", ptserve->port);
+	ptserve_serve(ptserve);
+	return 0;
+}
+
+int main_nocb(int argc, char *argv[]) {
+	int fd;
+	ptserve_t *ptserve = ptserve_new();
+	ptserve_listen(ptserve);
+	printf("listening on port %d\n", ptserve->port);
+	while((fd = ptserve_accept(ptserve)) >= 0) {
+		ptserve_message_t *msg = ptserve_message_new(ptserve, fd);
+		ptserve_cb_dir(msg);
+		ptserve_message_free(msg);
+	}
+	ptserve_free(ptserve);
+}
+#endif
+
+#endif /* PTSERVE_C */
+
+/* vim: set ts=2 sw=2 noet: */
diff --git a/test/alpm/tap.c b/test/alpm/tap.c
new file mode 100644
index 00000000..dcff2add
--- /dev/null
+++ b/test/alpm/tap.c
@@ -0,0 +1,305 @@ 
+/*
+ * Copyright 2014-2015 Andrew Gregory <andrew.gregory.8@gmail.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ *
+ * Project URL: http://github.com/andrewgregory/tap.c
+ */
+
+#ifndef TAP_C
+#define TAP_C
+
+#include <inttypes.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static int _tap_tests_planned = 0;
+static int _tap_tests_run = 0;
+static int _tap_tests_failed = 0;
+static const char *_tap_todo = NULL;
+
+#define _tap_output stdout
+#define _tap_failure_output stderr
+#define _tap_todo_output stdout
+
+#ifndef TAP_EXIT_SUCCESS
+#define TAP_EXIT_SUCCESS EXIT_SUCCESS
+#endif
+
+#ifndef TAP_EXIT_FAILURE
+#define TAP_EXIT_FAILURE EXIT_FAILURE
+#endif
+
+#ifndef TAP_EXIT_ASSERT
+#define TAP_EXIT_ASSERT TAP_EXIT_FAILURE
+#endif
+
+void tap_plan(int test_count);
+void tap_skip_all(const char *reason, ...)
+    __attribute__ ((format (printf, 1, 2)));
+void tap_done_testing(void);
+int tap_finish(void);
+void tap_todo(const char *reason);
+void tap_skip(int count, const char *reason, ...)
+    __attribute__ ((format (printf, 2, 3)));
+void tap_bail(const char *reason, ...)
+    __attribute__ ((format (printf, 1, 2)));
+void tap_diag(const char *message, ...)
+    __attribute__ ((format (printf, 1, 2)));
+void tap_note(const char *message, ...)
+    __attribute__ ((format (printf, 1, 2)));
+
+int tap_get_testcount_planned(void);
+int tap_get_testcount_run(void);
+int tap_get_testcount_failed(void);
+const char *tap_get_todo(void);
+
+#define tap_assert(x) \
+    if(!(x)) { tap_bail("ASSERT FAILED: '%s'", #x); exit(TAP_EXIT_ASSERT); }
+
+#define tap_ok(...) _tap_ok(__FILE__, __LINE__, __VA_ARGS__)
+#define tap_vok(success, args) _tap_vok(__FILE__, __LINE__, success, args)
+#define tap_is_float(...) _tap_is_float(__FILE__, __LINE__, __VA_ARGS__)
+#define tap_is_int(...) _tap_is_int(__FILE__, __LINE__, __VA_ARGS__)
+#define tap_is_str(...) _tap_is_str(__FILE__, __LINE__, __VA_ARGS__)
+
+int _tap_ok(const char *file, int line, int success, const char *name, ...)
+    __attribute__ ((format (printf, 4, 5)));
+int _tap_vok(const char *file, int line,
+        int success, const char *name, va_list args)
+    __attribute__ ((format (printf, 4, 0)));
+int _tap_is_float(const char *file, int line,
+        double got, double expected, double delta, const char *name, ...)
+    __attribute__ ((format (printf, 6, 7)));
+int _tap_is_int(const char *file, int line,
+        intmax_t got, intmax_t expected, const char *name, ...)
+    __attribute__ ((format (printf, 5, 6)));
+int _tap_is_str(const char *file, int line,
+        const char *got, const char *expected, const char *name, ...)
+    __attribute__ ((format (printf, 5, 6)));
+
+#define TAP_VPRINT_MSG(stream, msg) if(msg) { \
+        va_list args; \
+        va_start(args, msg); \
+        fputc(' ', stream); \
+        vfprintf(stream, msg, args); \
+        va_end(args); \
+    }
+
+
+void tap_plan(int test_count)
+{
+    _tap_tests_planned = test_count;
+    fprintf(_tap_output, "1..%d\n", test_count);
+    fflush(_tap_output);
+}
+
+void tap_skip_all(const char *reason, ...)
+{
+    FILE *stream = _tap_output;
+    fputs("1..0 # SKIP", _tap_output);
+    TAP_VPRINT_MSG(stream, reason);
+    fputc('\n', _tap_output);
+    fflush(_tap_output);
+}
+
+void tap_done_testing(void)
+{
+    tap_plan(_tap_tests_run);
+}
+
+int tap_finish(void)
+{
+    if(_tap_tests_run != _tap_tests_planned) {
+        tap_diag("Looks like you planned %d tests but ran %d.",
+                _tap_tests_planned, _tap_tests_run);
+    }
+    return _tap_tests_run == _tap_tests_planned
+        && _tap_tests_failed == 0 ? TAP_EXIT_SUCCESS : TAP_EXIT_FAILURE;
+}
+
+int tap_get_testcount_planned(void)
+{
+    return _tap_tests_planned;
+}
+
+int tap_get_testcount_run(void)
+{
+    return _tap_tests_run;
+}
+
+int tap_get_testcount_failed(void)
+{
+    return _tap_tests_failed;
+}
+
+const char *tap_get_todo(void)
+{
+    return _tap_todo;
+}
+
+void tap_todo(const char *reason)
+{
+    _tap_todo = reason;
+}
+
+void tap_skip(int count, const char *reason, ...)
+{
+    FILE *stream = _tap_output;
+    while(count--) {
+        fprintf(_tap_output, "ok %d # SKIP", ++_tap_tests_run);
+        TAP_VPRINT_MSG(stream, reason);
+        fputc('\n', _tap_output);
+    }
+    fflush(_tap_output);
+}
+
+void tap_bail(const char *reason, ...)
+{
+    FILE *stream = _tap_output;
+    fputs("Bail out!", _tap_output);
+    TAP_VPRINT_MSG(stream, reason);
+    fputc('\n', _tap_output);
+    fflush(_tap_output);
+}
+
+void tap_diag(const char *message, ...)
+{
+    FILE *stream = _tap_todo ? _tap_todo_output : _tap_failure_output;
+
+    fputs("#", stream);
+    TAP_VPRINT_MSG(stream, message);
+    fputc('\n', stream);
+    fflush(stream);
+
+}
+
+void tap_note(const char *message, ...)
+{
+    FILE *stream = _tap_output;
+    fputs("#", _tap_output);
+    TAP_VPRINT_MSG(stream, message);
+    fputc('\n', _tap_output);
+    fflush(_tap_output);
+}
+
+int _tap_vok(const char *file, int line,
+        int success, const char *name, va_list args)
+{
+    const char *result;
+    if(success) {
+        result = "ok";
+        if(_tap_todo) ++_tap_tests_failed;
+    } else {
+        result = "not ok";
+        if(!_tap_todo) ++_tap_tests_failed;
+    }
+
+    fprintf(_tap_output, "%s %d", result, ++_tap_tests_run);
+
+    if(name) {
+        fputs(" - ", _tap_output);
+        vfprintf(_tap_output, name, args);
+    }
+
+    if(_tap_todo) {
+        fputs(" # TODO", _tap_output);
+        if(*_tap_todo) {
+            fputc(' ', _tap_output);
+            fputs(_tap_todo, _tap_output);
+        }
+    }
+
+    fputc('\n', _tap_output);
+    fflush(_tap_output);
+
+    if(!success && file) {
+        /* TODO add test name if available */
+        tap_diag("  Failed%s test at %s line %d.",
+                _tap_todo ? " (TODO)" : "", file, line);
+    }
+
+    return success;
+}
+
+#define TAP_OK(success, name) do { \
+        va_list args; \
+        va_start(args, name); \
+        _tap_vok(file, line, success, name, args); \
+        va_end(args); \
+    } while(0)
+
+int _tap_ok(const char *file, int line,
+        int success, const char *name, ...)
+{
+    TAP_OK(success, name);
+    return success;
+}
+
+int _tap_is_float(const char *file, int line,
+        double got, double expected, double delta, const char *name, ...)
+{
+    double diff = (expected > got ? expected - got : got - expected);
+    int match = diff < delta;
+    TAP_OK(match, name);
+    if(!match) {
+        tap_diag("         got: '%f'", got);
+        tap_diag("    expected: '%f'", expected);
+        tap_diag("       delta: '%f'", diff);
+        tap_diag("     allowed: '%f'", delta);
+    }
+    return match;
+}
+
+int _tap_is_int(const char *file, int line,
+        intmax_t got, intmax_t expected, const char *name, ...)
+{
+    int match = got == expected;
+    TAP_OK(match, name);
+    if(!match) {
+        tap_diag("         got: '%" PRIdMAX "'", got);
+        tap_diag("    expected: '%" PRIdMAX "'", expected);
+    }
+    return match;
+}
+
+int _tap_is_str(const char *file, int line,
+        const char *got, const char *expected, const char *name, ...)
+{
+    int match;
+    if(got && expected) {
+        match = (strcmp(got, expected) == 0);
+    } else {
+        match = (got == expected);
+    }
+    TAP_OK(match, name);
+    if(!match) {
+        tap_diag("         got: '%s'", got);
+        tap_diag("    expected: '%s'", expected);
+    }
+    return match;
+}
+
+#undef TAP_OK
+#undef TAP_VPRINT_MSG
+
+#endif /* TAP_C */
diff --git a/test/alpm/tests/.gitignore b/test/alpm/tests/.gitignore
new file mode 100644
index 00000000..d06c5240
--- /dev/null
+++ b/test/alpm/tests/.gitignore
@@ -0,0 +1,3 @@ 
+.deps/
+.libs/
+*.t
diff --git a/test/alpm/tests/Makefile.am b/test/alpm/tests/Makefile.am
new file mode 100644
index 00000000..a3724e3f
--- /dev/null
+++ b/test/alpm/tests/Makefile.am
@@ -0,0 +1,21 @@ 
+check_PROGRAMS = \
+	add_remove.t \
+	cached_part_file.t \
+	part_file_in_secondary_cache.t
+
+noinst_PROGRAMS = $(check_PROGRAMS)
+
+EXTRA_DIST = $(check_PROGRAMS) 
+
+AM_CPPFLAGS = \
+	-imacros $(top_builddir)/config.h \
+	-I$(top_srcdir)/lib/libalpm \
+	-DLOCALEDIR=\"@localedir@\"
+
+AM_CFLAGS = -g -D_GNU_SOURCE -pthread
+
+LDADD = $(LTLIBINTL) $(top_builddir)/lib/libalpm/.libs/libalpm.la
+
+%.t: %.c
+
+# vim:set noet:
diff --git a/test/alpm/tests/TESTS b/test/alpm/tests/TESTS
new file mode 100644
index 00000000..26b0fa51
--- /dev/null
+++ b/test/alpm/tests/TESTS
@@ -0,0 +1,3 @@ 
+TESTS += test/alpm/tests/add_remove.t
+TESTS += test/alpm/tests/cached_part_file.t
+TESTS += test/alpm/tests/part_file_in_secondary_cache.t
diff --git a/test/alpm/tests/add_remove.c b/test/alpm/tests/add_remove.c
new file mode 100644
index 00000000..13899570
--- /dev/null
+++ b/test/alpm/tests/add_remove.c
@@ -0,0 +1,56 @@ 
+#include "../alpmtest.h"
+
+/* install and them remove a package with a single handle */
+/* http://lists.archlinux.org/pipermail/pacman-dev/2015-February/019906.html */
+
+pt_env_t *pt = NULL;
+alpm_handle_t *handle = NULL;
+
+void cleanup(void)
+{
+	alpm_release(handle);
+	pt_cleanup(pt);
+}
+
+int main(void)
+{
+	pt_pkg_t *pkg;
+	alpm_pkg_t *lpkg;
+	alpm_handle_t *handle;
+	alpm_list_t *data = NULL;
+	const char *pkg_db_path = "var/lib/pacman/local/foo-1-1";
+
+	ASSERT(atexit(cleanup) == 0);
+
+	ASSERT(pt = pt_init(NULL));
+	ASSERT(pkg = pt_pkg_new(pt, "foo", "1-1"));
+	ASSERT(pt_pkg_writeat(pt->rootfd, "foo.pkg.tar", pkg) == 0);
+
+	ASSERT(handle = alpm_initialize(pt->root, pt->dbpath, NULL));
+	ASSERT(alpm_pkg_load(handle, pt_path(pt, "foo.pkg.tar"), 1, 0, &lpkg) == 0);
+
+	/* install the package */
+	ASSERT(alpm_trans_init(handle, 0) == 0);
+	ASSERT(alpm_add_pkg(handle, lpkg) == 0);
+	ASSERT(alpm_trans_prepare(handle, &data) == 0);
+	ASSERT(alpm_trans_commit(handle, &data) == 0);
+	ASSERT(alpm_trans_release(handle) == 0);
+	ASSERT(lpkg = alpm_db_get_pkg(alpm_get_localdb(handle), "foo"));
+	ASSERT(faccessat(pt->rootfd, pkg_db_path, F_OK, 0) == 0);
+
+	/* remove the package */
+	tap_plan(7);
+	tap_is_int(alpm_trans_init(handle, 0), 0, "alpm_trans_init");
+	tap_is_int(alpm_remove_pkg(handle, lpkg), 0, "alpm_remove_pkg");
+	tap_is_int(alpm_trans_prepare(handle, &data), 0, "alpm_trans_prepare");
+	tap_is_int(alpm_trans_commit(handle, &data), 0, "alpm_trans_commit");
+	tap_is_int(alpm_trans_release(handle), 0, "alpm_trans_release");
+
+	/* make sure the removal was actually performed */
+	tap_ok(alpm_db_get_pkg(alpm_get_localdb(handle), "foo") == NULL,
+			"foo removed from local cache");
+	tap_ok(faccessat(pt->rootfd, pkg_db_path, F_OK, 0) == -1
+			&& errno == ENOENT, "foo entry removed from db");
+
+	return tap_finish();
+}
diff --git a/test/alpm/tests/cached_part_file.c b/test/alpm/tests/cached_part_file.c
new file mode 100644
index 00000000..5811ebad
--- /dev/null
+++ b/test/alpm/tests/cached_part_file.c
@@ -0,0 +1,65 @@ 
+#include "../alpmtest.h"
+
+/* install a package with a completed .part file in the cache (FS#35789) */
+
+pt_env_t *pt = NULL;
+alpm_handle_t *handle = NULL;
+int file_downloaded = 0;
+
+void cleanup(void)
+{
+	alpm_release(handle);
+	pt_cleanup(pt);
+}
+
+void cb_dl_progress(const char *filename, off_t file_xfered, off_t file_total)
+{
+	file_downloaded = 1;
+}
+
+int main(void)
+{
+	pt_pkg_t *pkg;
+	pt_db_t *db;
+	alpm_pkg_t *lpkg;
+	alpm_db_t *adb;
+	alpm_list_t *data = NULL;
+
+	ASSERT(atexit(cleanup) == 0);
+
+	ASSERT(pt = pt_init(NULL));
+
+	ASSERT(pkg = pt_pkg_new(pt, "foo", "1-1"));
+	ASSERT(pkg->filename = strdup("foo.pkg.tar"));
+	ASSERT(pt_pkg_writeat(pt->rootfd, "tmp/foo.pkg.tar.part", pkg) == 0);
+
+	ASSERT(db = pt_db_new(pt, "sync"));
+	ASSERT(pt_db_add_pkg(db, pkg));
+	ASSERT(pt_install_db(pt, db) == 0);
+
+	ASSERT(handle = alpm_initialize(pt->root, pt->dbpath, NULL));
+	ASSERT(alpm_option_add_cachedir(handle, pt_path(pt, "tmp")) == 0);
+	ASSERT(adb = alpm_register_syncdb(handle, "sync", 0));
+	ASSERT(alpm_db_add_server(adb, "http://foo") == 0);
+	ASSERT(lpkg = alpm_db_get_pkg(adb, "foo"));
+
+	tap_plan(8);
+	tap_is_int(alpm_trans_init(handle, 0), 0, "alpm_trans_init");
+	tap_is_int(alpm_add_pkg(handle, lpkg), 0, "alpm_add_pkg");
+	tap_is_int(alpm_trans_prepare(handle, &data), 0, "alpm_trans_prepare");
+
+	tap_todo("don't fail on .part files");
+	tap_is_int(alpm_trans_commit(handle, &data), 0, "alpm_trans_commit");
+	tap_todo(NULL);
+
+	tap_is_int(alpm_trans_release(handle), 0, "alpm_trans_release");
+
+	tap_todo("don't fail on .part files");
+	tap_ok(alpm_db_get_pkg(alpm_get_localdb(handle), "foo") != NULL, "foo in local cache");
+	tap_ok(faccessat(pt->dbpathfd, "local/foo-1-1", F_OK, 0) == 0, "foo entry in local db");
+	tap_todo(NULL);
+
+	tap_ok(file_downloaded == 0, ".part file used");
+
+	return tap_finish();
+}
diff --git a/test/alpm/tests/part_file_in_secondary_cache.c b/test/alpm/tests/part_file_in_secondary_cache.c
new file mode 100644
index 00000000..cbf519a6
--- /dev/null
+++ b/test/alpm/tests/part_file_in_secondary_cache.c
@@ -0,0 +1,102 @@ 
+#include "../alpmtest.h"
+
+/* install a package with a partial .part file in a secondary cache */
+
+pt_env_t *pt = NULL;
+ptserve_t *ptserve = NULL;
+alpm_handle_t *handle = NULL;
+off_t total_download = 0;
+off_t actual_download = 0;
+
+void cleanup(void)
+{
+	alpm_release(handle);
+	pt_cleanup(pt);
+	ptserve_free(ptserve);
+}
+
+off_t getsize(int dirfd, const char *path)
+{
+	struct stat s;
+	return fstatat(dirfd, path, &s, AT_SYMLINK_NOFOLLOW) == 0 ? s.st_size : -1;
+}
+
+void cb_dl_progress(const char *filename, off_t file_xfered, off_t file_total)
+{
+	if(file_xfered == file_total) {
+		actual_download += file_xfered;
+	}
+}
+
+void cb_dl_total(off_t total)
+{
+	total_download += total;
+}
+
+int main(void)
+{
+	int fd;
+	const char *cpath = "secondary/foo.pkg.tar.part";
+	off_t csize;
+	pt_pkg_t *pkg;
+	pt_db_t *db;
+	alpm_pkg_t *lpkg;
+	alpm_db_t *adb;
+	alpm_list_t *data = NULL;
+
+	ASSERT(atexit(cleanup) == 0);
+
+	ASSERT(pt = pt_init(NULL));
+
+	ASSERT(pkg = pt_pkg_new(pt, "foo", "1-1"));
+	ASSERT(pkg->filename = strdup("foo.pkg.tar"));
+
+	/* write partial copy to our cache */
+	ASSERT(pt_pkg_writeat(pt->rootfd, cpath, pkg) == 0);
+	ASSERT((csize = getsize(pt->rootfd, cpath)) > 2);
+	ASSERT(pkg->csize = pt_asprintf("%lu", csize));
+	ASSERT((fd = openat(pt->rootfd, cpath, O_WRONLY)) >= 0);
+	ASSERT(ftruncate(fd, csize / 2) == 0);
+	ASSERT(close(fd) == 0);
+
+	/* write full copy to server */
+	ASSERT(pt_pkg_writeat(pt->rootfd, "srv/foo.pkg.tar", pkg) == 0);
+	ASSERT(ptserve = ptserve_serve_dirat(pt->rootfd, "srv/"));
+
+	ASSERT(db = pt_db_new(pt, "sync"));
+	ASSERT(pt_db_add_pkg(db, pkg));
+	ASSERT(pt_install_db(pt, db) == 0);
+
+	ASSERT(handle = alpm_initialize(pt->root, pt->dbpath, NULL));
+	ASSERT(alpm_option_add_cachedir(handle, pt_path(pt, "primary")) == 0);
+	ASSERT(alpm_option_add_cachedir(handle, pt_path(pt, "secondary")) == 0);
+	ASSERT(alpm_option_set_totaldlcb(handle, cb_dl_total) == 0);
+	ASSERT(alpm_option_set_dlcb(handle, cb_dl_progress) == 0);
+
+	ASSERT(adb = alpm_register_syncdb(handle, "sync", 0));
+	ASSERT(alpm_db_add_server(adb, ptserve->url) == 0);
+	ASSERT(lpkg = alpm_db_get_pkg(adb, "foo"));
+
+	tap_plan(10);
+	tap_is_int(alpm_trans_init(handle, 0), 0, "alpm_trans_init");
+	tap_is_int(alpm_add_pkg(handle, lpkg), 0, "alpm_add_pkg");
+	tap_is_int(alpm_trans_prepare(handle, &data), 0, "alpm_trans_prepare");
+	tap_is_int(alpm_trans_commit(handle, &data), 0, "alpm_trans_commit");
+	tap_is_int(alpm_trans_release(handle), 0, "alpm_trans_release");
+
+	tap_is_int(total_download, csize - csize / 2, "predicted download size");
+
+	tap_todo("use .part files in secondary caches");
+
+	tap_is_int(actual_download, csize - csize / 2, "actual download size");
+	tap_ok(faccessat(pt->rootfd, "secondary/foo.pkg.tar", F_OK, 0) == 0,
+			"downloaded to secondary");
+	tap_ok(faccessat(pt->rootfd, "secondary/foo.pkg.tar.part", F_OK, 0) == -1
+			&& errno == ENOENT, ".part file removed");
+	tap_ok(faccessat(pt->rootfd, "primary/foo.pkg.tar", F_OK, 0) == -1
+			&& errno == ENOENT, "package not downloaded to primary");
+
+	tap_todo(NULL);
+
+	return tap_finish();
+}