blob: 81e1e629c7adccaeaad63eed07f3f9e8ea720226 [file] [log] [blame]
# Pylint doesn't play well with fixtures and dependency injection from pytest
# pylint: disable=redefined-outer-name
import os
import sys
import shutil
import itertools
import pytest
from buildstream.testing import cli # pylint: disable=unused-import
from buildstream import _yaml
from buildstream.exceptions import ErrorDomain, LoadErrorReason
from tests.testutils import generate_junction
from . import configure_project
# Project directory
DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)),)
@pytest.mark.datafiles(os.path.join(DATA_DIR, "project"))
@pytest.mark.parametrize(
"target,fmt,expected",
[
("import-bin.bst", "%{name}", "import-bin.bst"),
("import-bin.bst", "%{state}", "buildable"),
("compose-all.bst", "%{state}", "waiting"),
],
)
def test_show(cli, datafiles, target, fmt, expected):
project = str(datafiles)
result = cli.run(project=project, silent=True, args=["show", "--deps", "none", "--format", fmt, target])
result.assert_success()
if result.output.strip() != expected:
raise AssertionError("Expected output:\n{}\nInstead received output:\n{}".format(expected, result.output))
@pytest.mark.datafiles(os.path.join(os.path.dirname(os.path.realpath(__file__)), "invalid_element_path",))
def test_show_invalid_element_path(cli, datafiles):
project = str(datafiles)
cli.run(project=project, silent=True, args=["show", "foo.bst"])
@pytest.mark.datafiles(os.path.join(DATA_DIR, "project_fail"))
def test_show_fail(cli, datafiles):
project = str(datafiles)
result = cli.run(project=project, silent=True, args=["show"])
result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.INVALID_DATA)
# Test behaviors of user supplied glob patterns
@pytest.mark.datafiles(os.path.join(DATA_DIR, "simple"))
@pytest.mark.parametrize(
"pattern,expected_elements",
[
# Use catch all glob. This should report all elements.
#
("**", ["import-bin.bst", "import-dev.bst", "compose-all.bst", "target.bst", "subdir/target.bst"]),
# Only bst files, same as "**" for `bst show`
#
("**.bst", ["import-bin.bst", "import-dev.bst", "compose-all.bst", "target.bst", "subdir/target.bst"]),
# Use regular globbing without matching path separators, this should exclude
# the target in the subdirectory.
#
("*.bst", ["import-bin.bst", "import-dev.bst", "compose-all.bst", "target.bst"]),
# Report only targets in the subdirectory
#
("subdir/*", ["subdir/target.bst"]),
# Report both targets which end in "target.bst"
#
("**target.bst", ["target.bst", "subdir/target.bst"]),
# All elements starting with the prefix "import"
#
("import*", ["import-bin.bst", "import-dev.bst"]),
# Glob would match artifact refs, but `bst show` does not accept these as input.
#
("test/**", []),
],
ids=["**", "**.bst", "*.bst", "subdir/*", "**target.bst", "import*", "test/**"],
)
def test_show_glob(cli, tmpdir, datafiles, pattern, expected_elements):
project = str(datafiles)
result = cli.run(project=project, args=["show", "--deps", "none", "--format", "%{name}", pattern])
result.assert_success()
output = result.output.strip().splitlines()
# Assert that the number of results match the number of expected results
assert len(output) == len(expected_elements)
# Assert that each expected result was found.
for expected in expected_elements:
assert expected in output, "Expected result {} not found".format(expected)
@pytest.mark.datafiles(os.path.join(DATA_DIR, "project"))
@pytest.mark.parametrize(
"target,except_,expected",
[
("target.bst", "import-bin.bst", ["import-dev.bst", "compose-all.bst", "target.bst"]),
("target.bst", "import-dev.bst", ["import-bin.bst", "compose-all.bst", "target.bst"]),
("target.bst", "compose-all.bst", ["import-bin.bst", "target.bst"]),
("compose-all.bst", "import-bin.bst", ["import-dev.bst", "compose-all.bst"]),
],
)
def test_show_except_simple(cli, datafiles, target, except_, expected):
project = str(datafiles)
result = cli.run(
project=project,
silent=True,
args=["show", "--deps", "all", "--format", "%{name}", "--except", except_, target],
)
result.assert_success()
results = result.output.strip().splitlines()
if results != expected:
raise AssertionError("Expected elements:\n{}\nInstead received elements:\n{}".format(expected, results))
# This test checks various constructions of a pipeline
# with one or more targets and 0 or more exception elements,
# each data set provides the targets, exceptions and expected
# result list.
#
@pytest.mark.datafiles(os.path.join(DATA_DIR, "exceptions"))
@pytest.mark.parametrize(
"targets,exceptions,expected",
[
# Test without exceptions, lets just see the whole list here
(
["build.bst"],
None,
[
"fourth-level-1.bst",
"third-level-1.bst",
"fourth-level-2.bst",
"third-level-2.bst",
"fourth-level-3.bst",
"third-level-3.bst",
"second-level-1.bst",
"first-level-1.bst",
"first-level-2.bst",
"build.bst",
],
),
# Test one target and excepting a part of the pipeline, this
# removes forth-level-1 and third-level-1
(
["build.bst"],
["third-level-1.bst"],
[
"fourth-level-2.bst",
"third-level-2.bst",
"fourth-level-3.bst",
"third-level-3.bst",
"second-level-1.bst",
"first-level-1.bst",
"first-level-2.bst",
"build.bst",
],
),
# Test one target and excepting a part of the pipeline, check that
# excepted dependencies remain in the pipeline if depended on from
# outside of the except element
(
["build.bst"],
["second-level-1.bst"],
[
"fourth-level-2.bst",
"third-level-2.bst", # first-level-2 depends on this, so not excepted
"first-level-1.bst",
"first-level-2.bst",
"build.bst",
],
),
# The same as the above test, but excluding the toplevel build.bst,
# instead only select the two toplevel dependencies as targets
(
["first-level-1.bst", "first-level-2.bst"],
["second-level-1.bst"],
[
"fourth-level-2.bst",
"third-level-2.bst", # first-level-2 depends on this, so not excepted
"first-level-1.bst",
"first-level-2.bst",
],
),
# Test one target and excepting an element outisde the pipeline
(
["build.bst"],
["unrelated-1.bst"],
[
"fourth-level-2.bst",
"third-level-2.bst", # first-level-2 depends on this, so not excepted
"first-level-1.bst",
"first-level-2.bst",
"build.bst",
],
),
# Test one target and excepting two elements
(["build.bst"], ["unrelated-1.bst", "unrelated-2.bst"], ["first-level-1.bst", "build.bst",]),
],
)
def test_show_except(cli, datafiles, targets, exceptions, expected):
basedir = str(datafiles)
results = cli.get_pipeline(basedir, targets, except_=exceptions, scope="all")
if results != expected:
raise AssertionError("Expected elements:\n{}\nInstead received elements:\n{}".format(expected, results))
###############################################################
# Testing multiple targets #
###############################################################
@pytest.mark.datafiles(os.path.join(DATA_DIR, "project"))
def test_parallel_order(cli, datafiles):
project = str(datafiles)
elements = ["multiple_targets/order/0.bst", "multiple_targets/order/1.bst"]
args = ["show", "-d", "plan", "-f", "%{name}", *elements]
result = cli.run(project=project, args=args)
result.assert_success()
# Get the planned order
names = result.output.splitlines()
names = [name[len("multiple_targets/order/") :] for name in names]
# Create all possible 'correct' topological orderings
orderings = itertools.product(
[("5.bst", "6.bst")],
itertools.permutations(["4.bst", "7.bst"]),
itertools.permutations(["3.bst", "8.bst"]),
itertools.permutations(["2.bst", "9.bst"]),
itertools.permutations(["0.bst", "1.bst", "run.bst"]),
)
orderings = [list(itertools.chain.from_iterable(perm)) for perm in orderings]
# Ensure that our order is among the correct orderings
assert names in orderings, "We got: {}".format(", ".join(names))
@pytest.mark.datafiles(os.path.join(DATA_DIR, "project"))
def test_target_is_dependency(cli, datafiles):
project = str(datafiles)
elements = ["multiple_targets/dependency/zebry.bst", "multiple_targets/dependency/horsey.bst"]
args = ["show", "-d", "plan", "-f", "%{name}", *elements]
result = cli.run(project=project, args=args)
result.assert_success()
# Get the planned order
names = result.output.splitlines()
names = [name[len("multiple_targets/dependency/") :] for name in names]
assert names == ["pony.bst", "horsey.bst", "zebry.bst"]
@pytest.mark.datafiles(os.path.join(DATA_DIR, "project"))
@pytest.mark.parametrize("ref_storage", [("inline"), ("project.refs")])
@pytest.mark.parametrize("element_name", ["junction-dep.bst", "junction.bst:import-etc.bst"])
@pytest.mark.parametrize("workspaced", [True, False], ids=["workspace", "no-workspace"])
def test_unfetched_junction(cli, tmpdir, datafiles, ref_storage, element_name, workspaced):
project = str(datafiles)
subproject_path = os.path.join(project, "files", "sub-project")
junction_path = os.path.join(project, "elements", "junction.bst")
element_path = os.path.join(project, "elements", "junction-dep.bst")
configure_project(project, {"ref-storage": ref_storage})
# Create a repo to hold the subproject and generate a junction element for it
ref = generate_junction(tmpdir, subproject_path, junction_path, store_ref=(ref_storage == "inline"))
# Create a stack element to depend on a cross junction element
#
element = {"kind": "stack", "depends": [{"junction": "junction.bst", "filename": "import-etc.bst"}]}
_yaml.roundtrip_dump(element, element_path)
# Dump a project.refs if we're using project.refs storage
#
if ref_storage == "project.refs":
project_refs = {"projects": {"test": {"junction.bst": [{"ref": ref}]}}}
_yaml.roundtrip_dump(project_refs, os.path.join(project, "junction.refs"))
# Open a workspace if we're testing workspaced behavior
if workspaced:
result = cli.run(
project=project,
silent=True,
args=["workspace", "open", "--no-checkout", "--directory", subproject_path, "junction.bst"],
)
result.assert_success()
# Assert successful bst show (requires implicit subproject fetching)
result = cli.run(project=project, silent=True, args=["show", element_name])
result.assert_success()
@pytest.mark.datafiles(os.path.join(DATA_DIR, "project"))
@pytest.mark.parametrize("ref_storage", [("inline"), ("project.refs")])
@pytest.mark.parametrize("workspaced", [True, False], ids=["workspace", "no-workspace"])
def test_inconsistent_junction(cli, tmpdir, datafiles, ref_storage, workspaced):
project = str(datafiles)
subproject_path = os.path.join(project, "files", "sub-project")
junction_path = os.path.join(project, "elements", "junction.bst")
element_path = os.path.join(project, "elements", "junction-dep.bst")
configure_project(project, {"ref-storage": ref_storage})
# Create a repo to hold the subproject and generate a junction element for it
generate_junction(tmpdir, subproject_path, junction_path, store_ref=False)
# Create a stack element to depend on a cross junction element
#
element = {"kind": "stack", "depends": [{"junction": "junction.bst", "filename": "import-etc.bst"}]}
_yaml.roundtrip_dump(element, element_path)
# Open a workspace if we're testing workspaced behavior
if workspaced:
result = cli.run(
project=project,
silent=True,
args=["workspace", "open", "--no-checkout", "--directory", subproject_path, "junction.bst"],
)
result.assert_success()
# Assert the correct error when trying to show the pipeline
dep_result = cli.run(project=project, silent=True, args=["show", "junction-dep.bst"])
# Assert the correct error when trying to show the pipeline
etc_result = cli.run(project=project, silent=True, args=["show", "junction.bst:import-etc.bst"])
# If a workspace is open, no ref is needed
if workspaced:
dep_result.assert_success()
etc_result.assert_success()
else:
# Assert that we have the expected provenance encoded into the error
element_node = _yaml.load(element_path, shortname="junction-dep.bst")
ref_node = element_node.get_sequence("depends").mapping_at(0)
provenance = ref_node.get_provenance()
assert str(provenance) in dep_result.stderr
dep_result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.SUBPROJECT_INCONSISTENT)
etc_result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.SUBPROJECT_INCONSISTENT)
@pytest.mark.datafiles(os.path.join(DATA_DIR, "project"))
@pytest.mark.parametrize("element_name", ["junction-dep.bst", "junction.bst:import-etc.bst"])
@pytest.mark.parametrize("workspaced", [True, False], ids=["workspace", "no-workspace"])
def test_fetched_junction(cli, tmpdir, datafiles, element_name, workspaced):
project = str(datafiles)
project = os.path.join(datafiles.dirname, datafiles.basename)
subproject_path = os.path.join(project, "files", "sub-project")
junction_path = os.path.join(project, "elements", "junction.bst")
element_path = os.path.join(project, "elements", "junction-dep.bst")
# Create a repo to hold the subproject and generate a junction element for it
generate_junction(tmpdir, subproject_path, junction_path, store_ref=True)
# Create a stack element to depend on a cross junction element
#
element = {"kind": "stack", "depends": [{"junction": "junction.bst", "filename": "import-etc.bst"}]}
_yaml.roundtrip_dump(element, element_path)
result = cli.run(project=project, silent=True, args=["source", "fetch", "junction.bst"])
result.assert_success()
# Open a workspace if we're testing workspaced behavior
if workspaced:
result = cli.run(
project=project,
silent=True,
args=["workspace", "open", "--no-checkout", "--directory", subproject_path, "junction.bst"],
)
result.assert_success()
# Assert the correct error when trying to show the pipeline
result = cli.run(project=project, silent=True, args=["show", "--format", "%{name}-%{state}", element_name])
results = result.output.strip().splitlines()
assert "junction.bst:import-etc.bst-buildable" in results
###############################################################
# Testing recursion depth #
###############################################################
@pytest.mark.parametrize("dependency_depth", [100, 150, 1200])
def test_exceed_max_recursion_depth(cli, tmpdir, dependency_depth):
project_name = "recursion-test"
path = str(tmpdir)
project_path = os.path.join(path, project_name)
def setup_test():
"""
Creates a bst project with dependencydepth + 1 elements, each of which
depends of the previous element to be created. Each element created
is of type import and has an empty source file.
"""
os.mkdir(project_path)
result = cli.run(silent=True, args=["init", "--project-name", project_name, project_path])
result.assert_success()
sourcefiles_path = os.path.join(project_path, "files")
os.mkdir(sourcefiles_path)
element_path = os.path.join(project_path, "elements")
for i in range(0, dependency_depth + 1):
element = {
"kind": "import",
"sources": [{"kind": "local", "path": "files/source{}".format(str(i))}],
"depends": ["element{}.bst".format(str(i - 1))],
}
if i == 0:
del element["depends"]
_yaml.roundtrip_dump(element, os.path.join(element_path, "element{}.bst".format(str(i))))
source = os.path.join(sourcefiles_path, "source{}".format(str(i)))
open(source, "x").close()
assert os.path.exists(source)
setup_test()
result = cli.run(project=project_path, silent=True, args=["show", "element{}.bst".format(str(dependency_depth))])
recursion_limit = sys.getrecursionlimit()
if dependency_depth <= recursion_limit:
result.assert_success()
else:
# Assert exception is thown and handled
assert not result.unhandled_exception
assert result.exit_code == -1
shutil.rmtree(project_path)
###############################################################
# Testing format symbols #
###############################################################
@pytest.mark.datafiles(os.path.join(DATA_DIR, "project"))
@pytest.mark.parametrize(
"dep_kind, expected_deps",
[
("%{deps}", "[import-dev.bst, import-links.bst, import-bin.bst]"),
("%{build-deps}", "[import-dev.bst, import-links.bst]"),
("%{runtime-deps}", "[import-links.bst, import-bin.bst]"),
],
)
def test_format_deps(cli, datafiles, dep_kind, expected_deps):
project = str(datafiles)
target = "format-deps.bst"
result = cli.run(
project=project, silent=True, args=["show", "--deps", "none", "--format", "%{name}: " + dep_kind, target]
)
result.assert_success()
expected = "{name}: {deps}".format(name=target, deps=expected_deps)
if result.output.strip() != expected:
raise AssertionError("Expected output:\n{}\nInstead received output:\n{}".format(expected, result.output))
# This tests the resolved value of the 'max-jobs' variable,
# ensuring at least that the variables are resolved according
# to how the user has configured max-jobs
#
@pytest.mark.datafiles(os.path.join(DATA_DIR, "project"))
@pytest.mark.parametrize(
"cli_value, config_value", [(None, None), (None, "16"), ("16", None), ("5", "16"), ("0", "16"), ("16", "0"),]
)
def test_max_jobs(cli, datafiles, cli_value, config_value):
project = str(datafiles)
target = "target.bst"
# Specify `--max-jobs` if this test sets it
args = []
if cli_value is not None:
args += ["--max-jobs", cli_value]
args += ["show", "--deps", "none", "--format", "%{vars}", target]
# Specify `max-jobs` in user configuration if this test sets it
if config_value is not None:
cli.configure({"build": {"max-jobs": config_value}})
result = cli.run(project=project, silent=True, args=args)
result.assert_success()
loaded = _yaml.load_data(result.output)
loaded_value = loaded.get_int("max-jobs")
# We expect the value provided on the command line to take
# precedence over the configuration file value, if specified.
#
# If neither are specified then we expect the default
expected_value = cli_value or config_value or "0"
if expected_value == "0":
# If we are expecting the automatic behavior of using the maximum
# number of cores available, just check that it is a value > 0
assert loaded_value > 0, "Automatic setting of max-jobs didnt work"
else:
# Check that we got the explicitly set value
assert loaded_value == int(expected_value)
# This tests that cache keys behave as expected when
# dependencies have been specified as `strict` and
# when building in strict mode.
#
# This test will:
#
# * Build the target once (and assert that it is cached)
# * Modify some local files which are imported
# by an import element which the target depends on
# * Assert that the cached state of the target element
# is as expected
#
# We run the test twice, once with an element which strict
# depends on the changing import element, and one which
# depends on it regularly.
#
@pytest.mark.datafiles(os.path.join(DATA_DIR, "strict-depends"))
@pytest.mark.parametrize(
"target, expected_state", [("non-strict-depends.bst", "cached"), ("strict-depends.bst", "waiting"),]
)
def test_strict_dependencies(cli, datafiles, target, expected_state):
project = str(datafiles)
# Configure non strict mode, this will have
# an effect on the build and the `bst show`
# commands run via cli.get_element_states()
cli.configure({"projects": {"test": {"strict": False}}})
result = cli.run(project=project, silent=True, args=["build", target])
result.assert_success()
states = cli.get_element_states(project, ["base.bst", target])
assert states["base.bst"] == "cached"
assert states[target] == "cached"
# Now modify the file, effectively causing the common base.bst
# dependency to change it's cache key
hello_path = os.path.join(project, "files", "hello.txt")
with open(hello_path, "w") as f:
f.write("Goodbye")
# Now assert that we have the states we expect as a result
states = cli.get_element_states(project, ["base.bst", target])
assert states["base.bst"] == "buildable"
assert states[target] == expected_state