blob: 4c42551b93adb8766c1585c6bbf3d778c3022e43 [file] [log] [blame]
# Pylint doesn't play well with fixtures and dependency injection from pytest
# pylint: disable=redefined-outer-name
import os
import shutil
import pytest
from buildstream.testing import cli, cli_integration, Cli # pylint: disable=unused-import
from buildstream.exceptions import ErrorDomain
from buildstream.testing._utils.site import HAVE_SANDBOX
from tests.testutils import ArtifactShare
pytestmark = pytest.mark.integration
DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "project")
#
# Ensure that we didn't get a build tree if we didn't ask for one
#
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_buildtree_unused(cli_integration, datafiles):
# We can only test the non interacitve case
# The non interactive case defaults to not using buildtrees
# for `bst shell --build`
project = str(datafiles)
element_name = "build-shell/buildtree.bst"
res = cli_integration.run(project=project, args=["--cache-buildtrees", "always", "build", element_name])
res.assert_success()
res = cli_integration.run(project=project, args=["shell", "--build", element_name, "--", "cat", "test"])
res.assert_shell_error()
#
# Ensure we can use a buildtree from a successful build
#
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_buildtree_from_success(cli_integration, datafiles):
# Test that if we ask for a build tree it is there.
project = str(datafiles)
element_name = "build-shell/buildtree.bst"
res = cli_integration.run(project=project, args=["--cache-buildtrees", "always", "build", element_name])
res.assert_success()
res = cli_integration.run(
project=project, args=["shell", "--build", "--use-buildtree", element_name, "--", "cat", "test"]
)
res.assert_success()
assert "Hi" in res.output
#
# Ensure we can use a buildtree from a failed build
#
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_buildtree_from_failure(cli_integration, datafiles):
# Test that we can use a build tree after a failure
project = str(datafiles)
element_name = "build-shell/buildtree-fail.bst"
res = cli_integration.run(project=project, args=["build", element_name])
res.assert_main_error(ErrorDomain.STREAM, None)
# Assert that file has expected contents
res = cli_integration.run(
project=project, args=["shell", "--build", element_name, "--use-buildtree", "--", "cat", "test"]
)
res.assert_success()
assert "WARNING using a buildtree from a failed build" in res.stderr
assert "Hi" in res.output
###########################################################################
# Custom fixture ahead #
###########################################################################
#
# There are a lot of scenarios to test with launching shells with various states
# of local cache, which all require that artifacts be built in an artifact share.
#
# We want to use @pytest.mark.parametrize() here so that we can more coherently test
# specific scenarios, but testing each of these in a separate test is very expensive.
#
# For this reason, we use some module scope fixtures which will prepare the
# ArtifactShare() object by building and pushing to it, and the same ArtifactShare()
# object is shared across all tests which need the ArtifactShare() to be in that
# given state.
#
# This means we only need to download (fetch) the external alpine runtime and
# push it to our internal ArtifactShare() once, but we can reuse it for many
# parametrized tests.
#
# It is important that none of the tests using these fixtures access the
# module scope ArtifactShare() instances with "push" access, as tests
# should not be modifying the state of the shared data.
#
###########################################################################
# create_built_artifact_share()
#
# A helper function to create an ArtifactShare object with artifacts
# prebuilt, this can be shared across multiple tests which access
# the artifact share in a read-only fashion.
#
# Args:
# tmpdir (str): The temp directory to be used
# cache_buildtrees (bool): Whether to cache buildtrees when building
# integration_cache (IntegrationCache): The session wide integration cache so that we
# can reuse the sources from previous runs
#
def create_built_artifact_share(tmpdir, cache_buildtrees, integration_cache):
element_name = "build-shell/buildtree.bst"
# Replicate datafiles behavior and do work entirely in the temp directory
project = os.path.join(tmpdir, "project")
shutil.copytree(DATA_DIR, project)
# Create the share to be hosted from this temp directory
share = ArtifactShare(os.path.join(tmpdir, "artifactcache"))
# Create a Cli instance to build and populate the share
cli = Cli(os.path.join(tmpdir, "cache"))
cli.configure(
{"artifacts": {"servers": [{"url": share.repo, "push": True}]}, "sourcedir": integration_cache.sources}
)
# Optionally cache build trees
args = []
if cache_buildtrees:
args += ["--cache-buildtrees", "always"]
args += ["build", element_name]
# Build
result = cli.run(project=project, args=args)
result.assert_success()
# Assert that the artifact is indeed in the share
assert cli.get_element_state(project, element_name) == "cached"
artifact_name = cli.get_artifact_name(project, "test", element_name)
assert share.get_artifact(artifact_name)
return share
# share_with_buildtrees()
#
# A module scope fixture which prepares an ArtifactShare() instance
# which will have all dependencies of "build-shell/buildtree.bst" built and
# cached with buildtrees also cached.
#
@pytest.fixture(scope="module")
def share_with_buildtrees(tmp_path_factory, integration_cache):
# Get a temporary directory for this module scope fixture
tmpdir = tmp_path_factory.mktemp("artifact_share_with_buildtrees")
# Create our ArtifactShare instance which will persist for the duration of
# the class scope fixture.
share = create_built_artifact_share(tmpdir, True, integration_cache)
try:
yield share
finally:
share.close()
# share_without_buildtrees()
#
# A module scope fixture which prepares an ArtifactShare() instance
# which will have all dependencies of "build-shell/buildtree.bst" built
# but without caching any buildtrees.
#
@pytest.fixture(scope="module")
def share_without_buildtrees(tmp_path_factory, integration_cache):
# Get a temporary directory for this module scope fixture
tmpdir = tmp_path_factory.mktemp("artifact_share_without_buildtrees")
# Create our ArtifactShare instance which will persist for the duration of
# the class scope fixture.
share = create_built_artifact_share(tmpdir, False, integration_cache)
try:
yield share
finally:
share.close()
# maybe_pull_deps()
#
# Convenience function for optionally pulling element dependencies
# in the following parametrized tests.
#
# Args:
# cli (Cli): The Cli object
# project (str): The project path
# element_name (str): The element name
# pull_deps (str): The argument for `--deps`, or None
# pull_buildtree (bool): Whether to also pull buildtrees
#
def maybe_pull_deps(cli, project, element_name, pull_deps, pull_buildtree):
# Optionally pull the buildtree along with `bst artifact pull`
if pull_deps:
args = []
if pull_buildtree:
args += ["--pull-buildtrees"]
args += ["artifact", "pull", "--deps", pull_deps, element_name]
# Pull from cache
result = cli.run(project=project, args=args)
result.assert_success()
#
# Test behavior of launching a shell and requesting to use a buildtree, with
# various states of local cache (ranging from nothing cached to everything cached)
#
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
@pytest.mark.parametrize(
"pull_deps,pull_buildtree,expect_error",
[
# Don't pull at all
(None, False, "shell-missing-deps"),
# Pull only dependencies
("build", False, "missing-buildtree-artifact-not-cached"),
# Pull all elements including the shell element, but without the buildtree
("all", False, "missing-buildtree-artifact-buildtree-not-cached"),
# Pull all elements including the shell element, and pull buildtrees
("all", True, None),
],
ids=["no-pull", "pull-only-deps", "pull-without-buildtree", "pull-with-buildtree"],
)
def test_shell_use_cached_buildtree(share_with_buildtrees, datafiles, cli, pull_deps, pull_buildtree, expect_error):
project = str(datafiles)
element_name = "build-shell/buildtree.bst"
cli.configure({"artifacts": {"servers": [{"url": share_with_buildtrees.repo}]}})
# Optionally pull the buildtree along with `bst artifact pull`
maybe_pull_deps(cli, project, element_name, pull_deps, pull_buildtree)
# Run the shell without asking it to pull any buildtree, just asking to use a buildtree
result = cli.run(project=project, args=["shell", "--build", element_name, "--use-buildtree", "--", "cat", "test"])
if expect_error:
result.assert_main_error(ErrorDomain.APP, expect_error)
else:
result.assert_success()
assert "Hi" in result.output
#
# Test behavior of launching a shell and requesting to use a buildtree, while
# also requesting to download any missing bits from the artifact server on the fly,
# again with various states of local cache (ranging from nothing cached to everything cached)
#
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
@pytest.mark.parametrize(
"pull_deps,pull_buildtree",
[
# Don't pull at all
(None, False),
# Pull only dependencies
("build", False),
# Pull all elements including the shell element, but without the buildtree
("all", False),
# Pull all elements including the shell element, and pull buildtrees
("all", True),
],
ids=["no-pull", "pull-only-deps", "pull-without-buildtree", "pull-with-buildtree"],
)
def test_shell_pull_cached_buildtree(share_with_buildtrees, datafiles, cli, pull_deps, pull_buildtree):
project = str(datafiles)
element_name = "build-shell/buildtree.bst"
cli.configure({"artifacts": {"servers": [{"url": share_with_buildtrees.repo}]}})
# Optionally pull the buildtree along with `bst artifact pull`
maybe_pull_deps(cli, project, element_name, pull_deps, pull_buildtree)
# Run the shell and request that required artifacts and buildtrees should be pulled
result = cli.run(
project=project,
args=[
"--pull-buildtrees",
"shell",
"--build",
element_name,
"--pull",
"--use-buildtree",
"--",
"cat",
"test",
],
)
# In this case, we should succeed every time, regardless of what was
# originally available in the local cache.
#
result.assert_success()
assert "Hi" in result.output
#
# Test behavior of launching a shell and requesting to use a buildtree.
#
# In this case we download everything we need first, but the buildtree was never cached at build time
#
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_shell_use_uncached_buildtree(share_without_buildtrees, datafiles, cli):
project = str(datafiles)
element_name = "build-shell/buildtree.bst"
cli.configure({"artifacts": {"servers": [{"url": share_without_buildtrees.repo}]}})
# Pull everything we would need
maybe_pull_deps(cli, project, element_name, "all", True)
# Run the shell without asking it to pull any buildtree, just asking to use a buildtree
result = cli.run(project=project, args=["shell", "--build", element_name, "--use-buildtree", "--", "cat", "test"])
# Sorry, a buildtree was never cached for this element
result.assert_main_error(ErrorDomain.APP, "missing-buildtree-artifact-created-without-buildtree")
#
# Test behavior of launching a shell and requesting to use a buildtree.
#
# In this case we download everything we need first, but the buildtree was never cached at build time
#
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_shell_pull_uncached_buildtree(share_without_buildtrees, datafiles, cli):
project = str(datafiles)
element_name = "build-shell/buildtree.bst"
cli.configure({"artifacts": {"servers": [{"url": share_without_buildtrees.repo}]}})
# Run the shell and request that required artifacts and buildtrees should be pulled
result = cli.run(
project=project,
args=[
"--pull-buildtrees",
"shell",
"--build",
element_name,
"--pull",
"--use-buildtree",
"--",
"cat",
"test",
],
)
# Sorry, a buildtree was never cached for this element
result.assert_main_error(ErrorDomain.APP, "missing-buildtree-artifact-created-without-buildtree")