| # 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") |