blob: 1db9860b292023d378c5468ec2098151732a755a [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 pytest
from buildstream import _yaml
from buildstream._testing import cli_integration as cli # pylint: disable=unused-import
from buildstream._testing._utils.site import HAVE_SANDBOX
from buildstream.exceptions import ErrorDomain
from buildstream.utils import BST_ARBITRARY_TIMESTAMP
from tests.testutils import wait_for_cache_granularity
pytestmark = pytest.mark.integration
DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "project")
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_workspace_stages_once(cli, datafiles):
project = str(datafiles)
workspace = os.path.join(cli.directory, "workspace")
element_name = "workspace/workspace-mount.bst"
res = cli.run(project=project, args=["workspace", "open", "--directory", workspace, element_name])
assert res.exit_code == 0
assert cli.get_element_key(project, element_name) != "{:?<64}".format("")
res = cli.run(project=project, args=["build", element_name])
assert res.exit_code == 0
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_workspace_mount(cli, datafiles):
project = str(datafiles)
workspace = os.path.join(cli.directory, "workspace")
element_name = "workspace/workspace-mount.bst"
res = cli.run(project=project, args=["workspace", "open", "--directory", workspace, element_name])
assert res.exit_code == 0
res = cli.run(project=project, args=["build", element_name])
assert res.exit_code == 0
assert os.path.exists(os.path.join(cli.directory, "workspace"))
@pytest.mark.datafiles(DATA_DIR)
def test_workspace_mount_on_read_only_directory(cli, datafiles):
project = str(datafiles)
workspace = os.path.join(cli.directory, "workspace")
os.makedirs(workspace)
element_name = "workspace/workspace-mount.bst"
# make directory RO
os.chmod(workspace, 0o555)
res = cli.run(project=project, args=["workspace", "open", "--directory", workspace, element_name])
assert res.exit_code == 0
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_workspace_commanddir(cli, datafiles):
project = str(datafiles)
workspace = os.path.join(cli.directory, "workspace")
element_name = "workspace/workspace-commanddir.bst"
res = cli.run(project=project, args=["workspace", "open", "--directory", workspace, element_name])
assert res.exit_code == 0
res = cli.run(project=project, args=["build", element_name])
assert res.exit_code == 0
# Check that the object file was created in the command-subdir `build`
# using the cached buildtree.
res = cli.run(
project=project,
args=[
"shell",
"--build",
element_name,
"--use-buildtree",
"--",
"find",
"..",
"-mindepth",
"1",
],
)
res.assert_success()
files = res.output.splitlines()
assert "../build/hello.o" in files
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_workspace_updated_dependency(cli, datafiles):
project = str(datafiles)
workspace = os.path.join(cli.directory, "workspace")
element_path = os.path.join(project, "elements")
element_name = "workspace/workspace-updated-dependency.bst"
dep_name = "workspace/dependency.bst"
dependency = {
"kind": "manual",
"depends": [{"filename": "base.bst", "type": "build"}],
"config": {
"build-commands": [
"mkdir -p %{install-root}/etc/test/",
'echo "Hello world!" > %{install-root}/etc/test/hello.txt',
]
},
}
os.makedirs(os.path.dirname(os.path.join(element_path, dep_name)), exist_ok=True)
_yaml.roundtrip_dump(dependency, os.path.join(element_path, dep_name))
# First open the workspace
res = cli.run(project=project, args=["workspace", "open", "--directory", workspace, element_name])
assert res.exit_code == 0
# We build the workspaced element, so that we have an artifact
# with specific built dependencies
res = cli.run(project=project, args=["build", element_name])
assert res.exit_code == 0
# Now we update a dependency of our element.
dependency["config"]["build-commands"] = [
"mkdir -p %{install-root}/etc/test/",
'echo "Hello china!" > %{install-root}/etc/test/hello.txt',
]
_yaml.roundtrip_dump(dependency, os.path.join(element_path, dep_name))
# `Make` would look at timestamps and normally not realize that
# our dependency's header files changed. BuildStream must
# therefore ensure that we change the mtimes of any files touched
# since the last successful build of this element, otherwise this
# build will fail.
res = cli.run(project=project, args=["build", element_name])
assert res.exit_code == 0
res = cli.run(project=project, args=["shell", element_name, "/usr/bin/test.sh"])
assert res.exit_code == 0
assert res.output == "Hello china!\n\n"
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_workspace_update_dependency_failed(cli, datafiles):
project = str(datafiles)
workspace = os.path.join(cli.directory, "workspace")
element_path = os.path.join(project, "elements")
element_name = "workspace/workspace-updated-dependency-failed.bst"
dep_name = "workspace/dependency.bst"
dependency = {
"kind": "manual",
"depends": [{"filename": "base.bst", "type": "build"}],
"config": {
"build-commands": [
"mkdir -p %{install-root}/etc/test/",
'echo "Hello world!" > %{install-root}/etc/test/hello.txt',
'echo "Hello brazil!" > %{install-root}/etc/test/brazil.txt',
]
},
}
os.makedirs(os.path.dirname(os.path.join(element_path, dep_name)), exist_ok=True)
_yaml.roundtrip_dump(dependency, os.path.join(element_path, dep_name))
# First open the workspace
res = cli.run(project=project, args=["workspace", "open", "--directory", workspace, element_name])
assert res.exit_code == 0
# We build the workspaced element, so that we have an artifact
# with specific built dependencies
res = cli.run(project=project, args=["build", element_name])
assert res.exit_code == 0
# Now we update a dependency of our element.
dependency["config"]["build-commands"] = [
"mkdir -p %{install-root}/etc/test/",
'echo "Hello china!" > %{install-root}/etc/test/hello.txt',
'echo "Hello brazil!" > %{install-root}/etc/test/brazil.txt',
]
_yaml.roundtrip_dump(dependency, os.path.join(element_path, dep_name))
# And our build fails!
with open(os.path.join(workspace, "Makefile"), "a", encoding="utf-8") as f:
f.write("\texit 1")
res = cli.run(project=project, args=["build", element_name])
assert res.exit_code != 0
# We update our dependency again...
dependency["config"]["build-commands"] = [
"mkdir -p %{install-root}/etc/test/",
'echo "Hello world!" > %{install-root}/etc/test/hello.txt',
'echo "Hello spain!" > %{install-root}/etc/test/brazil.txt',
]
_yaml.roundtrip_dump(dependency, os.path.join(element_path, dep_name))
# And fix the source
with open(os.path.join(workspace, "Makefile"), "r", encoding="utf-8") as f:
makefile = f.readlines()
with open(os.path.join(workspace, "Makefile"), "w", encoding="utf-8") as f:
f.write("\n".join(makefile[:-1]))
# Since buildstream thinks hello.txt did not change, we could end
# up not rebuilding a file! We need to make sure that a case like
# this can't blind-side us.
res = cli.run(project=project, args=["build", element_name])
assert res.exit_code == 0
res = cli.run(project=project, args=["shell", element_name, "/usr/bin/test.sh"])
assert res.exit_code == 0
assert res.output == "Hello world!\nHello spain!\n\n"
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_updated_dependency_nested(cli, datafiles):
project = str(datafiles)
workspace = os.path.join(cli.directory, "workspace")
element_path = os.path.join(project, "elements")
element_name = "workspace/workspace-updated-dependency-nested.bst"
dep_name = "workspace/dependency.bst"
dependency = {
"kind": "manual",
"depends": [{"filename": "base.bst", "type": "build"}],
"config": {
"build-commands": [
"mkdir -p %{install-root}/etc/test/tests/",
'echo "Hello world!" > %{install-root}/etc/test/hello.txt',
'echo "Hello brazil!" > %{install-root}/etc/test/tests/brazil.txt',
]
},
}
os.makedirs(os.path.dirname(os.path.join(element_path, dep_name)), exist_ok=True)
_yaml.roundtrip_dump(dependency, os.path.join(element_path, dep_name))
# First open the workspace
res = cli.run(project=project, args=["workspace", "open", "--directory", workspace, element_name])
assert res.exit_code == 0
# We build the workspaced element, so that we have an artifact
# with specific built dependencies
res = cli.run(project=project, args=["build", element_name])
assert res.exit_code == 0
# Now we update a dependency of our element.
dependency["config"]["build-commands"] = [
"mkdir -p %{install-root}/etc/test/tests/",
'echo "Hello world!" > %{install-root}/etc/test/hello.txt',
'echo "Hello test!" > %{install-root}/etc/test/tests/tests.txt',
]
_yaml.roundtrip_dump(dependency, os.path.join(element_path, dep_name))
res = cli.run(project=project, args=["build", element_name])
assert res.exit_code == 0
# Buildstream should pick up the newly added element, and pick up
# the lack of the newly removed element
res = cli.run(project=project, args=["shell", element_name, "/usr/bin/test.sh"])
assert res.exit_code == 0
assert res.output == "Hello world!\nHello test!\n\n"
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_incremental_configure_commands_run_only_once(cli, datafiles):
project = str(datafiles)
workspace = os.path.join(cli.directory, "workspace")
element_path = os.path.join(project, "elements")
element_name = "workspace/incremental.bst"
element = {
"kind": "manual",
"depends": [{"filename": "base.bst", "type": "build"}],
"sources": [{"kind": "local", "path": "files/workspace-configure-only-once"}],
"config": {"configure-commands": ["$SHELL configure"]},
}
_yaml.roundtrip_dump(element, os.path.join(element_path, element_name))
# We open a workspace on the above element
res = cli.run(project=project, args=["workspace", "open", "--directory", workspace, element_name])
res.assert_success()
# Then we build, and check whether the configure step succeeded
res = cli.run(project=project, args=["--cache-buildtrees", "always", "build", element_name])
res.assert_success()
# check that the workspace was not configured outside the sandbox
assert not os.path.exists(os.path.join(workspace, "prepared"))
# the configure should have been run in the sandbox, so check the buildtree
res = cli.run(
project=project,
args=[
"shell",
"--build",
element_name,
"--use-buildtree",
"--",
"find",
".",
"-mindepth",
"1",
],
)
res.assert_success()
files = res.output.splitlines()
assert "./prepared" in files
assert not "./prepared-again" in files
# Add file to workspace to trigger an (incremental) build
with open(os.path.join(workspace, "newfile"), "w", encoding="utf-8"):
pass
# When we build again, the configure commands should not be
# called, and we should therefore exit cleanly (the configure
# commands are set to always fail after the first run)
res = cli.run(project=project, args=["--cache-buildtrees", "always", "build", element_name])
res.assert_success()
assert not os.path.exists(os.path.join(workspace, "prepared-again"))
res = cli.run(
project=project,
args=[
"shell",
"--build",
element_name,
"--use-buildtree",
"--",
"find",
".",
"-mindepth",
"1",
],
)
res.assert_success()
files = res.output.splitlines()
assert "./prepared" in files
assert not "./prepared-again" in files
# Test that rebuilding an already built workspaced element does
# not crash after the last successfully built artifact is removed
# from the cache
#
# A user can remove their artifact cache, or manually remove the
# artifact with `bst artifact delete`, or BuildStream can delete
# the last successfully built artifact for this workspace as a
# part of a cleanup job.
#
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_workspace_missing_last_successful(cli, datafiles):
project = str(datafiles)
workspace = os.path.join(cli.directory, "workspace")
element_name = "workspace/workspace-commanddir.bst"
# Open workspace
res = cli.run(project=project, args=["workspace", "open", "--directory", workspace, element_name])
assert res.exit_code == 0
# Build first, this will record the last successful build in local state
res = cli.run(project=project, args=["build", element_name])
assert res.exit_code == 0
# Remove the artifact from the cache, invalidating the last successful build
res = cli.run(project=project, args=["artifact", "delete", element_name])
assert res.exit_code == 0
# Build again, ensure we dont crash just because the artifact went missing
res = cli.run(project=project, args=["build", element_name])
assert res.exit_code == 0
# Check that we can still read failed workspace logs
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_workspace_failed_logs(cli, datafiles):
project = str(datafiles)
workspace = os.path.join(cli.directory, "failing_amhello")
element_name = "autotools/amhello-failure.bst"
# Open workspace
res = cli.run(project=project, args=["workspace", "open", "--directory", workspace, element_name])
res.assert_success()
# Try to build and ensure the build fails
res = cli.run(project=project, args=["build", element_name])
res.assert_main_error(ErrorDomain.STREAM, None)
assert cli.get_element_state(project, element_name) == "failed"
res = cli.run(project=project, args=["artifact", "log", element_name])
res.assert_success()
log = res.output
# Assert that we can get the log
assert log != ""
fail_str = "FAILURE {}: Running build-commands".format(element_name)
batch_fail_str = "FAILURE {}: Running commands".format(element_name)
assert fail_str in log or batch_fail_str in log
def get_buildtree_file_contents(cli, project, element_name, filename):
res = cli.run(
project=project,
args=[
"shell",
"--build",
element_name,
"--use-buildtree",
"--",
"cat",
filename,
],
)
res.assert_success()
return res.output
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_incremental(cli, datafiles):
project = str(datafiles)
workspace = os.path.join(cli.directory, "workspace")
element_path = os.path.join(project, "elements")
element_name = "workspace/incremental.bst"
element = {
"kind": "manual",
"depends": [{"filename": "base.bst", "type": "build"}],
"sources": [{"kind": "local", "path": "files/workspace-incremental"}],
"config": {"build-commands": ["make"]},
}
_yaml.roundtrip_dump(element, os.path.join(element_path, element_name))
# We open a workspace on the above element
res = cli.run(project=project, args=["workspace", "open", "--directory", workspace, element_name])
res.assert_success()
# Initial (non-incremental) build of the workspace
res = cli.run(project=project, args=["build", element_name])
res.assert_success()
# Save the random hash
random_hash = get_buildtree_file_contents(cli, project, element_name, "random")
# Verify the expected output file of the initial build
assert get_buildtree_file_contents(cli, project, element_name, "copy") == "1"
wait_for_cache_granularity()
# Replace source file contents with '2'
with open(os.path.join(workspace, "source"), "w", encoding="utf-8") as f:
f.write("2")
# Perform incremental build of the workspace
res = cli.run(project=project, args=["build", element_name])
res.assert_success()
# Verify that this was an incremental build by comparing the random hash
assert get_buildtree_file_contents(cli, project, element_name, "random") == random_hash
# Verify that the output file matches the new source file
assert get_buildtree_file_contents(cli, project, element_name, "copy") == "2"
wait_for_cache_granularity()
# Replace source file contents with '3', however, set an old mtime such
# that `make` will not pick up the change
with open(os.path.join(workspace, "source"), "w", encoding="utf-8") as f:
f.write("3")
os.utime(os.path.join(workspace, "source"), (BST_ARBITRARY_TIMESTAMP, BST_ARBITRARY_TIMESTAMP))
# Perform incremental build of the workspace
res = cli.run(project=project, args=["build", element_name])
res.assert_success()
# Verify that this was an incremental build by comparing the random hash
assert get_buildtree_file_contents(cli, project, element_name, "random") == random_hash
# Verify that the output file still matches the previous content '2'
assert get_buildtree_file_contents(cli, project, element_name, "copy") == "2"
# Test incremental build after partial build / build failure
@pytest.mark.datafiles(DATA_DIR)
@pytest.mark.skipif(not HAVE_SANDBOX, reason="Only available with a functioning sandbox")
def test_incremental_partial(cli, datafiles):
project = str(datafiles)
workspace = os.path.join(cli.directory, "workspace")
element_path = os.path.join(project, "elements")
element_name = "workspace/incremental.bst"
element = {
"kind": "manual",
"depends": [{"filename": "base.bst", "type": "build"}],
"sources": [{"kind": "local", "path": "files/workspace-partial"}],
"config": {"build-commands": ["make random", "make copy1", "make copy2"]},
}
_yaml.roundtrip_dump(element, os.path.join(element_path, element_name))
# We open a workspace on the above element
res = cli.run(project=project, args=["workspace", "open", "--directory", workspace, element_name])
res.assert_success()
# Initial (non-incremental) build of the workspace
res = cli.run(project=project, args=["build", element_name])
res.assert_success()
# Save the random hash
random_hash = get_buildtree_file_contents(cli, project, element_name, "random")
# Verify the expected output files of the initial build
assert get_buildtree_file_contents(cli, project, element_name, "copy1") == "1"
assert get_buildtree_file_contents(cli, project, element_name, "copy2") == "1"
wait_for_cache_granularity()
# Delete source1 and replace source2 file contents with '2'
os.unlink(os.path.join(workspace, "source1"))
with open(os.path.join(workspace, "source2"), "w", encoding="utf-8") as f:
f.write("2")
# Perform incremental build of the workspace
# This should fail because of the missing source1 file.
res = cli.run(project=project, args=["build", element_name])
res.assert_main_error(ErrorDomain.STREAM, None)
wait_for_cache_granularity()
# Recreate source1 file
with open(os.path.join(workspace, "source1"), "w", encoding="utf-8") as f:
f.write("2")
# Perform incremental build of the workspace
res = cli.run(project=project, args=["build", element_name])
res.assert_success()
# Verify that this was an incremental build by comparing the random hash
assert get_buildtree_file_contents(cli, project, element_name, "random") == random_hash
# Verify that both files got rebuilt
assert get_buildtree_file_contents(cli, project, element_name, "copy1") == "2"
assert get_buildtree_file_contents(cli, project, element_name, "copy2") == "2"