blob: c4f1d35bcdbe97136c1c75515500b34e11985f6e [file] [log] [blame]
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# 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, "missing-buildtree-artifact-not-cached"),
# 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),
# Pull only the artifact, but without the buildtree
("none", False, "missing-buildtree-artifact-buildtree-not-cached"),
# Pull only the artifact with its buildtree
("none", True, None),
],
ids=[
"no-pull",
"pull-only-deps",
"pull-without-buildtree",
"pull-with-buildtree",
"pull-target-without-buildtree",
"pull-target-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)
# Disable access to the artifact server after pulling, so that `bst shell` cannot automatically
# pull the missing bits, this should be equivalent to the missing bits being missing in a
# remote server
cli.configure({"artifacts": {}})
# 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 allowing
# BuildStream to download any missing bits from the artifact server on the fly (which
# it will do by default) 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,
"--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 shelling into a buildtree by its artifact name
#
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_shell_pull_artifact_cached_buildtree(share_with_buildtrees, datafiles, cli):
project = str(datafiles)
artifact_name = "test/build-shell-buildtree/4a47c98a10df39e65e99d471f96edc5b58d4ea5b9b1f221d0be832a8124b8099"
cli.configure({"artifacts": {"servers": [{"url": share_with_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",
"--use-buildtree",
artifact_name,
"--",
"cat",
# We don't preserve the working directory in artifacts, so we will be executing at /
"/buildstream/test/build-shell/buildtree.bst/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")
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_shell_script_element(datafiles, cli_integration):
project = str(datafiles)
element_name = "build-shell/script.bst"
result = cli_integration.run(project=project, args=["--cache-buildtrees", "always", "build", element_name])
result.assert_success()
# Run the shell and use the cached buildtree on this script element
result = cli_integration.run(
project=project, args=["shell", "--build", element_name, "--use-buildtree", "--", "cat", "/test"]
)
result.assert_success()
assert "Hi" in result.output
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
@pytest.mark.parametrize(
"element_name,expect_success",
[
# Build shell into a compose element which succeeded
("build-shell/compose-success.bst", True),
# Build shell into a compose element with failed integration commands
("build-shell/compose-fail.bst", False),
],
ids=["integration-success", "integration-fail"],
)
def test_shell_compose_element(datafiles, cli_integration, element_name, expect_success):
project = str(datafiles)
# Build the element so it's in the local cache, ensure caching of buildtrees at build time
result = cli_integration.run(project=project, args=["--cache-buildtrees", "always", "build", element_name])
if expect_success:
result.assert_success()
else:
result.assert_main_error(ErrorDomain.STREAM, None)
# Ensure that the shell works regardless of success expectations
#
result = cli_integration.run(
project=project, args=["shell", "--build", element_name, "--use-buildtree", "--", "echo", "Hi"]
)
result.assert_success()
assert "Hi" in result.output
# Check the file created with integration commands
#
result = cli_integration.run(
project=project,
args=["shell", "--build", element_name, "--use-buildtree", "--", "cat", "/integration-success"],
)
if expect_success:
result.assert_success()
assert "Hi" in result.output
else:
# Here the exit code is determined by `cat`, and will be non-zero.
#
# We cannot use result.assert_main_error() because that explicitly expects -1
assert result.exit_code != 0