diff --git a/.gitignore b/.gitignore
index ab8670fcbfd6fa60aaf931dc38029b852e97271e..1868f0b877f2c74b8feb9b81edc2f29f8cfa0d6b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -60,4 +60,5 @@ libcapsule-*.txt
 *.prerequisites
 *.signals
 !/build-aux/git-version-gen
+!/build-aux/tap-test.sh
 !/m4/ax_is_release.m4
diff --git a/Makefile.am b/Makefile.am
index c5d39930a2337099fe2ac69c23d75a16e9417c47..c5370a1501d547a73537c0782d597577c9ff49c2 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -92,8 +92,7 @@ dist_pkgdata_DATA       = data/capsule-shim.h     \
                           data/capsule-shim.mk.in \
                           disabled.mk
 
-BUILT_SOURCES           = $(shim_srcs)
-BUILT_SOURCES          += $(top_srcdir)/.version
+BUILT_SOURCES           = $(top_srcdir)/.version
 
 $(top_srcdir)/.version:
 	$(AM_V_GEN)echo $(VERSION) > $@-t && mv $@-t $@
@@ -105,6 +104,127 @@ dist-hook-git-version-gen:
 
 include $(top_srcdir)/disabled.mk
 
+# ============================================================================
+# GNOME-style installed-tests
+
+insttestsdir                         = $(libexecdir)/installed-tests/$(PACKAGE_TARNAME)
+
+# /!\ Order is important here: the helper library must come first
+nobase_insttests_LTLIBRARIES         = tests/red/libhelper.la                  \
+                                       tests/green/libhelper.la                \
+                                       tests/lib/libhelper.la                  \
+                                       tests/red/libnotgl.la                   \
+                                       tests/green/libnotgl.la                 \
+                                       tests/lib/libnotgl.la                   \
+                                       tests/shim/libnotgl.la
+# Install the source code so we can rebuild it inside a container if we want to
+nobase_dist_insttests_DATA           = tests/libtest.sh                        \
+                                       tests/notgl.h                           \
+                                       tests/notgl-green.c                     \
+                                       tests/notgl-red.c                       \
+                                       tests/notgl-ref.c                       \
+                                       tests/notgl-helper.h                    \
+                                       tests/notgl-helper-green.c              \
+                                       tests/notgl-helper-red.c                \
+                                       tests/notgl-helper-ref.c                \
+                                       tests/shim/libnotgl.so.c.dlopen         \
+                                       tests/shim/libnotgl.so.c.dlsym          \
+                                       tests/shim/libnotgl.so.c.excluded       \
+                                       tests/shim/libnotgl.so.c.shared         \
+                                       tests/shim/libnotgl.so.c.symbols
+
+tests_lib_libnotgl_la_SOURCES        = tests/notgl-ref.c                       \
+                                       tests/notgl.h                           \
+                                       tests/notgl-helper.h
+tests_lib_libnotgl_la_LDFLAGS        = -shared -version-number 0:42:23
+tests_lib_libnotgl_la_LIBADD         = tests/lib/libhelper.la
+
+tests_red_libnotgl_la_SOURCES        = tests/notgl-red.c                       \
+                                       tests/notgl.h                           \
+                                       tests/notgl-helper.h
+# Give it a gratuitously higher version number
+tests_red_libnotgl_la_LDFLAGS        = -shared -version-number 0:123:456
+# We want the RUNPATH to point to tests/lib here, so link to that helper
+tests_red_libnotgl_la_LIBADD         = tests/lib/libhelper.la
+
+tests_green_libnotgl_la_SOURCES      = tests/notgl-green.c                     \
+                                       tests/notgl.h                           \
+                                       tests/notgl-helper.h
+# Give this one a gratuitously *lower* version number
+tests_green_libnotgl_la_LDFLAGS      = -shared -version-number 0:23:42
+# Again, we want the RUNPATH to point to tests/lib here, so link to that helper
+tests_green_libnotgl_la_LIBADD       = tests/lib/libhelper.la
+
+tests_lib_libhelper_la_SOURCES       = tests/notgl-helper-ref.c                \
+                                       tests/notgl-helper.h
+tests_lib_libhelper_la_LDFLAGS       = -shared -version-number 0:42:23
+
+tests_red_libhelper_la_SOURCES       = tests/notgl-helper-red.c                \
+                                       tests/notgl-helper.h
+tests_red_libhelper_la_LDFLAGS       = -shared -version-number 0:123:456
+
+tests_green_libhelper_la_SOURCES     = tests/notgl-helper-green.c              \
+                                       tests/notgl-helper.h
+tests_green_libhelper_la_LDFLAGS     = -shared -version-number 0:23:42
+
+nodist_tests_shim_libnotgl_la_SOURCES = tests/shim/libnotgl.so.c
+tests_shim_libnotgl_la_LIBADD        = libcapsule.la
+tests_shim_libnotgl_la_LDFLAGS       = -shared -version-number 0:0:0
+tests_shim_libnotgl_la_CFLAGS        = $(AM_CFLAGS) -I$(top_srcdir)/capsule
+
+tests/shim/lib%.so.c: tests/shim/lib%.so.c.excluded tests/shim/lib%.so.c.dlopen tests/shim/lib%.so.c.dlsym tests/shim/lib%.so.c.shared tests/shim/lib%.so.c.symbols data/capsule-mkstublib Makefile
+	$(AM_V_GEN)\
+	PKG_CONFIG_PATH=$(abs_builddir)/data \
+	V=$V \
+	$(top_srcdir)/data/capsule-mkstublib \
+		--dlopen-implementation=$(srcdir)/$@.dlopen \
+		--dlsym-implementation=$(srcdir)/$@.dlsym \
+		--symbols-from=$(srcdir)/$@.symbols \
+		--no-update-symbols \
+		--runtime-tree=/host \
+		$$(basename $@ .c).0 \
+		$(srcdir)/$@.excluded \
+		$(srcdir)/$@.shared \
+		$@
+
+LOG_DRIVER                           = env AM_TAP_AWK='$(AWK)'                 \
+                                       $(SHELL) $(top_srcdir)/build-aux/tap-driver.sh
+LOG_COMPILER                         = $(top_srcdir)/build-aux/tap-test.sh
+TEST_EXTENSIONS                      = .sh
+SH_LOG_DRIVER                        = $(LOG_DRIVER)
+SH_LOG_COMPILER                      = $(LOG_COMPILER)
+AM_TESTS_ENVIRONMENT                 = export CAPSULE_TESTS_UNINSTALLED=1;     \
+                                       export G_TEST_SRCDIR="$(abs_srcdir)";   \
+                                       export G_TEST_BUILDDIR="$(abs_builddir)";\
+                                       export G_DEBUG=gc-friendly;             \
+                                       export MALLOC_CHECK_=2;
+test_extra_programs                  = tests/notgl-user                        \
+                                       tests/notgl-helper-user
+tests_notgl_user_LDADD               = tests/lib/libnotgl.la
+tests_notgl_helper_user_LDADD        = tests/lib/libnotgl.la                   \
+                                       tests/lib/libhelper.la
+test_extra_scripts                   =
+
+test_programs                        =
+test_scripts                         = tests/notgl.sh
+nobase_insttests_PROGRAMS            = $(test_programs)                        \
+                                       $(test_extra_programs)
+nobase_dist_insttests_SCRIPTS        = $(test_scripts)                         \
+                                       $(test_extra_scripts)
+TESTS                                = $(test_programs)                        \
+                                       $(test_scripts)
+testmetadir                          = $(datadir)/installed-tests/$(PACKAGE_TARNAME)
+testmeta_DATA                        = $(patsubst %,%.test,$(test_programs) $(test_scripts))
+$(testmeta_DATA): tests/%.test: tests/% Makefile
+	@$(MKDIR_P) $(dir $@)
+	$(AM_V_GEN)set -e; \
+	(\
+		echo "[Test]"; \
+		echo "Type=session"; \
+		echo "Output=TAP"; \
+		echo "Exec=$(insttestsdir)/tests/$* --tap"; \
+	) > $@.tmp && mv $@.tmp $@
+
 # ============================================================================
 # gtk-doc configuration: see /usr/share/doc/gtk-doc-tools/examples/Makefile.am
 DOC_MODULE           = libcapsule
