Commit b9eec0ca authored by bst-marge-bot's avatar bst-marge-bot

Merge branch 'phil/expose-templated-tests' into 'master'

Expose templated source tests

See merge request !1261
parents 92c9a047 c90cf9e8
Pipeline #56813318 failed with stages
in 54 minutes and 47 seconds
......@@ -6,6 +6,9 @@ include MAINTAINERS
include NEWS
include README.rst
# Data files required by BuildStream's generic source tests
recursive-include buildstream/plugintestutils/_sourcetests/project *
# Documentation package includes
include doc/Makefile
include doc/badges.py
......
......@@ -15,7 +15,14 @@
# 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/>.
"""
This package contains various utilities which make it easier to test plugins.
"""
import os
from collections import OrderedDict
from . import _sourcetests
from .repo import Repo
from .runcli import cli, cli_integration, cli_remote_execution
# To make use of these test utilities it is necessary to have pytest
......@@ -28,3 +35,64 @@ except ImportError:
msg = "Could not import pytest:\n" \
"To use the {} module, you must have pytest installed.".format(module_name)
raise ImportError(msg)
ALL_REPO_KINDS = OrderedDict()
def create_repo(kind, directory, subdir='repo'):
"""Convenience method for creating a Repo
Args:
kind (str): The kind of repo to create (a source plugin basename). This
must have previously been registered using
`register_repo_kind`
directory (str): The path where the repo will keep a cache
Returns:
(Repo): A new Repo object
"""
try:
constructor = ALL_REPO_KINDS[kind]
except KeyError as e:
raise AssertionError("Unsupported repo kind {}".format(kind)) from e
return constructor(directory, subdir=subdir)
def register_repo_kind(kind, cls):
"""Register a new repo kind.
Registering a repo kind will allow the use of the `create_repo`
method for that kind and include that repo kind in ALL_REPO_KINDS
In addition, repo_kinds registred prior to
`sourcetests_collection_hook` being called will be automatically
used to test the basic behaviour of their associated source
plugins using the tests in `plugintestutils._sourcetests`.
Args:
kind (str): The kind of repo to create (a source plugin basename)
cls (cls) : A class derived from Repo.
"""
ALL_REPO_KINDS[kind] = cls
def sourcetests_collection_hook(session):
""" Used to hook the templated source plugin tests into a pyest test suite.
This should be called via the `pytest_sessionstart
hook <https://docs.pytest.org/en/latest/reference.html#collection-hooks>`_.
The tests in the _sourcetests package will be collected as part of
whichever test package this hook is called from.
Args:
session (pytest.Session): The current pytest session
"""
SOURCE_TESTS_PATH = os.path.dirname(_sourcetests.__file__)
# Add the location of the source tests to the session's
# python_files config. Without this, pytest may filter out these
# tests during collection.
session.config.addinivalue_line("python_files", os.path.join(SOURCE_TESTS_PATH, "*.py"))
session.config.args.append(SOURCE_TESTS_PATH)
......@@ -22,8 +22,7 @@
import os
import pytest
from tests.testutils import create_repo, ALL_REPO_KINDS
from buildstream.plugintestutils import create_repo, ALL_REPO_KINDS
from buildstream.plugintestutils import cli # pylint: disable=unused-import
from buildstream import _yaml
......
......@@ -22,12 +22,10 @@
import os
import pytest
from tests.testutils import create_repo, ALL_REPO_KINDS, generate_junction
from tests.frontend import configure_project
from buildstream.plugintestutils import cli # pylint: disable=unused-import
from buildstream import _yaml
from .._utils import generate_junction, configure_project
from .. import create_repo, ALL_REPO_KINDS
from .. import cli # pylint: disable=unused-import
# Project directory
TOP_DIR = os.path.dirname(os.path.realpath(__file__))
......
......@@ -22,11 +22,11 @@
import os
import pytest
from tests.testutils import create_repo, ALL_REPO_KINDS, generate_junction
from buildstream.plugintestutils import cli # pylint: disable=unused-import
from buildstream import _yaml
from buildstream._exceptions import ErrorDomain
from .._utils import generate_junction
from .. import create_repo, ALL_REPO_KINDS
from .. import cli # pylint: disable=unused-import
# Project directory
TOP_DIR = os.path.dirname(os.path.realpath(__file__))
......
......@@ -22,11 +22,10 @@
import os
import pytest
from tests.testutils import create_repo, ALL_REPO_KINDS
from tests.testutils.site import HAVE_SANDBOX
from buildstream.plugintestutils import cli # pylint: disable=unused-import
from buildstream import _yaml
from .._utils.site import HAVE_SANDBOX
from .. import create_repo, ALL_REPO_KINDS
from .. import cli # pylint: disable=unused-import
# Project directory
TOP_DIR = os.path.dirname(os.path.realpath(__file__))
......
......@@ -22,13 +22,11 @@
import os
import pytest
from tests.testutils import create_repo, ALL_REPO_KINDS, generate_junction
from tests.frontend import configure_project
from buildstream.plugintestutils import cli # pylint: disable=unused-import
from buildstream import _yaml
from buildstream._exceptions import ErrorDomain
from .._utils import generate_junction, configure_project
from .. import create_repo, ALL_REPO_KINDS
from .. import cli # pylint: disable=unused-import
# Project directory
TOP_DIR = os.path.dirname(os.path.realpath(__file__))
......
......@@ -22,10 +22,10 @@
import os
import pytest
from tests.testutils import create_repo, ALL_REPO_KINDS, generate_junction
from buildstream.plugintestutils import cli # pylint: disable=unused-import
from buildstream import _yaml
from .._utils import generate_junction
from .. import create_repo, ALL_REPO_KINDS
from .. import cli # pylint: disable=unused-import
# Project directory
TOP_DIR = os.path.dirname(os.path.realpath(__file__))
......
......@@ -23,10 +23,9 @@ import os
import shutil
import pytest
from tests.testutils import create_repo, ALL_REPO_KINDS
from buildstream.plugintestutils import cli # pylint: disable=unused-import
from buildstream import _yaml
from .. import create_repo, ALL_REPO_KINDS
from .. import cli # pylint: disable=unused-import
# Project directory
TOP_DIR = os.path.dirname(os.path.realpath(__file__))
......
import os
from buildstream import _yaml
from .junction import generate_junction
def configure_project(path, config):
config['name'] = 'test'
config['element-path'] = 'elements'
_yaml.dump(config, os.path.join(path, 'project.conf'))
import subprocess
import pytest
from buildstream import _yaml
from .. import Repo
from .site import HAVE_GIT, GIT, GIT_ENV
# generate_junction()
#
# Generates a junction element with a git repository
#
# Args:
# tmpdir: The tmpdir fixture, for storing the generated git repo
# subproject_path: The path for the subproject, to add to the git repo
# junction_path: The location to store the generated junction element
# store_ref: Whether to store the ref in the junction.bst file
#
# Returns:
# (str): The ref
#
def generate_junction(tmpdir, subproject_path, junction_path, *, store_ref=True):
# Create a repo to hold the subproject and generate
# a junction element for it
#
repo = _SimpleGit(str(tmpdir))
source_ref = ref = repo.create(subproject_path)
if not store_ref:
source_ref = None
element = {
'kind': 'junction',
'sources': [
repo.source_config(ref=source_ref)
]
}
_yaml.dump(element, junction_path)
return ref
# A barebones Git Repo class to use for generating junctions
class _SimpleGit(Repo):
def __init__(self, directory, subdir='repo'):
if not HAVE_GIT:
pytest.skip('git is not available')
super().__init__(directory, subdir)
def create(self, directory):
self.copy_directory(directory, self.repo)
self._run_git('init', '.')
self._run_git('add', '.')
self._run_git('commit', '-m', 'Initial commit')
return self.latest_commit()
def latest_commit(self):
return self._run_git(
'rev-parse', 'HEAD',
stdout=subprocess.PIPE,
universal_newlines=True,
).stdout.strip()
def source_config(self, ref=None, checkout_submodules=None):
config = {
'kind': 'git',
'url': 'file://' + self.repo,
'track': 'master'
}
if ref is not None:
config['ref'] = ref
if checkout_submodules is not None:
config['checkout-submodules'] = checkout_submodules
return config
def _run_git(self, *args, **kwargs):
argv = [GIT]
argv.extend(args)
if 'env' not in kwargs:
kwargs['env'] = dict(GIT_ENV, PWD=self.repo)
kwargs.setdefault('cwd', self.repo)
kwargs.setdefault('check', True)
return subprocess.run(argv, **kwargs)
# Some things resolved about the execution site,
# so we dont have to repeat this everywhere
#
import os
import sys
import platform
from buildstream import _site, utils, ProgramNotFoundError
try:
GIT = utils.get_host_tool('git')
HAVE_GIT = True
GIT_ENV = {
'GIT_AUTHOR_DATE': '1320966000 +0200',
'GIT_AUTHOR_NAME': 'tomjon',
'GIT_AUTHOR_EMAIL': 'tom@jon.com',
'GIT_COMMITTER_DATE': '1320966000 +0200',
'GIT_COMMITTER_NAME': 'tomjon',
'GIT_COMMITTER_EMAIL': 'tom@jon.com'
}
except ProgramNotFoundError:
GIT = None
HAVE_GIT = False
GIT_ENV = dict()
try:
utils.get_host_tool('bwrap')
HAVE_BWRAP = True
HAVE_BWRAP_JSON_STATUS = _site.get_bwrap_version() >= (0, 3, 2)
except ProgramNotFoundError:
HAVE_BWRAP = False
HAVE_BWRAP_JSON_STATUS = False
IS_LINUX = os.getenv('BST_FORCE_BACKEND', sys.platform).startswith('linux')
IS_WSL = (IS_LINUX and 'Microsoft' in platform.uname().release)
IS_WINDOWS = (os.name == 'nt')
if not IS_LINUX:
HAVE_SANDBOX = True # fallback to a chroot sandbox on unix
elif IS_WSL:
HAVE_SANDBOX = False # Sandboxes are inoperable under WSL due to lack of FUSE
elif IS_LINUX and HAVE_BWRAP:
HAVE_SANDBOX = True
else:
HAVE_SANDBOX = False
#
# Copyright (C) 2016-2018 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/>.
"""
Repo - Utility class for testing source plugins
===============================================
"""
import os
import shutil
# Repo()
#
# Abstract class providing scaffolding for
# generating data to be used with various sources
#
# Args:
# directory (str): The base temp directory for the test
# subdir (str): The subdir for the repo, in case there is more than one
#
class Repo():
"""Repo()
Abstract class providing scaffolding for generating data to be
used with various sources. Subclasses of Repo may be registered to
run through the suite of generic source plugin tests provided in
buildstream.plugintestutils.
Args:
directory (str): The base temp directory for the test
subdir (str): The subdir for the repo, in case there is more than one
"""
def __init__(self, directory, subdir='repo'):
# The working directory for the repo object
......@@ -24,44 +49,40 @@ class Repo():
os.makedirs(self.repo, exist_ok=True)
# create():
#
# Create a repository in self.directory and add the initial content
#
# Args:
# directory: A directory with content to commit
#
# Returns:
# (smth): A new ref corresponding to this commit, which can
# be passed as the ref in the Repo.source_config() API.
#
def create(self, directory):
pass
# source_config()
#
# Args:
# ref (smth): An optional abstract ref object, usually a string.
#
# Returns:
# (dict): A configuration which can be serialized as a
# source when generating an element file on the fly
#
"""Create a repository in self.directory and add the initial content
Args:
directory: A directory with content to commit
Returns:
(smth): A new ref corresponding to this commit, which can
be passed as the ref in the Repo.source_config() API.
"""
raise NotImplementedError("create method has not been implemeted")
def source_config(self, ref=None):
pass
# copy_directory():
#
# Copies the content of src to the directory dest
#
# Like shutil.copytree(), except dest is expected
# to exist.
#
# Args:
# src (str): The source directory
# dest (str): The destination directory
#
"""
Args:
ref (smth): An optional abstract ref object, usually a string.
Returns:
(dict): A configuration which can be serialized as a
source when generating an element file on the fly
"""
raise NotImplementedError("source_config method has not been implemeted")
def copy_directory(self, src, dest):
""" Copies the content of src to the directory dest
Like shutil.copytree(), except dest is expected
to exist.
Args:
src (str): The source directory
dest (str): The destination directory
"""
for filename in os.listdir(src):
src_path = os.path.join(src, filename)
dest_path = os.path.join(dest, filename)
......@@ -70,17 +91,15 @@ class Repo():
else:
shutil.copy2(src_path, dest_path)
# copy():
#
# Creates a copy of this repository in the specified
# destination.
#
# Args:
# dest (str): The destination directory
#
# Returns:
# (Repo): A Repo object for the new repository.
def copy(self, dest):
"""Creates a copy of this repository in the specified destination.
Args:
dest (str): The destination directory
Returns:
(Repo): A Repo object for the new repository.
"""
subdir = self.repo[len(self.directory):].lstrip(os.sep)
new_dir = os.path.join(dest, subdir)
os.makedirs(new_dir, exist_ok=True)
......
......@@ -20,3 +20,4 @@ useful for working on BuildStream itself.
buildstream.scriptelement
buildstream.sandbox.sandbox
buildstream.utils
buildstream.plugintestutils
......@@ -323,6 +323,7 @@ setup(name='BuildStream',
packages=find_packages(exclude=('tests', 'tests.*')),
package_data={'buildstream': ['plugins/*/*.py', 'plugins/*/*.yaml',
'data/*.yaml', 'data/*.sh.in']},
include_package_data=True,
data_files=[
# This is a weak attempt to integrate with the user nicely,
# installing things outside of the python package itself with pip is
......
#!/usr/bin/env python3
#
# Copyright (C) 2018 Codethink Limited
# Copyright (C) 2019 Bloomberg Finance LLP
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
......@@ -23,6 +24,14 @@ import shutil
import tempfile
import pytest
from buildstream._platform.platform import Platform
from buildstream.plugintestutils import register_repo_kind, sourcetests_collection_hook
from tests.testutils.repo.git import Git
from tests.testutils.repo.bzr import Bzr
from tests.testutils.repo.ostree import OSTree
from tests.testutils.repo.tar import Tar
from tests.testutils.repo.zip import Zip
#
# This file is loaded by pytest, we use it to add a custom
......@@ -155,3 +164,19 @@ def clean_platform_cache():
@pytest.fixture(autouse=True)
def ensure_platform_cache_is_clean():
clean_platform_cache()
#################################################
# Setup for templated source tests #
#################################################
register_repo_kind('git', Git)
register_repo_kind('bzr', Bzr)
register_repo_kind('ostree', OSTree)
register_repo_kind('tar', Tar)
register_repo_kind('zip', Zip)
# This hook enables pytest to collect the templated source tests from
# buildstream.plugintestutils
def pytest_sessionstart(session):
sourcetests_collection_hook(session)
......@@ -6,7 +6,7 @@ import shutil
import pytest
from tests.testutils import create_repo
from buildstream.plugintestutils import create_repo
from buildstream.plugintestutils import cli # pylint: disable=unused-import
from buildstream._exceptions import ErrorDomain
from buildstream import _yaml
......
......@@ -7,8 +7,8 @@ import pytest
from buildstream import _yaml
from buildstream._exceptions import ErrorDomain, LoadErrorReason
from buildstream.plugintestutils import cli # pylint: disable=unused-import
from tests.testutils import generate_junction, create_repo
from buildstream.plugintestutils import create_repo
from tests.testutils import generate_junction
# Project directory
......
......@@ -9,7 +9,7 @@ import pytest
from buildstream import _yaml
from buildstream._exceptions import ErrorDomain, LoadErrorReason
from buildstream.plugintestutils import cli # pylint: disable=unused-import
from tests.testutils import create_repo
from buildstream.plugintestutils import create_repo
from tests.testutils.site import HAVE_GIT
......
......@@ -8,9 +8,8 @@ import itertools
import pytest
from tests.testutils import create_repo
from buildstream import _yaml
from buildstream.plugintestutils import create_repo
from buildstream.plugintestutils import cli # pylint: disable=unused-import
from buildstream._exceptions import ErrorDomain
......
......@@ -3,10 +3,9 @@
import os
from buildstream.plugintestutils import cli # pylint: disable=unused-import
from buildstream.plugintestutils import create_repo
from buildstream import _yaml
from tests.testutils import create_repo
def prepare_junction_project(cli, tmpdir):
main_project = tmpdir.join("main")
......
......@@ -4,8 +4,8 @@
import os
import pytest
from tests.testutils import create_repo, generate_junction, yaml_file_get_provenance
from tests.testutils import generate_junction, yaml_file_get_provenance
from buildstream.plugintestutils import create_repo
from buildstream.plugintestutils import cli # pylint: disable=unused-import
from buildstream import _yaml
from buildstream._exceptions import ErrorDomain, LoadErrorReason
......
......@@ -6,7 +6,7 @@ import re
import pytest
from tests.testutils import create_repo
from buildstream.plugintestutils import create_repo
from buildstream import _yaml
from buildstream._exceptions import ErrorDomain
......
......@@ -4,9 +4,8 @@
import os
import pytest
from tests.testutils import create_repo
from buildstream import _yaml
from buildstream.plugintestutils import create_repo
from buildstream.plugintestutils import cli # pylint: disable=unused-import
......
......@@ -4,7 +4,7 @@
import os
import pytest
from tests.testutils import create_repo
from buildstream.plugintestutils import create_repo
from buildstream.plugintestutils import cli # pylint: disable=unused-import
from buildstream import _yaml