Expose basic api for testing external plugins.

We want external plugins to be able to make use of the core testing utils.
This commit exposes the basic utilities which are currently in use in
bst-external plugins. If necessary, more utilities could be exposed in the
future.

Moves the following files from tests/testutils/ to
buildstream/plugintestingutils/:
o runcli.py
o integration.py

As part of this, this commit makes the following changes to runcli.py
and integration.py:
o runcli.py: Fix linting errors
o runcli.py: Add user facing documentation
o Integration.py: Add user facing documentation
diff --git a/buildstream/plugintestutils/__init__.py b/buildstream/plugintestutils/__init__.py
new file mode 100644
index 0000000..c7238a2
--- /dev/null
+++ b/buildstream/plugintestutils/__init__.py
@@ -0,0 +1,30 @@
+#
+#  Copyright (C) 2019 Codethink Limited
+#  Copyright (C) 2019 Bloomberg Finance LP
+#
+#  This program 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 of the License, or (at your option) any later version.
+#
+#  This library 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 this library. If not, see <http://www.gnu.org/licenses/>.
+
+
+from .runcli import cli, cli_integration
+
+# To make use of these test utilities it is necessary to have pytest
+# available. However, we don't want to have a hard dependency on
+# pytest.
+try:
+    import pytest
+except ImportError:
+    module_name = globals()['__name__']
+    msg = "Could not import pytest:\n" \
+          "To use the {} module, you must have pytest installed.".format(module_name)
+    raise ImportError(msg)
diff --git a/buildstream/plugintestutils/integration.py b/buildstream/plugintestutils/integration.py
new file mode 100644
index 0000000..e29f480
--- /dev/null
+++ b/buildstream/plugintestutils/integration.py
@@ -0,0 +1,51 @@
+#
+#  Copyright (C) 2017 Codethink Limited
+#  Copyright (C) 2018 Bloomberg Finance LP
+#
+#  This program 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 of the License, or (at your option) any later version.
+#
+#  This library 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 this library. If not, see <http://www.gnu.org/licenses/>.
+"""
+Integration - tools for inspecting the output of plugin integration tests
+=========================================================================
+
+This module contains utilities for inspecting the artifacts produced during
+integration tests.
+"""
+
+import os
+
+
+# Return a list of files relative to the given directory
+def walk_dir(root):
+    for dirname, dirnames, filenames in os.walk(root):
+        # ensure consistent traversal order, needed for consistent
+        # handling of symlinks.
+        dirnames.sort()
+        filenames.sort()
+
+        # print path to all subdirectories first.
+        for subdirname in dirnames:
+            yield os.path.join(dirname, subdirname)[len(root):]
+
+        # print path to all filenames.
+        for filename in filenames:
+            yield os.path.join(dirname, filename)[len(root):]
+
+
+# Ensure that a directory contains the given filenames.
+def assert_contains(directory, expected):
+    missing = set(expected)
+    missing.difference_update(walk_dir(directory))
+    if missing:
+        raise AssertionError("Missing {} expected elements from list: {}"
+                             .format(len(missing), missing))
diff --git a/tests/testutils/runcli.py b/buildstream/plugintestutils/runcli.py
similarity index 92%
rename from tests/testutils/runcli.py
rename to buildstream/plugintestutils/runcli.py
index b051dec..fb7c23c 100644
--- a/tests/testutils/runcli.py
+++ b/buildstream/plugintestutils/runcli.py
@@ -1,3 +1,34 @@
+#
+#  Copyright (C) 2017 Codethink Limited
+#  Copyright (C) 2018 Bloomberg Finance LP
+#
+#  This program 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 of the License, or (at your option) any later version.
+#
+#  This library 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 this library. If not, see <http://www.gnu.org/licenses/>.
+"""
+runcli - Test fixtures used for running BuildStream commands
+============================================================
+
+:function:'cli' Use result = cli.run([arg1, arg2]) to run buildstream commands
+
+:function:'cli_integration' A variant of the main fixture that keeps persistent
+                            artifact and source caches. It also does not use
+                            the click test runner to avoid deadlock issues when
+                            running `bst shell`, but unfortunately cannot produce
+                            nice stacktraces.
+
+"""
+
+
 import os
 import re
 import sys
@@ -5,7 +36,6 @@
 import tempfile
 import itertools
 import traceback
-import subprocess
 from contextlib import contextmanager, ExitStack
 from ruamel import yaml
 import pytest
@@ -333,7 +363,7 @@
 
         return result
 
-    def invoke(self, cli, args=None, color=False, binary_capture=False, **extra):
+    def invoke(self, cli_object, args=None, color=False, binary_capture=False, **extra):
         exc_info = None
         exception = None
         exit_code = 0
@@ -348,7 +378,7 @@
             capture.start_capturing()
 
             try:
-                cli.main(args=args or (), prog_name=cli.name, **extra)
+                cli_object.main(args=args or (), prog_name=cli_object.name, **extra)
             except SystemExit as e:
                 if e.code != 0:
                     exception = e
@@ -361,7 +391,7 @@
                     sys.stdout.write(str(exit_code))
                     sys.stdout.write('\n')
                     exit_code = 1
-            except Exception as e:
+            except Exception as e:  # pylint: disable=broad-except
                 exception = e
                 exit_code = -1
                 exc_info = sys.exc_info()
diff --git a/tests/artifactcache/cache_size.py b/tests/artifactcache/cache_size.py
index 63ab9ad..88f8ead 100644
--- a/tests/artifactcache/cache_size.py
+++ b/tests/artifactcache/cache_size.py
@@ -5,8 +5,9 @@
 from buildstream import _yaml
 from buildstream._artifactcache import CACHE_SIZE_FILE
 from buildstream._exceptions import ErrorDomain
+from buildstream.plugintestutils import cli
 
-from tests.testutils import cli, create_element_size
+from tests.testutils import create_element_size
 
 # XXX: Currently lacking:
 #      * A way to check whether it's faster to read cache size on
diff --git a/tests/artifactcache/config.py b/tests/artifactcache/config.py
index fecb3dd..fda3097 100644
--- a/tests/artifactcache/config.py
+++ b/tests/artifactcache/config.py
@@ -10,7 +10,7 @@
 from buildstream import _yaml
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
 
-from tests.testutils.runcli import cli
+from buildstream.plugintestutils.runcli import cli
 
 
 DATA_DIR = os.path.dirname(os.path.realpath(__file__))
diff --git a/tests/artifactcache/expiry.py b/tests/artifactcache/expiry.py
index 2cc59e0..d92e68f 100644
--- a/tests/artifactcache/expiry.py
+++ b/tests/artifactcache/expiry.py
@@ -25,8 +25,9 @@
 
 from buildstream import _yaml
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
+from buildstream.plugintestutils import cli
 