@@ -126,13 +246,12 @@ GTKDOC_CFLAGS        =
 GTKDOC_LIBS          =
 
 include $(top_srcdir)/gtk-doc.make
-# ============================================================================
-# CLEANFILES is unconditionally set by gtk-doc.make, have to do these after:
-CLEANFILES    += $(shim_files) $(shim_srcs)
-# ============================================================================
 # standalone man pages
 include  $(top_srcdir)/documentation.mk
 
-# This needs to come after we include gtk-doc.make, which unconditionally
-# defines it
+# These need to come after we include gtk-doc.make, which unconditionally
+# defines these variables
+CLEANFILES             += $(testmeta_DATA)
+CLEANFILES             += tests/shim/libnotgl.so.c
 EXTRA_DIST             += $(top_srcdir)/.version
+EXTRA_DIST             += build-aux/tap-test.sh
diff --git a/build-aux/tap-test.sh b/build-aux/tap-test.sh
new file mode 100755
index 0000000000000000000000000000000000000000..67858ebcf363b7eb701d259bed0074c383d52ac4
--- /dev/null
+++ b/build-aux/tap-test.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+# Wrapper to make GTest tests output TAP syntax, because Automake's test
+# drivers do not currently support passing the same command-line argument
+# to each test executable. All GTest tests produce TAP output if invoked
+# with the --tap option.
+#
+# Usage: "tap-test.sh test-foo --verbose ..." is equivalent to
+# "test-foo --tap --verbose ..."
+
+set -e
+t="$1"
+shift
+exec "$t" --tap "$@"
diff --git a/configure.ac b/configure.ac
index 69cfc5fc540a372853e50550470e31e128d1101a..875aa62d8b5fba6e27907f265a67d0a51a66747f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -22,6 +22,7 @@ AC_INIT([libcapsule],
         [], [], [https://gitlab.collabora.com/vivek/libcapsule])
 AC_CONFIG_AUX_DIR([build-aux])
 AC_CONFIG_MACRO_DIR([m4])
+AC_REQUIRE_AUX_FILE([tap-driver.sh])
 
 m4_pattern_forbid([^AX_], [Please install GNU autoconf-archive])
 
diff --git a/debian/control b/debian/control
index e73fd1acec2f95753aebf5e96f28886030ca7455..0cd4ffeb216af62345e6041bed3e4a186722ef9a 100644
--- a/debian/control
+++ b/debian/control
@@ -7,6 +7,7 @@ Build-Depends:
  autoconf,
  autoconf-archive (>= 20160916-1),
  automake,
+ bubblewrap,
  debhelper,
  dh-autoreconf,
  gtk-doc-tools,
@@ -49,6 +50,21 @@ Description:
  .
  This package contains the files needed to generate libcapsule proxy libraries.
 
+Package: libcapsule-tests
+Architecture: amd64 i386
+Section: misc
+Priority: optional
+Depends:
+ bubblewrap,
+ ${shlibs:Depends},
+Description:
+ libcapsule is a helper library used to implement segregated run-time
+ dynamic linking proxy libraries - used (for example) to load a library
+ that has dependencies incompatible with the regular libraries that are
+ part of the host OS, such as an incompatible libstdc++ version.
+ .
+ This package contains automated tests.
+
 Package: libcapsule0
 Architecture: amd64 i386
 Multi-Arch: same
diff --git a/debian/libcapsule-tests.install b/debian/libcapsule-tests.install
new file mode 100644
index 0000000000000000000000000000000000000000..142caa69c1d08d0ce9fe0a6a020d5ec42688a115
--- /dev/null
+++ b/debian/libcapsule-tests.install
@@ -0,0 +1,2 @@
+usr/lib/*/capsule/installed-tests/libcapsule
+usr/share/installed-tests/libcapsule
diff --git a/debian/rules b/debian/rules
index 1008db081d18eed745f695f76b4c614a763c0642..cab626a4f571068520d5e29a59d6bf76a8eea5da 100755
--- a/debian/rules
+++ b/debian/rules
@@ -12,6 +12,7 @@ override_dh_auto_configure:
 
 override_dh_install:
 	rm -f debian/tmp/usr/lib/*/*.la
+	find debian/tmp/usr/lib/*/capsule/installed-tests -name '*.la' -print -delete
 	dh_install --fail-missing
 
 override_dh_autoreconf:
diff --git a/debian/tests/control b/debian/tests/control
index bb7544f170938511b5c8076632f51f19f74440e9..2b70beb5c046e2cc81b0af7b534a3dee64eaddb5 100644
--- a/debian/tests/control
+++ b/debian/tests/control
@@ -2,3 +2,8 @@ Tests: smoke
 Depends:
  build-essential,
  libcapsule-dev,
+
+Tests: gnome-desktop-testing
+Depends:
+ gnome-desktop-testing,
+ libcapsule-tests,
diff --git a/debian/tests/gnome-desktop-testing b/debian/tests/gnome-desktop-testing
new file mode 100755
index 0000000000000000000000000000000000000000..2958be2dab55e801a2c51efa85e0fe4added2c6c
--- /dev/null
+++ b/debian/tests/gnome-desktop-testing
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+set -e
+exec 2>&1
+exec gnome-desktop-testing-runner libcapsule
diff --git a/tests/libtest.sh b/tests/libtest.sh
new file mode 100644
index 0000000000000000000000000000000000000000..f81601c5d3894a9dde7d8d5592a4565e28e1f453
--- /dev/null
+++ b/tests/libtest.sh
@@ -0,0 +1,119 @@
+# vim:set ft=sh sts=4 sw=4 et:
+
+# Copyright © 2017 Collabora Ltd
+
+# This file is part of libcapsule.
+
+# libcapsule is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+
+# libcapsule is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+
+# You should have received a copy of the GNU Lesser General Public
+# License along with libcapsule.  If not, see <http://www.gnu.org/licenses/>.
+
+test_tempdir="$(mktemp -d /tmp/libcapsule.XXXXXXXX)"
+trap 'rm -fr --one-file-system $test_tempdir' EXIT
+mkdir "$test_tempdir/host"
+export CAPSULE_PREFIX="$test_tempdir/host"
+
+test_num=0
+any_failed=0
+
+skip_all () {
+    echo "1..0 # SKIP - $*"
+    exit 0
+}
+
+pass () {
+    test_num=$((test_num + 1))
+    echo "ok $test_num - $*"
+}
+
+fail () {
+    test_num=$((test_num + 1))
+    echo "not ok $test_num - $*"
+    any_failed=1
+}
+
+skip () {
+    test_num=$((test_num + 1))
+    echo "ok $test_num # SKIP - $*"
+}
+
+ok () {
+    local condition="$1"
+    shift
+
+    if $condition; then
+        pass "$*"
+    else
+        fail "$*"
+    fi
+}
+
+is () {
+    local got="$1"
+    local expected="$2"
+    shift 2
+
+    if [ "x$got" = "x$expected" ]; then
+        pass "$* ($got)"
+    else
+        echo "# Got: $got"
+        echo "# Expected: $expected"
+        fail "$* ($got != $expected)"
+    fi
+}
+
+isnt () {
+    local got="$1"
+    local unexpected="$2"
+    shift 2
+
+    if [ "x$got" != "x$unexpected" ]; then
+        echo "# Got: $got"
+        echo "# Expected: anything but $unexpected"
+        fail "$* (expected anything but $unexpected)"
+    else
+        pass "$* ($got)"
+    fi
+}
+
+exec_is () {
+    local command="$1"
+    local expected_status="$2"
+    local expected="$3"
+    local status=0
+    shift 3
+
+    got="$(eval "$command")" || status="$?"
+
+    if [ "x$status" != "x$expected_status" ]; then
+        fail "$* (status $status != $expected_status)"
+    fi
+
+    if [ "x$got" = "x$expected" ]; then
+        pass "$* ($got)"
+    else
+        echo "# Got: $got"
+        echo "# Expected: $expected"
+        fail "$* ($got != $expected)"
+    fi
+}
+
+run_verbose () {
+    echo "# \$($*)..."
+    "$@"
+}
+
+done_testing () {
+    echo "# End of tests"
+    echo "1..$test_num"
+    exit $any_failed
+}
diff --git a/tests/notgl-green.c b/tests/notgl-green.c
new file mode 100644
index 0000000000000000000000000000000000000000..ec7d1bb1edb017370e0e0b82dde7754d04379a4c
--- /dev/null
+++ b/tests/notgl-green.c
@@ -0,0 +1,49 @@
+// Copyright © 2017 Collabora Ltd
+
+// notgl-green — one of two implementations of libnotgl, taking the
+// role of the NVIDIA implementation of OpenGL
+
+// This file is part of libcapsule.
+//
+// libcapsule is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// libcapsule is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with libcapsule.  If not, see <http://www.gnu.org/licenses/>.
+
+#include "notgl.h"
+
+#include <dlfcn.h>
+
+#include "notgl-helper.h"
+
+const char *
+notgl_get_implementation( void )
+{
+    return "green";
+}
+
+const char *
+notgl_extension_both( void )
+{
+    return "green implementation of common extension";
+}
+
+const char *
+notgl_extension_green( void )
+{
+    return "green-only extension";
+}
+
+const char *
+notgl_use_helper( void )
+{
+    return helper_get_implementation();
+}
diff --git a/tests/notgl-helper-green.c b/tests/notgl-helper-green.c
new file mode 100644
index 0000000000000000000000000000000000000000..0cfd2c99adb7d9319bb7ba8e0572f1d768c2322d
--- /dev/null
+++ b/tests/notgl-helper-green.c
@@ -0,0 +1,27 @@
+// Copyright © 2017 Collabora Ltd
+
+// notgl-helper-green — An implementation of libnotgl-helper in the
+// host, taking the role of the host's libstdc++
+
+// This file is part of libcapsule.
+//
+// libcapsule is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// libcapsule is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with libcapsule.  If not, see <http://www.gnu.org/licenses/>.
+
+#include "notgl-helper.h"
+
+const char *
+helper_get_implementation( void )
+{
+    return "host (green)";
+}
diff --git a/tests/notgl-helper-red.c b/tests/notgl-helper-red.c
new file mode 100644
index 0000000000000000000000000000000000000000..c0da1ee910ddb3bc9bbe3e0f70368bc2bd8aeef4
--- /dev/null
+++ b/tests/notgl-helper-red.c
@@ -0,0 +1,27 @@
+// Copyright © 2017 Collabora Ltd
+
+// notgl-helper-red — An implementation of libnotgl-helper in the
+// host, taking the role of the host's libstdc++
+
+// This file is part of libcapsule.
+//
+// libcapsule is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// libcapsule is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with libcapsule.  If not, see <http://www.gnu.org/licenses/>.
+
+#include "notgl-helper.h"
+
+const char *
+helper_get_implementation( void )
+{
+    return "host (red)";
+}
diff --git a/tests/notgl-helper-ref.c b/tests/notgl-helper-ref.c
new file mode 100644
index 0000000000000000000000000000000000000000..29557c6c31e1cb72a0d90643352657f2cec37f6e
--- /dev/null
+++ b/tests/notgl-helper-ref.c
@@ -0,0 +1,27 @@
+// Copyright © 2017 Collabora Ltd
+
+// notgl-helper-ref — The implementation of libnotgl-helper in the
+// container, taking the role of the container's libstdc++
+
+// This file is part of libcapsule.
+//
+// libcapsule is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// libcapsule is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with libcapsule.  If not, see <http://www.gnu.org/licenses/>.
+
+#include "notgl-helper.h"
+
+const char *
+helper_get_implementation( void )
+{
+    return "container (reference)";
+}
diff --git a/tests/notgl-helper-user.c b/tests/notgl-helper-user.c
new file mode 100644
index 0000000000000000000000000000000000000000..f6362b0dd376686b75a3f9025980c042c57d5b8e
--- /dev/null
+++ b/tests/notgl-helper-user.c
@@ -0,0 +1,64 @@
+// Copyright © 2017 Collabora Ltd
+
+// This file is part of libcapsule.
+//
+// libcapsule is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// libcapsule is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with libcapsule.  If not, see <http://www.gnu.org/licenses/>.
+
+#include <dlfcn.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "notgl.h"
+#include "notgl-helper.h"
+
+static notgl_extension_function
+get_extension( const char *name )
+{
+    return dlsym( RTLD_DEFAULT, name );
+}
+
+int
+main ( int argc,
+       char **argv )
+{
+    notgl_extension_function f;
+
+    printf( "NotGL implementation: %s\n", notgl_get_implementation() );
+    printf( "NotGL helper implementation: %s\n", notgl_use_helper() );
+
+    f = get_extension( "notgl_extension_both" );
+
+    if( f )
+        printf( "notgl_extension_both: %s\n", f() );
+    else
+        printf( "notgl_extension_both: (not found)\n" );
+
+    f = get_extension( "notgl_extension_red" );
+
+    if( f )
+        printf( "notgl_extension_red: %s\n", f() );
+    else
+        printf( "notgl_extension_red: (not found)\n" );
+
+    f = get_extension( "notgl_extension_green" );
+
+    if( f )
+        printf( "notgl_extension_green: %s\n", f() );
+    else
+        printf( "notgl_extension_green: (not found)\n" );
+
+    printf( "NotGL helper implementation as seen by executable: %s\n", helper_get_implementation () );
+
+    return 0;
+}
diff --git a/tests/notgl-helper.h b/tests/notgl-helper.h
new file mode 100644
index 0000000000000000000000000000000000000000..2c622dc457a45235c5abc02f336f80eba51dad31
--- /dev/null
+++ b/tests/notgl-helper.h
@@ -0,0 +1,26 @@
+// Copyright © 2017 Collabora Ltd
+
+// libnotgl-helper — A library used by the OpenGL driver, taking the
+// role of libstdc++
+
+// This file is part of libcapsule.
+//
+// libcapsule is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// libcapsule is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with libcapsule.  If not, see <http://www.gnu.org/licenses/>.
+
+#pragma once
+
+#define HELPER_PUBLIC __attribute__((visibility("default")))
+
+HELPER_PUBLIC
+const char *helper_get_implementation( void );
diff --git a/tests/notgl-red.c b/tests/notgl-red.c
new file mode 100644
index 0000000000000000000000000000000000000000..090f7187510ce3b4d8a337e1f4138b7bbd6dba1c
--- /dev/null
+++ b/tests/notgl-red.c
@@ -0,0 +1,49 @@
+// Copyright © 2017 Collabora Ltd
+
+// notgl-red — one of two implementations of libnotgl, taking the
+// role of the AMD implementation of OpenGL
+
+// This file is part of libcapsule.
+//
+// libcapsule is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// libcapsule is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with libcapsule.  If not, see <http://www.gnu.org/licenses/>.
+
+#include "notgl.h"
+
+#include <dlfcn.h>
+
+#include "notgl-helper.h"
+
+const char *
+notgl_get_implementation( void )
+{
+    return "red";
+}
+
+const char *
+notgl_extension_both( void )
+{
+    return "red implementation of common extension";
+}
+
+const char *
+notgl_extension_red( void )
+{
+    return "red-only extension";
+}
+
+const char *
+notgl_use_helper( void )
+{
+    return helper_get_implementation ();
+}
diff --git a/tests/notgl-ref.c b/tests/notgl-ref.c
new file mode 100644
index 0000000000000000000000000000000000000000..a1c7993bed9d2663caf6d6e7be26a19e79e7fc05
--- /dev/null
+++ b/tests/notgl-ref.c
@@ -0,0 +1,43 @@
+// Copyright © 2017 Collabora Ltd
+
+// notgl-ref — reference implementation of libnotgl, taking the
+// role of the (possibly outdated) implementation of OpenGL in the container
+
+// This file is part of libcapsule.
+//
+// libcapsule is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// libcapsule is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with libcapsule.  If not, see <http://www.gnu.org/licenses/>.
+
+#include "notgl.h"
+
+#include <dlfcn.h>
+
+#include "notgl-helper.h"
+
+const char *
+notgl_get_implementation( void )
+{
+    return "reference";
+}
+
+const char *
+notgl_extension_both( void )
+{
+    return "reference implementation of common extension";
+}
+
+const char *
+notgl_use_helper( void )
+{
+    return helper_get_implementation ();
+}
diff --git a/tests/notgl-user.c b/tests/notgl-user.c
new file mode 100644
index 0000000000000000000000000000000000000000..5abc05497f6fa253836d5e5d0e1b6c1f27414942
--- /dev/null
+++ b/tests/notgl-user.c
@@ -0,0 +1,61 @@
+// Copyright © 2017 Collabora Ltd
+
+// This file is part of libcapsule.
+//
+// libcapsule is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// libcapsule is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with libcapsule.  If not, see <http://www.gnu.org/licenses/>.
+
+#include <dlfcn.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "notgl.h"
+
+static notgl_extension_function
+get_extension( const char *name )
+{
+    return dlsym( RTLD_DEFAULT, name );
+}
+
+int
+main ( int argc,
+       char **argv )
+{
+    notgl_extension_function f;
+
+    printf( "NotGL implementation: %s\n", notgl_get_implementation() );
+    printf( "NotGL helper implementation: %s\n", notgl_use_helper() );
+
+    f = get_extension( "notgl_extension_both" );
+
+    if( f )
+        printf( "notgl_extension_both: %s\n", f() );
+    else
+        printf( "notgl_extension_both: (not found)\n" );
+
+    f = get_extension( "notgl_extension_red" );
+
+    if( f )
+        printf( "notgl_extension_red: %s\n", f() );
+    else
+        printf( "notgl_extension_red: (not found)\n" );
+
+    f = get_extension( "notgl_extension_green" );
+
+    if( f )
+        printf( "notgl_extension_green: %s\n", f() );
+    else
+        printf( "notgl_extension_green: (not found)\n" );
+
+    return 0;
+}
diff --git a/tests/notgl.h b/tests/notgl.h
new file mode 100644
index 0000000000000000000000000000000000000000..42b84d5f196ad66fd830b62ee48e03c50a5b8855
--- /dev/null
+++ b/tests/notgl.h
@@ -0,0 +1,35 @@
+// Copyright © 2017 Collabora Ltd
+
+// notgl — a library not entirely unlike OpenGL
+
+// This file is part of libcapsule.
+//
+// libcapsule is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// libcapsule is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with libcapsule.  If not, see <http://www.gnu.org/licenses/>.
+
+#pragma once
+
+#define NOTGL_PUBLIC __attribute__((visibility("default")))
+
+typedef const char *(*notgl_extension_function)( void );
+
+NOTGL_PUBLIC
+const char *notgl_get_implementation( void );
+NOTGL_PUBLIC
+const char *notgl_extension_both( void );
+NOTGL_PUBLIC
+const char *notgl_extension_red( void );
+NOTGL_PUBLIC
+const char *notgl_extension_green( void );
+NOTGL_PUBLIC
+const char *notgl_use_helper( void );
diff --git a/tests/notgl.sh b/tests/notgl.sh
new file mode 100755
index 0000000000000000000000000000000000000000..225772315c02ab2fa03c4b48c655c41f62864188
--- /dev/null
+++ b/tests/notgl.sh
@@ -0,0 +1,171 @@
+#!/bin/bash
+
+# Copyright © 2017 Collabora Ltd
+
+# This file is part of libcapsule.
+
+# libcapsule is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation; either version 2.1 of the
+# License, or (at your option) any later version.
+
+# libcapsule is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Lesser General Public License for more details.
+
+# You should have received a copy of the GNU Lesser General Public
+# License along with libcapsule.  If not, see <http://www.gnu.org/licenses/>.
+
+set -eu -o pipefail
+
+. "$(dirname "$0")"/libtest.sh
+
+if ! bwrap --ro-bind / / true; then
+    skip_all "1..0 # SKIP - cannot run bwrap"
+fi
+
+: ${G_TEST_SRCDIR:="$(cd "$(dirname "$0")"/..; pwd)"}
+: ${G_TEST_BUILDDIR:="$(cd "$(dirname "$0")"/..; pwd)"}
+
+echo "# Source or installation directory: $G_TEST_SRCDIR"
+echo "# Build or installation directory: $G_TEST_BUILDDIR"
+
+if [ -n "${CAPSULE_TESTS_UNINSTALLED:-}" ]; then
+    echo "# Running uninstalled: yes"
+    # We need to bypass libtool because it plays around with
+    # LD_LIBRARY_PATH, and so do we.
+    libs=/.libs
+    notgl_user="$("$G_TEST_BUILDDIR/libtool" --mode=execute ls -1 "$G_TEST_BUILDDIR/tests/notgl-user")"
+    notgl_helper_user="$("$G_TEST_BUILDDIR/libtool" --mode=execute ls -1 "$G_TEST_BUILDDIR/tests/notgl-helper-user")"
+else
+    echo "# Running uninstalled: no"
+    libs=
+    notgl_user="$G_TEST_BUILDDIR/tests/notgl-user"
+    notgl_helper_user="$G_TEST_BUILDDIR/tests/notgl-helper-user"
+fi
+
+echo
+echo "# Without special measures:"
+run_verbose env LD_LIBRARY_PATH="$G_TEST_BUILDDIR/tests/lib$libs" "$notgl_user" > "$test_tempdir/output"
+sed -e 's/^/#   /' "$test_tempdir/output"
+exec_is \
+    'sed -ne "s/^NotGL implementation: //p" $test_tempdir/output' \
+    0 reference
+exec_is \
+    'sed -ne "s/^NotGL helper implementation: //p" $test_tempdir/output' \
+    0 "container (reference)"
+exec_is \
+    'sed -ne "s/^notgl_extension_both: //p" $test_tempdir/output' \
+    0 "reference implementation of common extension"
+exec_is \
+    'sed -ne "s/^notgl_extension_red: //p" $test_tempdir/output' \
+    0 "(not found)"
+exec_is \
+    'sed -ne "s/^notgl_extension_green: //p" $test_tempdir/output' \
+    0 "(not found)"
+
+echo
+run_verbose env LD_LIBRARY_PATH="$G_TEST_BUILDDIR/tests/lib$libs" "$notgl_helper_user" > "$test_tempdir/output"
+sed -e 's/^/#   /' "$test_tempdir/output"
+exec_is \
+    'sed -ne "s/^NotGL implementation: //p" $test_tempdir/output' \
+    0 reference
+exec_is \
+    'sed -ne "s/^NotGL helper implementation: //p" $test_tempdir/output' \
+    0 "container (reference)"
+exec_is \
+    'sed -ne "s/^notgl_extension_both: //p" $test_tempdir/output' \
+    0 "reference implementation of common extension"
+exec_is \
+    'sed -ne "s/^notgl_extension_red: //p" $test_tempdir/output' \
+    0 "(not found)"
+exec_is \
+    'sed -ne "s/^NotGL helper implementation as seen by executable: //p" $test_tempdir/output' \
+    0 "container (reference)"
+
+echo
+echo "# With libcapsule:"
+# We mount the "host system" on $test_tempdir/host.
+#
+# In the "container", the shim libnotgl is picked up from tests/shim because
+# it was prepended to the LD_LIBRARY_PATH. The helper library used by
+# libnotgl, libhelper, is picked up from tests/lib as usual.
+#
+# In the "host system", there is a tmpfs over the build or installation
+# directory to avoid tests/shim being found *again*, and the "red"
+# implementations of libnotgl and libhelper is mounted over tests/lib.
+run_verbose bwrap \
+    --ro-bind / / \
+    --dev-bind /dev /dev \
+    --ro-bind / "$CAPSULE_PREFIX" \
+    --tmpfs "$CAPSULE_PREFIX$G_TEST_BUILDDIR" \
+    --ro-bind "$G_TEST_BUILDDIR/tests/red" "$CAPSULE_PREFIX$G_TEST_BUILDDIR/tests/lib" \
+    --setenv LD_LIBRARY_PATH "$G_TEST_BUILDDIR/tests/shim$libs:$G_TEST_BUILDDIR/tests/lib$libs" \
+    "$notgl_user" > "$test_tempdir/output"
+sed -e 's/^/#   /' "$test_tempdir/output"
+# Functions from libnotgl get dispatched through the shim to the "red"
+# implementation from the "host system". This mirrors functions from libGL
+# being dispatched through the shim to the AMD implementation of libGL.
+exec_is \
+    'sed -ne "s/^NotGL implementation: //p" $test_tempdir/output' \
+    0 red
+# When the "red" implementation of libnotgl calls functions from libhelper,
+# implementation from the "host system". This mirrors functions from
+# libstdc++ that are called by the host libGL ending up in the host libstdc++.
+exec_is \
+    'sed -ne "s/^NotGL helper implementation: //p" $test_tempdir/output' \
+    0 "host (red)"
+# We can dlsym() for an implemementation of an extension that is part of
+# the ABI of the shim and the reference implementation.
+exec_is \
+    'sed -ne "s/^notgl_extension_both: //p" $test_tempdir/output' \
+    0 "red implementation of common extension"
+# We can also dlsym() for an implemementation of an extension that is only
+# available in the "red" implementation.
+exec_is \
+    'sed -ne "s/^notgl_extension_red: //p" $test_tempdir/output' \
+    0 "red-only extension"
+exec_is \
+    'sed -ne "s/^notgl_extension_green: //p" $test_tempdir/output' \
+    0 "(not found)"
+
+echo
+# Similar to the above, but now the host system is using the "green"
+# implementation of libnotgl, mirroring the NVIDIA implementation of libGL.
+run_verbose bwrap \
+    --ro-bind / / \
+    --dev-bind /dev /dev \
+    --ro-bind / "$CAPSULE_PREFIX" \
+    --tmpfs "$CAPSULE_PREFIX$G_TEST_BUILDDIR" \
+    --ro-bind "$G_TEST_BUILDDIR/tests/green" "$CAPSULE_PREFIX$G_TEST_BUILDDIR/tests/lib" \
+    --setenv LD_LIBRARY_PATH "$G_TEST_BUILDDIR/tests/shim$libs:$G_TEST_BUILDDIR/tests/lib$libs" \
+    "$notgl_helper_user" > "$test_tempdir/output"
+sed -e 's/^/#   /' "$test_tempdir/output"
+exec_is \
+    'sed -ne "s/^NotGL implementation: //p" $test_tempdir/output' \
+    0 green
+exec_is \
+    'sed -ne "s/^NotGL helper implementation: //p" $test_tempdir/output' \
+    0 "host (green)"
+exec_is \
+    'sed -ne "s/^notgl_extension_both: //p" $test_tempdir/output' \
+    0 "green implementation of common extension"
+exec_is \
+    'sed -ne "s/^notgl_extension_red: //p" $test_tempdir/output' \
+    0 "(not found)"
+exec_is \
+    'sed -ne "s/^notgl_extension_green: //p" $test_tempdir/output' \
+    0 "green-only extension"
+# Also, this program is linked directly to libhelper, mirroring a program
+# that is linked directly to libstdc++ in the libGL case. It sees the
+# container's libhelper, not the host's - even though libnotgl sees the
+# host's libhelper when it looks up the same symbol. (This is the point
+# of libcapsule.)
+exec_is \
+    'sed -ne "s/^NotGL helper implementation as seen by executable: //p" $test_tempdir/output' \
+    0 "container (reference)"
+
+done_testing
+
+# vim:set sw=4 sts=4 et:
diff --git a/tests/shim/libnotgl.so.c.dlopen b/tests/shim/libnotgl.so.c.dlopen
new file mode 100644
index 0000000000000000000000000000000000000000..3fdfdf1fd56734a318e6c4f529fdf358b0c79c13
--- /dev/null
+++ b/tests/shim/libnotgl.so.c.dlopen
@@ -0,0 +1,11 @@
+static void *_dlopen (const char *filename, int flag)
+{
+    if( flag & RTLD_GLOBAL )
+    {
+        fprintf( stderr, "Warning: libcapsule dlopen wrapper cannot pass "
+                         "RTLD_GLOBAL to underlying dlmopen(%s...) call\n",
+                 filename );
+        flag = (flag & ~RTLD_GLOBAL) & 0xfffff;
+    }
+    return capsule_shim_dlopen( symbol_ns, prefix, exclude, filename, flag );
+}
diff --git a/tests/shim/libnotgl.so.c.dlsym b/tests/shim/libnotgl.so.c.dlsym
new file mode 100644
index 0000000000000000000000000000000000000000..62d13bae5be190ceb857dc88e7d562b5f5b73f19
--- /dev/null
+++ b/tests/shim/libnotgl.so.c.dlsym
@@ -0,0 +1,12 @@
+// This allows symbols inside the capsule to be found by dlopen calls from
+// outside the capsule iff they are in one of the exported DSOs.
+//
+// This is useful in libGL shims as libGL has an ‘interesting’ history
+// of symbols appearing and disappearing so its users often do a
+// bizarre dlopen()/dlsym() dance instead of referring to a symbol
+// directly (and we may be missing those symbols from our static
+// export list even if the target libGL has them)
+static void *_dlsym (void *handle, const char *symbol)
+{
+    return capsule_shim_dlsym( dso, handle, symbol, valid_dlsym_sources );
+}
diff --git a/tests/shim/libnotgl.so.c.excluded b/tests/shim/libnotgl.so.c.excluded
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/shim/libnotgl.so.c.shared b/tests/shim/libnotgl.so.c.shared
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/shim/libnotgl.so.c.symbols b/tests/shim/libnotgl.so.c.symbols
new file mode 100644
index 0000000000000000000000000000000000000000..338681e486b56f67aff9b164aa9fb254081217e3
--- /dev/null
+++ b/tests/shim/libnotgl.so.c.symbols
@@ -0,0 +1,2 @@
+notgl_get_implementation
+notgl_use_helper