-from tests.testutils import cli, create_element_size, update_element_size, wait_for_cache_granularity
+from tests.testutils import create_element_size, update_element_size, wait_for_cache_granularity
 
 
 DATA_DIR = os.path.join(
diff --git a/tests/artifactcache/junctions.py b/tests/artifactcache/junctions.py
index c6d6921..d5de162 100644
--- a/tests/artifactcache/junctions.py
+++ b/tests/artifactcache/junctions.py
@@ -1,9 +1,11 @@
 import os
 import shutil
 import pytest
-from tests.testutils import cli, create_artifact_share
 
 from buildstream import _yaml
+from buildstream.plugintestutils import cli
+
+from tests.testutils import create_artifact_share
 
 
 DATA_DIR = os.path.join(
diff --git a/tests/artifactcache/pull.py b/tests/artifactcache/pull.py
index 4c332bf..edd5a93 100644
--- a/tests/artifactcache/pull.py
+++ b/tests/artifactcache/pull.py
@@ -9,8 +9,9 @@
 from buildstream._context import Context
 from buildstream._project import Project
 from buildstream._protos.build.bazel.remote.execution.v2 import remote_execution_pb2
+from buildstream.plugintestutils import cli
 
-from tests.testutils import cli, create_artifact_share
+from tests.testutils import create_artifact_share
 
 
 # Project directory
diff --git a/tests/artifactcache/push.py b/tests/artifactcache/push.py
index 116fa78..ed2a140 100644
--- a/tests/artifactcache/push.py
+++ b/tests/artifactcache/push.py
@@ -10,8 +10,8 @@
 from buildstream._project import Project
 from buildstream._protos.build.bazel.remote.execution.v2 import remote_execution_pb2
 from buildstream.storage._casbaseddirectory import CasBasedDirectory
-
-from tests.testutils import cli, create_artifact_share
+from buildstream.plugintestutils import cli
+from tests.testutils import create_artifact_share
 
 
 # Project directory
diff --git a/tests/cachekey/cachekey.py b/tests/cachekey/cachekey.py
index 761ff0c..4a68968 100644
--- a/tests/cachekey/cachekey.py
+++ b/tests/cachekey/cachekey.py
@@ -35,7 +35,7 @@
 # run over to the corresponding .expected source files and commit
 # the result.
 #
-from tests.testutils.runcli import cli
+from buildstream.plugintestutils.runcli import cli
 from tests.testutils.site import HAVE_BZR, HAVE_GIT, HAVE_OSTREE, IS_LINUX, MACHINE_ARCH
 from buildstream.plugin import CoreWarnings
 from buildstream import _yaml
diff --git a/tests/cachekey/update.py b/tests/cachekey/update.py
index d574d07..3e09249 100755
--- a/tests/cachekey/update.py
+++ b/tests/cachekey/update.py
@@ -13,7 +13,7 @@
 #
 import os
 import tempfile
-from tests.testutils.runcli import Cli
+from buildstream.plugintestutils.runcli import Cli
 
 # This weird try / except is needed, because this will be imported differently
 # when pytest runner imports them vs when you run the updater directly from
diff --git a/tests/elements/filter.py b/tests/elements/filter.py
index d40a8bd..c0ef256 100644
--- a/tests/elements/filter.py
+++ b/tests/elements/filter.py
@@ -1,7 +1,8 @@
 import os
 import pytest
 import shutil
-from tests.testutils import cli, create_repo, ALL_REPO_KINDS
+from tests.testutils import create_repo, ALL_REPO_KINDS
+from buildstream.plugintestutils import cli
 from buildstream._exceptions import ErrorDomain
 from buildstream import _yaml
 
diff --git a/tests/examples/autotools.py b/tests/examples/autotools.py
index 30f5076..96827ff 100644
--- a/tests/examples/autotools.py
+++ b/tests/examples/autotools.py
@@ -1,8 +1,8 @@
 import os
 import pytest
 
-from tests.testutils import cli_integration as cli
-from tests.testutils.integration import assert_contains
+from buildstream.plugintestutils import cli_integration as cli
+from buildstream.plugintestutils.integration import assert_contains
 from tests.testutils.site import HAVE_BWRAP, IS_LINUX, MACHINE_ARCH
 
 pytestmark = pytest.mark.integration
diff --git a/tests/examples/developing.py b/tests/examples/developing.py
index 166fcf3..3ef78fd 100644
--- a/tests/examples/developing.py
+++ b/tests/examples/developing.py
@@ -2,8 +2,8 @@
 import pytest
 
 import tests.testutils.patch as patch
-from tests.testutils import cli_integration as cli
-from tests.testutils.integration import assert_contains
+from buildstream.plugintestutils import cli_integration as cli
+from buildstream.plugintestutils.integration import assert_contains
 from tests.testutils.site import HAVE_BWRAP, IS_LINUX, MACHINE_ARCH
 
 pytestmark = pytest.mark.integration
diff --git a/tests/examples/first-project.py b/tests/examples/first-project.py
index 821d2c1..3583977 100644
--- a/tests/examples/first-project.py
+++ b/tests/examples/first-project.py
@@ -1,8 +1,8 @@
 import os
 import pytest
 
-from tests.testutils import cli_integration as cli
-from tests.testutils.integration import assert_contains
+from buildstream.plugintestutils import cli_integration as cli
+from buildstream.plugintestutils.integration import assert_contains
 from tests.testutils.site import IS_LINUX
 
 
diff --git a/tests/examples/flatpak-autotools.py b/tests/examples/flatpak-autotools.py
index 4153a95..454b8d0 100644
--- a/tests/examples/flatpak-autotools.py
+++ b/tests/examples/flatpak-autotools.py
@@ -1,8 +1,8 @@
 import os
 import pytest
 
-from tests.testutils import cli_integration as cli
-from tests.testutils.integration import assert_contains
+from buildstream.plugintestutils import cli_integration as cli
+from buildstream.plugintestutils.integration import assert_contains
 from tests.testutils.site import HAVE_OSTREE, IS_LINUX, MACHINE_ARCH
 
 
diff --git a/tests/examples/integration-commands.py b/tests/examples/integration-commands.py
index 71e3830..abc64d9 100644
--- a/tests/examples/integration-commands.py
+++ b/tests/examples/integration-commands.py
@@ -1,8 +1,8 @@
 import os
 import pytest
 
-from tests.testutils import cli_integration as cli
-from tests.testutils.integration import assert_contains
+from buildstream.plugintestutils import cli_integration as cli
+from buildstream.plugintestutils.integration import assert_contains
 from tests.testutils.site import HAVE_BWRAP, IS_LINUX, MACHINE_ARCH
 
 
diff --git a/tests/examples/junctions.py b/tests/examples/junctions.py
index 753fa2d..3992b15 100644
--- a/tests/examples/junctions.py
+++ b/tests/examples/junctions.py
@@ -1,8 +1,8 @@
 import os
 import pytest
 
-from tests.testutils import cli_integration as cli
-from tests.testutils.integration import assert_contains
+from buildstream.plugintestutils import cli_integration as cli
+from buildstream.plugintestutils.integration import assert_contains
 from tests.testutils.site import IS_LINUX, HAVE_BWRAP, MACHINE_ARCH
 
 pytestmark = pytest.mark.integration
diff --git a/tests/examples/running-commands.py b/tests/examples/running-commands.py
index 61e23fd..6290204 100644
--- a/tests/examples/running-commands.py
+++ b/tests/examples/running-commands.py
@@ -1,8 +1,8 @@
 import os
 import pytest
 
-from tests.testutils import cli_integration as cli
-from tests.testutils.integration import assert_contains
+from buildstream.plugintestutils import cli_integration as cli
+from buildstream.plugintestutils.integration import assert_contains
 from tests.testutils.site import IS_LINUX, HAVE_BWRAP, MACHINE_ARCH
 
 
diff --git a/tests/format/assertion.py b/tests/format/assertion.py
index 42f69b7..b787e07 100644
--- a/tests/format/assertion.py
+++ b/tests/format/assertion.py
@@ -1,7 +1,7 @@
 import os
 import pytest
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
-from tests.testutils.runcli import cli
+from buildstream.plugintestutils.runcli import cli
 
 # Project directory
 DATA_DIR = os.path.join(
diff --git a/tests/format/dependencies.py b/tests/format/dependencies.py
index a6b417a..5513077 100644
--- a/tests/format/dependencies.py
+++ b/tests/format/dependencies.py
@@ -2,7 +2,7 @@
 import pytest
 
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
-from tests.testutils import cli
+from buildstream.plugintestutils import cli
 
 DATA_DIR = os.path.dirname(os.path.realpath(__file__))
 
diff --git a/tests/format/include.py b/tests/format/include.py
index 36e723e..1db3708 100644
--- a/tests/format/include.py
+++ b/tests/format/include.py
@@ -2,7 +2,9 @@
 import pytest
 from buildstream import _yaml
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
-from tests.testutils import cli, generate_junction, create_repo
+from buildstream.plugintestutils import cli
+
+from tests.testutils import generate_junction, create_repo
 
 
 # Project directory
diff --git a/tests/format/junctions.py b/tests/format/junctions.py
index 90608d0..7f27b59 100644
--- a/tests/format/junctions.py
+++ b/tests/format/junctions.py
@@ -4,7 +4,8 @@
 
 from buildstream import _yaml, ElementError
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
-from tests.testutils import cli, create_repo
+from buildstream.plugintestutils import cli
+from tests.testutils import create_repo
 from tests.testutils.site import HAVE_GIT
 
 
diff --git a/tests/format/listdirectiveerrors.py b/tests/format/listdirectiveerrors.py
index 4a2de86..8c39725 100644
--- a/tests/format/listdirectiveerrors.py
+++ b/tests/format/listdirectiveerrors.py
@@ -1,7 +1,7 @@
 import os
 import pytest
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
-from tests.testutils.runcli import cli
+from buildstream.plugintestutils.runcli import cli
 
 # Project directory
 DATA_DIR = os.path.dirname(os.path.realpath(__file__))
diff --git a/tests/format/optionarch.py b/tests/format/optionarch.py
index 09f9c07..a123700 100644
--- a/tests/format/optionarch.py
+++ b/tests/format/optionarch.py
@@ -3,7 +3,7 @@
 from contextlib import contextmanager
 from buildstream import _yaml
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
-from tests.testutils.runcli import cli
+from buildstream.plugintestutils.runcli import cli
 
 # Project directory
 DATA_DIR = os.path.dirname(os.path.realpath(__file__))
diff --git a/tests/format/optionbool.py b/tests/format/optionbool.py
index f02f0ee..598496c 100644
--- a/tests/format/optionbool.py
+++ b/tests/format/optionbool.py
@@ -2,7 +2,7 @@
 import pytest
 from buildstream import _yaml
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
-from tests.testutils.runcli import cli
+from buildstream.plugintestutils.runcli import cli
 
 # Project directory
 DATA_DIR = os.path.dirname(os.path.realpath(__file__))
diff --git a/tests/format/optioneltmask.py b/tests/format/optioneltmask.py
index b49eee8..7cbbd88 100644
--- a/tests/format/optioneltmask.py
+++ b/tests/format/optioneltmask.py
@@ -2,7 +2,7 @@
 import pytest
 from buildstream import _yaml
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
-from tests.testutils.runcli import cli
+from buildstream.plugintestutils.runcli import cli
 
 # Project directory
 DATA_DIR = os.path.dirname(os.path.realpath(__file__))
diff --git a/tests/format/optionenum.py b/tests/format/optionenum.py
index 6990ad9..936126d 100644
--- a/tests/format/optionenum.py
+++ b/tests/format/optionenum.py
@@ -2,7 +2,7 @@
 import pytest
 from buildstream import _yaml
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
-from tests.testutils.runcli import cli
+from buildstream.plugintestutils.runcli import cli
 
 # Project directory
 DATA_DIR = os.path.dirname(os.path.realpath(__file__))
diff --git a/tests/format/optionexports.py b/tests/format/optionexports.py
index 26eadc6..5e65e97 100644
--- a/tests/format/optionexports.py
+++ b/tests/format/optionexports.py
@@ -1,7 +1,7 @@
 import os
 import pytest
 from buildstream import _yaml
-from tests.testutils.runcli import cli
+from buildstream.plugintestutils.runcli import cli
 
 # Project directory
 DATA_DIR = os.path.dirname(os.path.realpath(__file__))
diff --git a/tests/format/optionflags.py b/tests/format/optionflags.py
index 3585f5e..341bb8c 100644
--- a/tests/format/optionflags.py
+++ b/tests/format/optionflags.py
@@ -2,7 +2,7 @@
 import pytest
 from buildstream import _yaml
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
-from tests.testutils.runcli import cli
+from buildstream.plugintestutils.runcli import cli
 
 # Project directory
 DATA_DIR = os.path.dirname(os.path.realpath(__file__))
diff --git a/tests/format/optionos.py b/tests/format/optionos.py
index b05d7e4..ba6ab2a 100644
--- a/tests/format/optionos.py
+++ b/tests/format/optionos.py
@@ -4,7 +4,7 @@
 
 from buildstream import _yaml
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
-from tests.testutils.runcli import cli
+from buildstream.plugintestutils.runcli import cli
 
 DATA_DIR = os.path.dirname(os.path.realpath(__file__))
 
diff --git a/tests/format/optionoverrides.py b/tests/format/optionoverrides.py
index e5c37b3..256562e 100644
--- a/tests/format/optionoverrides.py
+++ b/tests/format/optionoverrides.py
@@ -1,7 +1,7 @@
 import os
 import pytest
 from buildstream import _yaml
-from tests.testutils.runcli import cli
+from buildstream.plugintestutils.runcli import cli
 
 # Project directory
 DATA_DIR = os.path.dirname(os.path.realpath(__file__))
diff --git a/tests/format/options.py b/tests/format/options.py
index 2076106..3a1b128 100644
--- a/tests/format/options.py
+++ b/tests/format/options.py
@@ -2,7 +2,7 @@
 import pytest
 from buildstream import _yaml
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
-from tests.testutils.runcli import cli
+from buildstream.plugintestutils.runcli import cli
 
 # Project directory
 DATA_DIR = os.path.join(
diff --git a/tests/format/project.py b/tests/format/project.py
index 0433df9..db66044 100644
--- a/tests/format/project.py
+++ b/tests/format/project.py
@@ -2,7 +2,9 @@
 import pytest
 from buildstream import _yaml
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
-from tests.testutils import cli, filetypegenerator
+from buildstream.plugintestutils import cli
+
+from tests.testutils import filetypegenerator
 
 
 # Project directory
diff --git a/tests/format/projectoverrides.py b/tests/format/projectoverrides.py
index d63b0a2..cf9a1d3 100644
--- a/tests/format/projectoverrides.py
+++ b/tests/format/projectoverrides.py
@@ -2,7 +2,7 @@
 import os
 import pytest
 from buildstream import _yaml
-from tests.testutils.runcli import cli
+from buildstream.plugintestutils.runcli import cli
 
 # Project directory
 DATA_DIR = os.path.join(
diff --git a/tests/format/variables.py b/tests/format/variables.py
index be7faef..5a0d212 100644
--- a/tests/format/variables.py
+++ b/tests/format/variables.py
@@ -3,7 +3,7 @@
 import sys
 from buildstream import _yaml
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
-from tests.testutils.runcli import cli
+from buildstream.plugintestutils.runcli import cli
 
 
 # Project directory
diff --git a/tests/frontend/buildcheckout.py b/tests/frontend/buildcheckout.py
index b35b148..80d710f 100644
--- a/tests/frontend/buildcheckout.py
+++ b/tests/frontend/buildcheckout.py
@@ -3,9 +3,10 @@
 import hashlib
 import pytest
 import subprocess
-from tests.testutils import cli, create_repo, ALL_REPO_KINDS, generate_junction
 from tests.testutils.site import IS_WINDOWS
+from tests.testutils import create_repo, ALL_REPO_KINDS, generate_junction
 
+from buildstream.plugintestutils import cli
 from buildstream import _yaml
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
 
diff --git a/tests/frontend/buildtrack.py b/tests/frontend/buildtrack.py
index 720ab7e..9c56fb4 100644
--- a/tests/frontend/buildtrack.py
+++ b/tests/frontend/buildtrack.py
@@ -4,9 +4,11 @@
 import itertools
 
 import pytest
-from tests.testutils import cli, create_repo
+
+from tests.testutils import create_repo
 
 from buildstream import _yaml
+from buildstream.plugintestutils import cli
 from buildstream._exceptions import ErrorDomain
 
 from . import configure_project
diff --git a/tests/frontend/completions.py b/tests/frontend/completions.py
index 93b908f..cb151c2 100644
--- a/tests/frontend/completions.py
+++ b/tests/frontend/completions.py
@@ -1,6 +1,6 @@
 import os
 import pytest
-from tests.testutils import cli
+from buildstream.plugintestutils import cli
 
 # Project directory
 DATA_DIR = os.path.join(
diff --git a/tests/frontend/compose_splits.py b/tests/frontend/compose_splits.py
index 97558b6..3d2db44 100644
--- a/tests/frontend/compose_splits.py
+++ b/tests/frontend/compose_splits.py
@@ -1,6 +1,6 @@
 import os
 import pytest
-from tests.testutils.runcli import cli
+from buildstream.plugintestutils.runcli import cli
 
 # Project directory
 DATA_DIR = os.path.join(
diff --git a/tests/frontend/configurable_warnings.py b/tests/frontend/configurable_warnings.py
index e8a7b6a..0428745 100644
--- a/tests/frontend/configurable_warnings.py
+++ b/tests/frontend/configurable_warnings.py
@@ -4,7 +4,7 @@
 from buildstream.plugin import CoreWarnings
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
 from buildstream import _yaml
-from tests.testutils.runcli import cli
+from buildstream.plugintestutils.runcli import cli
 
 TOP_DIR = os.path.join(
     os.path.dirname(os.path.realpath(__file__)),
diff --git a/tests/frontend/cross_junction_workspace.py b/tests/frontend/cross_junction_workspace.py
index a10eb74..837a09f 100644
--- a/tests/frontend/cross_junction_workspace.py
+++ b/tests/frontend/cross_junction_workspace.py
@@ -1,7 +1,9 @@
 import os
-from tests.testutils import cli, create_repo
+from buildstream.plugintestutils import cli
 from buildstream import _yaml
 
+from tests.testutils import create_repo
+
 
 def prepare_junction_project(cli, tmpdir):
     main_project = tmpdir.join("main")
diff --git a/tests/frontend/fetch.py b/tests/frontend/fetch.py
index 24d9a36..9edfad9 100644
--- a/tests/frontend/fetch.py
+++ b/tests/frontend/fetch.py
@@ -1,7 +1,9 @@
 import os
 import pytest
-from tests.testutils import cli, create_repo, ALL_REPO_KINDS, generate_junction
 
+from tests.testutils import create_repo, ALL_REPO_KINDS, generate_junction
+
+from buildstream.plugintestutils import cli
 from buildstream import _yaml
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
 
diff --git a/tests/frontend/help.py b/tests/frontend/help.py
index 331eb43..540a3a6 100644
--- a/tests/frontend/help.py
+++ b/tests/frontend/help.py
@@ -1,5 +1,5 @@
 import pytest
-from tests.testutils.runcli import cli
+from buildstream.plugintestutils.runcli import cli
 
 
 def assert_help(cli_output):
diff --git a/tests/frontend/init.py b/tests/frontend/init.py
index f6e7dd6..b323a01 100644
--- a/tests/frontend/init.py
+++ b/tests/frontend/init.py
@@ -1,6 +1,6 @@
 import os
 import pytest
-from tests.testutils import cli
+from buildstream.plugintestutils import cli
 
 from buildstream import _yaml
 from buildstream._frontend.app import App
diff --git a/tests/frontend/logging.py b/tests/frontend/logging.py
index ddaadfa..17d925c 100644
--- a/tests/frontend/logging.py
+++ b/tests/frontend/logging.py
@@ -1,10 +1,11 @@
 import os
 import pytest
 import re
-from tests.testutils import cli, create_repo, ALL_REPO_KINDS
+from tests.testutils import create_repo, ALL_REPO_KINDS
 
 from buildstream import _yaml
 from buildstream._exceptions import ErrorDomain
+from buildstream.plugintestutils import cli
 
 # Project directory
 DATA_DIR = os.path.join(
diff --git a/tests/frontend/mirror.py b/tests/frontend/mirror.py
index 3cadd15..c0c2e70 100644
--- a/tests/frontend/mirror.py
+++ b/tests/frontend/mirror.py
@@ -1,10 +1,11 @@
 import os
 import pytest
 
-from tests.testutils import cli, create_repo, ALL_REPO_KINDS, generate_junction
+from tests.testutils import create_repo, ALL_REPO_KINDS, generate_junction
 
 from buildstream import _yaml
 from buildstream._exceptions import ErrorDomain
+from buildstream.plugintestutils import cli
 
 # Project directory
 TOP_DIR = os.path.dirname(os.path.realpath(__file__))
diff --git a/tests/frontend/order.py b/tests/frontend/order.py
index 97ebc2b..422fce2 100644
--- a/tests/frontend/order.py
+++ b/tests/frontend/order.py
@@ -1,8 +1,8 @@
 import os
 
 import pytest
-from tests.testutils import cli, create_repo
-
+from tests.testutils import create_repo
+from buildstream.plugintestutils import cli
 from buildstream import _yaml
 
 # Project directory
diff --git a/tests/frontend/overlaps.py b/tests/frontend/overlaps.py
index 27be8de..22365e4 100644
--- a/tests/frontend/overlaps.py
+++ b/tests/frontend/overlaps.py
@@ -1,6 +1,6 @@
 import os
 import pytest
-from tests.testutils.runcli import cli
+from buildstream.plugintestutils.runcli import cli
 from buildstream._exceptions import ErrorDomain
 from buildstream import _yaml
 from buildstream.plugin import CoreWarnings
diff --git a/tests/frontend/pull.py b/tests/frontend/pull.py
index a2c710f..9579d9f 100644
--- a/tests/frontend/pull.py
+++ b/tests/frontend/pull.py
@@ -1,7 +1,8 @@
 import os
 import shutil
 import pytest
-from tests.testutils import cli, create_artifact_share, generate_junction
+from buildstream.plugintestutils import cli
+from tests.testutils import create_artifact_share, generate_junction
 
 
 # Project directory
diff --git a/tests/frontend/push.py b/tests/frontend/push.py
index 729df9a..7a4cb3c 100644
--- a/tests/frontend/push.py
+++ b/tests/frontend/push.py
@@ -24,8 +24,8 @@
 import pytest
 
 from buildstream._exceptions import ErrorDomain
-from tests.testutils import cli, create_artifact_share, create_element_size
-from tests.testutils import generate_junction, wait_for_cache_granularity
+from buildstream.plugintestutils import cli
+from tests.testutils import create_artifact_share, create_element_size, generate_junction, wait_for_cache_granularity
 from . import configure_project
 
 
diff --git a/tests/frontend/rebuild.py b/tests/frontend/rebuild.py
index d93aac0..6a46e85 100644
--- a/tests/frontend/rebuild.py
+++ b/tests/frontend/rebuild.py
@@ -1,6 +1,6 @@
 import os
 import pytest
-from tests.testutils import cli
+from buildstream.plugintestutils import cli
 
 # Project directory
 DATA_DIR = os.path.join(
diff --git a/tests/frontend/show.py b/tests/frontend/show.py
index 88f38dd..ad3ae35 100644
--- a/tests/frontend/show.py
+++ b/tests/frontend/show.py
@@ -3,7 +3,8 @@
 import shutil
 import itertools
 import pytest
-from tests.testutils import cli, generate_junction
+from tests.testutils import generate_junction
+from buildstream.plugintestutils import cli
 from buildstream import _yaml
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
 
diff --git a/tests/frontend/source_checkout.py b/tests/frontend/source_checkout.py
index d7ff86d..b41d324 100644
--- a/tests/frontend/source_checkout.py
+++ b/tests/frontend/source_checkout.py
@@ -3,7 +3,7 @@
 import tarfile
 from pathlib import Path
 
-from tests.testutils import cli
+from buildstream.plugintestutils import cli
 
 from buildstream import utils, _yaml
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
diff --git a/tests/frontend/track.py b/tests/frontend/track.py
index a4ace92..486a412 100644
--- a/tests/frontend/track.py
+++ b/tests/frontend/track.py
@@ -1,8 +1,9 @@
 import stat
 import os
 import pytest
-from tests.testutils import cli, create_repo, ALL_REPO_KINDS, generate_junction
+from tests.testutils import create_repo, ALL_REPO_KINDS, generate_junction
 
+from buildstream.plugintestutils import cli
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
 from buildstream import _yaml
 
diff --git a/tests/frontend/track_cross_junction.py b/tests/frontend/track_cross_junction.py
index 4bbf2db..7d9d468 100644
--- a/tests/frontend/track_cross_junction.py
+++ b/tests/frontend/track_cross_junction.py
@@ -1,6 +1,8 @@
 import os
 import pytest
-from tests.testutils import cli, create_repo, ALL_REPO_KINDS, generate_junction
+from tests.testutils import create_repo, ALL_REPO_KINDS, generate_junction
+
+from buildstream.plugintestutils import cli
 from buildstream import _yaml
 
 
diff --git a/tests/frontend/version.py b/tests/frontend/version.py
index 997eb23..1d36a33 100644
--- a/tests/frontend/version.py
+++ b/tests/frontend/version.py
@@ -1,4 +1,4 @@
-from tests.testutils.runcli import cli
+from buildstream.plugintestutils.runcli import cli
 
 # For utils.get_bst_version()
 from buildstream import utils
diff --git a/tests/frontend/workspace.py b/tests/frontend/workspace.py
index 6d4270e..f6d12e8 100644
--- a/tests/frontend/workspace.py
+++ b/tests/frontend/workspace.py
@@ -30,9 +30,10 @@
 import shutil
 import subprocess
 from ruamel.yaml.comments import CommentedSet
-from tests.testutils import cli, create_repo, ALL_REPO_KINDS, wait_for_cache_granularity
+from tests.testutils import create_repo, ALL_REPO_KINDS, wait_for_cache_granularity
 from tests.testutils import create_artifact_share, create_element_size
 
+from buildstream.plugintestutils import cli
 from buildstream import _yaml
 from buildstream._exceptions import ErrorDomain, LoadError, LoadErrorReason
 from buildstream._workspaces import BST_WORKSPACE_FORMAT_VERSION
diff --git a/tests/frontend/yamlcache.py b/tests/frontend/yamlcache.py
index 2038859..99b5d71 100644
--- a/tests/frontend/yamlcache.py
+++ b/tests/frontend/yamlcache.py
@@ -4,7 +4,8 @@
 import tempfile
 from ruamel import yaml
 
-from tests.testutils import cli, generate_junction, create_element_size, create_repo
+from tests.testutils import generate_junction, create_element_size, create_repo
+from buildstream.plugintestutils import cli
 from buildstream import _yaml
 from buildstream._yamlcache import YamlCache
 from buildstream._project import Project
diff --git a/tests/integration/artifact.py b/tests/integration/artifact.py
index 2e12e71..4592412 100644
--- a/tests/integration/artifact.py
+++ b/tests/integration/artifact.py
@@ -21,7 +21,7 @@
 import os
 import pytest
 
-from tests.testutils import cli_integration as cli
+from buildstream.plugintestutils import cli_integration as cli
 
 
 pytestmark = pytest.mark.integration
diff --git a/tests/integration/autotools.py b/tests/integration/autotools.py
index b1d33f4..3ed0533 100644
--- a/tests/integration/autotools.py
+++ b/tests/integration/autotools.py
@@ -1,8 +1,8 @@
 import os
 import pytest
 
-from tests.testutils import cli_integration as cli
-from tests.testutils.integration import assert_contains
+from buildstream.plugintestutils import cli_integration as cli
+from buildstream.plugintestutils.integration import assert_contains
 from tests.testutils.site import HAVE_SANDBOX
 
 
diff --git a/tests/integration/build-tree.py b/tests/integration/build-tree.py
index bacbf1c..98bb5b1 100644
--- a/tests/integration/build-tree.py
+++ b/tests/integration/build-tree.py
@@ -2,8 +2,9 @@
 import pytest
 import shutil
 
-from tests.testutils import cli, cli_integration, create_artifact_share
+from tests.testutils import create_artifact_share
 from tests.testutils.site import HAVE_SANDBOX
+from buildstream.plugintestutils import cli, cli_integration
 from buildstream._exceptions import ErrorDomain
 
 
diff --git a/tests/integration/build-uid.py b/tests/integration/build-uid.py
index 06779ee..88b887b 100644
--- a/tests/integration/build-uid.py
+++ b/tests/integration/build-uid.py
@@ -3,8 +3,8 @@
 
 from buildstream import _yaml
 
-from tests.testutils import cli_integration as cli
-from tests.testutils.integration import assert_contains
+from buildstream.plugintestutils import cli_integration as cli
+from buildstream.plugintestutils.integration import assert_contains
 from tests.testutils.site import HAVE_BWRAP, IS_LINUX, HAVE_SANDBOX
 
 
diff --git a/tests/integration/cachedfail.py b/tests/integration/cachedfail.py
index 08dbf81..5335ff5 100644
--- a/tests/integration/cachedfail.py
+++ b/tests/integration/cachedfail.py
@@ -3,10 +3,10 @@
 
 from buildstream import _yaml
 from buildstream._exceptions import ErrorDomain
+from buildstream.plugintestutils import cli_integration as cli
 
 from conftest import clean_platform_cache
-
-from tests.testutils import cli_integration as cli, create_artifact_share
+from tests.testutils import create_artifact_share
 from tests.testutils.site import HAVE_BWRAP, IS_LINUX, HAVE_SANDBOX
 
 
diff --git a/tests/integration/cmake.py b/tests/integration/cmake.py
index 7d03e29..51e2156 100644
--- a/tests/integration/cmake.py
+++ b/tests/integration/cmake.py
@@ -1,8 +1,8 @@
 import os
 import pytest
 
-from tests.testutils import cli_integration as cli
-from tests.testutils.integration import assert_contains
+from buildstream.plugintestutils import cli_integration as cli
+from buildstream.plugintestutils.integration import assert_contains
 from tests.testutils.site import HAVE_SANDBOX
 
 
diff --git a/tests/integration/compose-symlinks.py b/tests/integration/compose-symlinks.py
index 2599d8b..c6027bf 100644
--- a/tests/integration/compose-symlinks.py
+++ b/tests/integration/compose-symlinks.py
@@ -5,8 +5,8 @@
 
 from buildstream import _yaml
 
-from tests.testutils import cli_integration as cli
-from tests.testutils.integration import walk_dir
+from buildstream.plugintestutils import cli_integration as cli
+from buildstream.plugintestutils.integration import walk_dir
 
 
 pytestmark = pytest.mark.integration
diff --git a/tests/integration/compose.py b/tests/integration/compose.py
index 077d5ee..386e084 100644
--- a/tests/integration/compose.py
+++ b/tests/integration/compose.py
@@ -5,8 +5,8 @@
 
 from buildstream import _yaml
 
-from tests.testutils import cli_integration as cli
-from tests.testutils.integration import walk_dir
+from buildstream.plugintestutils import cli_integration as cli
+from buildstream.plugintestutils.integration import walk_dir
 from tests.testutils.site import HAVE_SANDBOX
 
 
diff --git a/tests/integration/import.py b/tests/integration/import.py
index 8426405..deef66b 100644
--- a/tests/integration/import.py
+++ b/tests/integration/import.py
@@ -3,8 +3,8 @@
 
 from buildstream import _yaml
 
-from tests.testutils import cli_integration as cli
-from tests.testutils.integration import walk_dir
+from buildstream.plugintestutils import cli_integration as cli
+from buildstream.plugintestutils.integration import walk_dir
 
 
 pytestmark = pytest.mark.integration
diff --git a/tests/integration/make.py b/tests/integration/make.py
index 12c027b..a76fe9a 100644
--- a/tests/integration/make.py
+++ b/tests/integration/make.py
@@ -1,8 +1,8 @@
 import os
 import pytest
 
-from tests.testutils import cli_integration as cli
-from tests.testutils.integration import assert_contains
+from buildstream.plugintestutils import cli_integration as cli
+from buildstream.plugintestutils.integration import assert_contains
 from tests.testutils.site import HAVE_SANDBOX
 
 
diff --git a/tests/integration/manual.py b/tests/integration/manual.py
index b9f09e2..97cbec5 100644
--- a/tests/integration/manual.py
+++ b/tests/integration/manual.py
@@ -3,7 +3,7 @@
 
 from buildstream import _yaml
 
-from tests.testutils import cli_integration as cli
+from buildstream.plugintestutils import cli_integration as cli
 from tests.testutils.site import HAVE_SANDBOX
 
 
diff --git a/tests/integration/messages.py b/tests/integration/messages.py
index 03c9224..7f4194e 100644
--- a/tests/integration/messages.py
+++ b/tests/integration/messages.py
@@ -22,8 +22,7 @@
 
 from buildstream import _yaml
 from buildstream._exceptions import ErrorDomain
-
-from tests.testutils import cli_integration as cli
+from buildstream.plugintestutils import cli_integration as cli
 from tests.testutils.site import HAVE_SANDBOX
 
 
diff --git a/tests/integration/pip_element.py b/tests/integration/pip_element.py
index 8071236..48ca13b 100644
--- a/tests/integration/pip_element.py
+++ b/tests/integration/pip_element.py
@@ -4,8 +4,8 @@
 
 from buildstream import _yaml
 
-from tests.testutils import cli_integration as cli
-from tests.testutils.integration import assert_contains
+from buildstream.plugintestutils import cli_integration as cli
+from buildstream.plugintestutils.integration import assert_contains
 from tests.testutils.site import HAVE_SANDBOX
 
 
diff --git a/tests/integration/pip_source.py b/tests/integration/pip_source.py
index d43b380..993b3a1 100644
--- a/tests/integration/pip_source.py
+++ b/tests/integration/pip_source.py
@@ -3,9 +3,9 @@
 
 from buildstream import _yaml
 
-from tests.testutils import cli_integration as cli
+from buildstream.plugintestutils import cli_integration as cli
+from buildstream.plugintestutils.integration import assert_contains
 from tests.testutils.python_repo import setup_pypi_repo
-from tests.testutils.integration import assert_contains
 from tests.testutils.site import HAVE_SANDBOX
 
 
diff --git a/tests/integration/pullbuildtrees.py b/tests/integration/pullbuildtrees.py
index ca6c40f..24fac7e 100644
--- a/tests/integration/pullbuildtrees.py
+++ b/tests/integration/pullbuildtrees.py
@@ -2,9 +2,12 @@
 import shutil
 import pytest
 
-from tests.testutils import cli, cli_integration as cli2, create_artifact_share
-from tests.testutils.integration import assert_contains
+from tests.testutils import create_artifact_share
 from tests.testutils.site import HAVE_SANDBOX
+
+from buildstream.plugintestutils.integration import assert_contains
+from buildstream.plugintestutils import cli, cli_integration as cli2
+from buildstream.plugintestutils.integration import assert_contains
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
 
 
diff --git a/tests/integration/sandbox-bwrap.py b/tests/integration/sandbox-bwrap.py
index 66e9f5b..2c939e9 100644
--- a/tests/integration/sandbox-bwrap.py
+++ b/tests/integration/sandbox-bwrap.py
@@ -3,8 +3,8 @@
 
 from buildstream._exceptions import ErrorDomain
 
-from tests.testutils import cli_integration as cli
-from tests.testutils.integration import assert_contains
+from buildstream.plugintestutils import cli_integration as cli
+from buildstream.plugintestutils.integration import assert_contains
 from tests.testutils.site import HAVE_BWRAP, HAVE_BWRAP_JSON_STATUS
 
 
diff --git a/tests/integration/script.py b/tests/integration/script.py
index 446a63f..0297a3f 100644
--- a/tests/integration/script.py
+++ b/tests/integration/script.py
@@ -2,8 +2,7 @@
 import pytest
 
 from buildstream import _yaml
-
-from tests.testutils import cli_integration as cli
+from buildstream.plugintestutils import cli_integration as cli
 from tests.testutils.site import HAVE_SANDBOX
 
 
diff --git a/tests/integration/shell.py b/tests/integration/shell.py
index 9b35db8..d1a551f 100644
--- a/tests/integration/shell.py
+++ b/tests/integration/shell.py
@@ -3,8 +3,7 @@
 
 from buildstream import _yaml
 from buildstream._exceptions import ErrorDomain
-
-from tests.testutils import cli_integration as cli
+from buildstream.plugintestutils import cli_integration as cli
 from tests.testutils.site import HAVE_SANDBOX
 
 
diff --git a/tests/integration/sockets.py b/tests/integration/sockets.py
index 30cf622..fc2fb2b 100644
--- a/tests/integration/sockets.py
+++ b/tests/integration/sockets.py
@@ -2,9 +2,8 @@
 import pytest
 
 from buildstream import _yaml
-
-from tests.testutils import cli_integration as cli
-from tests.testutils.integration import assert_contains
+from buildstream.plugintestutils import cli_integration as cli
+from buildstream.plugintestutils.integration import assert_contains
 from tests.testutils.site import HAVE_SANDBOX
 
 
diff --git a/tests/integration/source-determinism.py b/tests/integration/source-determinism.py
index 6be6a4d..f7b2bf2 100644
--- a/tests/integration/source-determinism.py
+++ b/tests/integration/source-determinism.py
@@ -2,8 +2,8 @@
 import pytest
 
 from buildstream import _yaml, utils
+from buildstream.plugintestutils import cli_integration as cli
 from tests.testutils import create_repo, ALL_REPO_KINDS
-from tests.testutils import cli_integration as cli
 from tests.testutils.site import HAVE_SANDBOX
 
 
diff --git a/tests/integration/stack.py b/tests/integration/stack.py
index 1b8d381..d5208d7 100644
--- a/tests/integration/stack.py
+++ b/tests/integration/stack.py
@@ -2,8 +2,7 @@
 import pytest
 
 from buildstream import _yaml
-
-from tests.testutils import cli_integration as cli
+from buildstream.plugintestutils import cli_integration as cli
 from tests.testutils.site import HAVE_SANDBOX
 
 
diff --git a/tests/integration/symlinks.py b/tests/integration/symlinks.py
index c45c811..22ff527 100644
--- a/tests/integration/symlinks.py
+++ b/tests/integration/symlinks.py
@@ -4,8 +4,8 @@
 
 from buildstream import _yaml
 
-from tests.testutils import cli_integration as cli
-from tests.testutils.integration import assert_contains
+from buildstream.plugintestutils import cli_integration as cli
+from buildstream.plugintestutils.integration import assert_contains
 from tests.testutils.site import HAVE_SANDBOX
 
 
diff --git a/tests/integration/workspace.py b/tests/integration/workspace.py
index ee1d575..f1d8d6f 100644
--- a/tests/integration/workspace.py
+++ b/tests/integration/workspace.py
@@ -2,9 +2,9 @@
 import pytest
 
 from buildstream import _yaml
-from tests.testutils import cli_integration as cli
+from buildstream.plugintestutils import cli_integration as cli
 from tests.testutils.site import HAVE_SANDBOX
-from tests.testutils.integration import walk_dir
+from buildstream.plugintestutils.integration import walk_dir
 
 
 pytestmark = pytest.mark.integration
diff --git a/tests/internals/storage_vdir_import.py b/tests/internals/storage_vdir_import.py
index 0bb47e3..1d61a6e 100644
--- a/tests/internals/storage_vdir_import.py
+++ b/tests/internals/storage_vdir_import.py
@@ -3,7 +3,7 @@
 import pytest
 import random
 import tempfile
-from tests.testutils import cli
+from buildstream.plugintestutils import cli
 
 from buildstream.storage._casbaseddirectory import CasBasedDirectory
 from buildstream.storage._filebaseddirectory import FileBasedDirectory
diff --git a/tests/sandboxes/missing-command.py b/tests/sandboxes/missing-command.py
index 8f210bc..ddf1c48 100644
--- a/tests/sandboxes/missing-command.py
+++ b/tests/sandboxes/missing-command.py
@@ -3,7 +3,7 @@
 
 from buildstream._exceptions import ErrorDomain
 
-from tests.testutils import cli
+from buildstream.plugintestutils import cli
 
 
 DATA_DIR = os.path.join(
diff --git a/tests/sandboxes/missing_dependencies.py b/tests/sandboxes/missing_dependencies.py
index d77674c..19515a1 100644
--- a/tests/sandboxes/missing_dependencies.py
+++ b/tests/sandboxes/missing_dependencies.py
@@ -1,6 +1,6 @@
 import os
 import pytest
-from tests.testutils import cli
+from buildstream.plugintestutils import cli
 from tests.testutils.site import IS_LINUX
 
 from buildstream import _yaml
diff --git a/tests/sandboxes/remote-exec-config.py b/tests/sandboxes/remote-exec-config.py
index 9cf0a8f..9864f25 100644
--- a/tests/sandboxes/remote-exec-config.py
+++ b/tests/sandboxes/remote-exec-config.py
@@ -6,7 +6,7 @@
 from buildstream import _yaml
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
 
-from tests.testutils.runcli import cli
+from buildstream.plugintestutils.runcli import cli
 
 DATA_DIR = os.path.join(
     os.path.dirname(os.path.realpath(__file__)),
diff --git a/tests/sources/bzr.py b/tests/sources/bzr.py
index a56005f..bf7d1e8 100644
--- a/tests/sources/bzr.py
+++ b/tests/sources/bzr.py
@@ -4,7 +4,8 @@
 from buildstream._pipeline import PipelineError
 from buildstream import _yaml
 
-from tests.testutils import cli, create_repo
+from buildstream.plugintestutils import cli
+from tests.testutils import create_repo
 from tests.testutils.site import HAVE_BZR
 
 DATA_DIR = os.path.join(
diff --git a/tests/sources/deb.py b/tests/sources/deb.py
index b40358e..480bea6 100644
--- a/tests/sources/deb.py
+++ b/tests/sources/deb.py
@@ -8,7 +8,7 @@
 from buildstream._exceptions import ErrorDomain
 from buildstream import _yaml
 from tempfile import TemporaryFile
-from tests.testutils import cli
+from buildstream.plugintestutils import cli
 from tests.testutils.site import HAVE_ARPY
 from . import list_dir_contents
 
diff --git a/tests/sources/git.py b/tests/sources/git.py
index 7959083..f3d2e28 100644
--- a/tests/sources/git.py
+++ b/tests/sources/git.py
@@ -28,8 +28,10 @@
 from buildstream._exceptions import ErrorDomain
 from buildstream import _yaml
 from buildstream.plugin import CoreWarnings
+from buildstream.plugintestutils import cli
 
-from tests.testutils import cli, create_repo
+from tests.testutils.site import HAVE_GIT, HAVE_OLD_GIT
+from tests.testutils import create_repo
 from tests.testutils.site import HAVE_GIT, HAVE_OLD_GIT
 
 DATA_DIR = os.path.join(
diff --git a/tests/sources/local.py b/tests/sources/local.py
index 4a0851d..f7c1f4b 100644
--- a/tests/sources/local.py
+++ b/tests/sources/local.py
@@ -2,7 +2,8 @@
 import pytest
 
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
-from tests.testutils import cli, filetypegenerator
+from buildstream.plugintestutils import cli
+from tests.testutils import filetypegenerator
 
 DATA_DIR = os.path.join(
     os.path.dirname(os.path.realpath(__file__)),
diff --git a/tests/sources/no_fetch_cached.py b/tests/sources/no_fetch_cached.py
index d2880f6..9ef838c 100644
--- a/tests/sources/no_fetch_cached.py
+++ b/tests/sources/no_fetch_cached.py
@@ -3,7 +3,8 @@
 
 from buildstream import _yaml
 
-from tests.testutils import cli, create_repo
+from buildstream.plugintestutils import cli
+from tests.testutils import create_repo
 from tests.testutils.site import HAVE_GIT
 
 DATA_DIR = os.path.join(
diff --git a/tests/sources/ostree.py b/tests/sources/ostree.py
index e059a88..7ddea77 100644
--- a/tests/sources/ostree.py
+++ b/tests/sources/ostree.py
@@ -22,8 +22,9 @@
 
 from buildstream._exceptions import ErrorDomain
 from buildstream import _yaml
+from buildstream.plugintestutils import cli
 
-from tests.testutils import cli, create_repo
+from tests.testutils import create_repo
 
 DATA_DIR = os.path.join(
     os.path.dirname(os.path.realpath(__file__)),
diff --git a/tests/sources/patch.py b/tests/sources/patch.py
index 51ae690..2a82bf1 100644
--- a/tests/sources/patch.py
+++ b/tests/sources/patch.py
@@ -2,7 +2,8 @@
 import pytest
 
 from buildstream._exceptions import ErrorDomain, LoadErrorReason
-from tests.testutils import cli, filetypegenerator
+from buildstream.plugintestutils import cli
+from tests.testutils import filetypegenerator
 
 DATA_DIR = os.path.join(
     os.path.dirname(os.path.realpath(__file__)),
diff --git a/tests/sources/pip.py b/tests/sources/pip.py
index 6e1a347..3114a82 100644
--- a/tests/sources/pip.py
+++ b/tests/sources/pip.py
@@ -4,7 +4,7 @@
 from buildstream._exceptions import ErrorDomain
 from buildstream import _yaml
 from buildstream.plugins.sources.pip import _match_package_name
-from tests.testutils import cli
+from buildstream.plugintestutils import cli
 
 DATA_DIR = os.path.join(
     os.path.dirname(os.path.realpath(__file__)),
diff --git a/tests/sources/previous_source_access.py b/tests/sources/previous_source_access.py
index 26640d1..9d83178 100644
--- a/tests/sources/previous_source_access.py
+++ b/tests/sources/previous_source_access.py
@@ -1,7 +1,7 @@
 import os
 import pytest
 
-from tests.testutils import cli
+from buildstream.plugintestutils import cli
 
 DATA_DIR = os.path.join(
     os.path.dirname(os.path.realpath(__file__)),
diff --git a/tests/sources/remote.py b/tests/sources/remote.py
index 1d77273..30b65b4 100644
--- a/tests/sources/remote.py
+++ b/tests/sources/remote.py
@@ -4,7 +4,7 @@
 
 from buildstream._exceptions import ErrorDomain
 from buildstream import _yaml
-from tests.testutils import cli
+from buildstream.plugintestutils import cli
 from tests.testutils.file_server import create_file_server
 
 DATA_DIR = os.path.join(
diff --git a/tests/sources/tar.py b/tests/sources/tar.py
index 406d670..959ff89 100644
--- a/tests/sources/tar.py
+++ b/tests/sources/tar.py
@@ -8,7 +8,7 @@
 
 from buildstream._exceptions import ErrorDomain
 from buildstream import _yaml
-from tests.testutils import cli
+from buildstream.plugintestutils import cli
 from tests.testutils.file_server import create_file_server
 from tests.testutils.site import HAVE_LZIP
 from . import list_dir_contents
diff --git a/tests/sources/zip.py b/tests/sources/zip.py
index 009862e..dd72dd5 100644
--- a/tests/sources/zip.py
+++ b/tests/sources/zip.py
@@ -4,7 +4,7 @@
 
 from buildstream._exceptions import ErrorDomain
 from buildstream import _yaml
-from tests.testutils import cli
+from buildstream.plugintestutils import cli
 from tests.testutils.file_server import create_file_server
 from . import list_dir_contents
 
diff --git a/tests/testutils/__init__.py b/tests/testutils/__init__.py
index eb7211e..173cc7c 100644
--- a/tests/testutils/__init__.py
+++ b/tests/testutils/__init__.py
@@ -23,7 +23,6 @@
 #           William Salmon <will.salmon@codethink.co.uk>
 #
 
-from .runcli import cli, cli_integration
 from .repo import create_repo, ALL_REPO_KINDS
 from .artifactshare import create_artifact_share
 from .element_generators import create_element_size, update_element_size
diff --git a/tests/testutils/integration.py b/tests/testutils/integration.py
deleted file mode 100644
index b2cf9fb..0000000
--- a/tests/testutils/integration.py
+++ /dev/null
@@ -1,29 +0,0 @@
-import os
-
-from buildstream import _yaml
-
-
-# Return a list of files relative to the given directory
-def walk_dir(root):
-    for dirname, dirnames, filenames in os.walk(root):
-        # ensure consistent traversal order, needed for consistent
-        # handling of symlinks.
-        dirnames.sort()
-        filenames.sort()
-
-        # print path to all subdirectories first.
-        for subdirname in dirnames:
-            yield os.path.join(dirname, subdirname)[len(root):]
-
-        # print path to all filenames.
-        for filename in filenames:
-            yield os.path.join(dirname, filename)[len(root):]
-
-
-# Ensure that a directory contains the given filenames.
-def assert_contains(directory, expected):
-    missing = set(expected)
-    missing.difference_update(walk_dir(directory))
-    if len(missing) > 0:
-        raise AssertionError("Missing {} expected elements from list: {}"
-                             .format(len(missing), missing))
diff --git a/tox.ini b/tox.ini
index 6683df7..9183e02 100644
--- a/tox.ini
+++ b/tox.ini
@@ -70,6 +70,7 @@
     sphinx
     sphinx-click
     sphinx_rtd_theme >= 0.4.2
+    pytest
     -rrequirements/requirements.txt
     -rrequirements/plugin-requirements.txt
 passenv =