# # Copyright (C) 2018 Codethink Limited # Copyright (C) 2019 Bloomberg Finance LP # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . # # 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._exceptions import ErrorDomain from .._utils import generate_junction from .. import create_repo from .. import cli # pylint: disable=unused-import from .utils import update_project_configuration from .utils import kind # pylint: disable=unused-import # Project directory TOP_DIR = os.path.dirname(os.path.realpath(__file__)) DATA_DIR = os.path.join(TOP_DIR, "project") def generate_element(repo, element_path, dep_name=None): element = {"kind": "import", "sources": [repo.source_config()]} if dep_name: element["depends"] = [dep_name] _yaml.roundtrip_dump(element, element_path) @pytest.mark.datafiles(DATA_DIR) @pytest.mark.parametrize("ref_storage", ["inline", "project.refs"]) def test_track(cli, tmpdir, datafiles, ref_storage, kind): project = str(datafiles) dev_files_path = os.path.join(project, "files", "dev-files") element_path = os.path.join(project, "elements") element_name = "track-test-{}.bst".format(kind) update_project_configuration(project, {"ref-storage": ref_storage}) # Create our repo object of the given source type with # the dev files, and then collect the initial ref. # repo = create_repo(kind, str(tmpdir)) repo.create(dev_files_path) # Generate the element generate_element(repo, os.path.join(element_path, element_name)) # Assert that a fetch is needed assert cli.get_element_state(project, element_name) == "no reference" # Now first try to track it result = cli.run(project=project, args=["source", "track", element_name]) result.assert_success() # And now fetch it: The Source has probably already cached the # latest ref locally, but it is not required to have cached # the associated content of the latest ref at track time, that # is the job of fetch. result = cli.run(project=project, args=["source", "fetch", element_name]) result.assert_success() # Assert that we are now buildable because the source is # now cached. assert cli.get_element_state(project, element_name) == "buildable" # Assert there was a project.refs created, depending on the configuration if ref_storage == "project.refs": assert os.path.exists(os.path.join(project, "project.refs")) else: assert not os.path.exists(os.path.join(project, "project.refs")) # NOTE: # # This test checks that recursive tracking works by observing # element states after running a recursive tracking operation. # # However, this test is ALSO valuable as it stresses the source # plugins in a situation where many source plugins are operating # at once on the same backing repository. # # Do not change this test to use a separate 'Repo' per element # as that would defeat the purpose of the stress test, otherwise # please refactor that aspect into another test. # @pytest.mark.datafiles(DATA_DIR) @pytest.mark.parametrize("amount", [1, 10]) def test_track_recurse(cli, tmpdir, datafiles, kind, amount): project = str(datafiles) dev_files_path = os.path.join(project, "files", "dev-files") element_path = os.path.join(project, "elements") # Try to actually launch as many fetch jobs as possible at the same time # # This stresses the Source plugins and helps to ensure that # they handle concurrent access to the store correctly. cli.configure({"scheduler": {"fetchers": amount,}}) # Create our repo object of the given source type with # the dev files, and then collect the initial ref. # repo = create_repo(kind, str(tmpdir)) repo.create(dev_files_path) # Write out our test targets element_names = [] last_element_name = None for i in range(amount + 1): element_name = "track-test-{}-{}.bst".format(kind, i + 1) filename = os.path.join(element_path, element_name) element_names.append(element_name) generate_element(repo, filename, dep_name=last_element_name) last_element_name = element_name # Assert that a fetch is needed states = cli.get_element_states(project, [last_element_name]) for element_name in element_names: assert states[element_name] == "no reference" # Now first try to track it result = cli.run(project=project, args=["source", "track", "--deps", "all", last_element_name]) result.assert_success() # And now fetch it: The Source has probably already cached the # latest ref locally, but it is not required to have cached # the associated content of the latest ref at track time, that # is the job of fetch. result = cli.run(project=project, args=["source", "fetch", "--deps", "all", last_element_name]) result.assert_success() # Assert that the base is buildable and the rest are waiting states = cli.get_element_states(project, [last_element_name]) for element_name in element_names: if element_name == element_names[0]: assert states[element_name] == "buildable" else: assert states[element_name] == "waiting" @pytest.mark.datafiles(DATA_DIR) def test_track_recurse_except(cli, tmpdir, datafiles, kind): project = str(datafiles) dev_files_path = os.path.join(project, "files", "dev-files") element_path = os.path.join(project, "elements") element_dep_name = "track-test-dep-{}.bst".format(kind) element_target_name = "track-test-target-{}.bst".format(kind) # Create our repo object of the given source type with # the dev files, and then collect the initial ref. # repo = create_repo(kind, str(tmpdir)) repo.create(dev_files_path) # Write out our test targets generate_element(repo, os.path.join(element_path, element_dep_name)) generate_element(repo, os.path.join(element_path, element_target_name), dep_name=element_dep_name) # Assert that a fetch is needed states = cli.get_element_states(project, [element_target_name]) assert states[element_dep_name] == "no reference" assert states[element_target_name] == "no reference" # Now first try to track it result = cli.run( project=project, args=["source", "track", "--deps", "all", "--except", element_dep_name, element_target_name] ) result.assert_success() # And now fetch it: The Source has probably already cached the # latest ref locally, but it is not required to have cached # the associated content of the latest ref at track time, that # is the job of fetch. result = cli.run(project=project, args=["source", "fetch", "--deps", "none", element_target_name]) result.assert_success() # Assert that the dependency is buildable and the target is waiting states = cli.get_element_states(project, [element_target_name]) assert states[element_dep_name] == "no reference" assert states[element_target_name] == "waiting" @pytest.mark.datafiles(DATA_DIR) @pytest.mark.parametrize("ref_storage", ["inline", "project.refs"]) def test_cross_junction(cli, tmpdir, datafiles, ref_storage, kind): project = str(datafiles) subproject_path = os.path.join(project, "files", "sub-project") junction_path = os.path.join(project, "elements", "junction.bst") etc_files = os.path.join(subproject_path, "files", "etc-files") repo_element_path = os.path.join(subproject_path, "elements", "import-etc-repo.bst") update_project_configuration(project, {"ref-storage": ref_storage}) repo = create_repo(kind, str(tmpdir.join("element_repo"))) repo.create(etc_files) generate_element(repo, repo_element_path) generate_junction(str(tmpdir.join("junction_repo")), subproject_path, junction_path, store_ref=False) # Track the junction itself first. result = cli.run(project=project, args=["source", "track", "junction.bst"]) result.assert_success() assert cli.get_element_state(project, "junction.bst:import-etc-repo.bst") == "no reference" # Track the cross junction element. -J is not given, it is implied. result = cli.run(project=project, args=["source", "track", "junction.bst:import-etc-repo.bst"]) if ref_storage == "inline": # This is not allowed to track cross junction without project.refs. result.assert_main_error(ErrorDomain.PIPELINE, "untrackable-sources") else: result.assert_success() assert cli.get_element_state(project, "junction.bst:import-etc-repo.bst") == "buildable" assert os.path.exists(os.path.join(project, "project.refs")) @pytest.mark.datafiles(DATA_DIR) @pytest.mark.parametrize("ref_storage", ["inline", "project.refs"]) def test_track_include(cli, tmpdir, datafiles, ref_storage, kind): project = str(datafiles) dev_files_path = os.path.join(project, "files", "dev-files") element_path = os.path.join(project, "elements") element_name = "track-test-{}.bst".format(kind) update_project_configuration(project, {"ref-storage": ref_storage}) # Create our repo object of the given source type with # the dev files, and then collect the initial ref. # repo = create_repo(kind, str(tmpdir)) ref = repo.create(dev_files_path) # Generate the element element = {"kind": "import", "(@)": ["elements/sources.yml"]} sources = {"sources": [repo.source_config()]} _yaml.roundtrip_dump(element, os.path.join(element_path, element_name)) _yaml.roundtrip_dump(sources, os.path.join(element_path, "sources.yml")) # Assert that a fetch is needed assert cli.get_element_state(project, element_name) == "no reference" # Now first try to track it result = cli.run(project=project, args=["source", "track", element_name]) result.assert_success() # And now fetch it: The Source has probably already cached the # latest ref locally, but it is not required to have cached # the associated content of the latest ref at track time, that # is the job of fetch. result = cli.run(project=project, args=["source", "fetch", element_name]) result.assert_success() # Assert that we are now buildable because the source is # now cached. assert cli.get_element_state(project, element_name) == "buildable" # Assert there was a project.refs created, depending on the configuration if ref_storage == "project.refs": assert os.path.exists(os.path.join(project, "project.refs")) else: assert not os.path.exists(os.path.join(project, "project.refs")) new_sources = _yaml.load(os.path.join(element_path, "sources.yml")) # Get all of the sources assert "sources" in new_sources sources_list = new_sources.get_sequence("sources") assert len(sources_list) == 1 # Get the first source from the sources list new_source = sources_list.mapping_at(0) assert "ref" in new_source assert ref == new_source.get_str("ref") @pytest.mark.datafiles(DATA_DIR) @pytest.mark.parametrize("ref_storage", ["inline", "project.refs"]) def test_track_include_junction(cli, tmpdir, datafiles, ref_storage, kind): project = str(datafiles) dev_files_path = os.path.join(project, "files", "dev-files") element_path = os.path.join(project, "elements") element_name = "track-test-{}.bst".format(kind) subproject_path = os.path.join(project, "files", "sub-project") sub_element_path = os.path.join(subproject_path, "elements") junction_path = os.path.join(element_path, "junction.bst") update_project_configuration(project, {"ref-storage": ref_storage}) # Create our repo object of the given source type with # the dev files, and then collect the initial ref. # repo = create_repo(kind, str(tmpdir.join("element_repo"))) repo.create(dev_files_path) # Generate the element element = {"kind": "import", "(@)": ["junction.bst:elements/sources.yml"]} sources = {"sources": [repo.source_config()]} _yaml.roundtrip_dump(element, os.path.join(element_path, element_name)) _yaml.roundtrip_dump(sources, os.path.join(sub_element_path, "sources.yml")) generate_junction(str(tmpdir.join("junction_repo")), subproject_path, junction_path, store_ref=True) result = cli.run(project=project, args=["source", "track", "junction.bst"]) result.assert_success() # Assert that a fetch is needed assert cli.get_element_state(project, element_name) == "no reference" # Now first try to track it result = cli.run(project=project, args=["source", "track", element_name]) # Assert there was a project.refs created, depending on the configuration if ref_storage == "inline": # FIXME: We should expect an error. But only a warning is emitted # result.assert_main_error(ErrorDomain.SOURCE, 'tracking-junction-fragment') assert "junction.bst:elements/sources.yml: Cannot track source in a fragment from a junction" in result.stderr else: assert os.path.exists(os.path.join(project, "project.refs")) # And now fetch it: The Source has probably already cached the # latest ref locally, but it is not required to have cached # the associated content of the latest ref at track time, that # is the job of fetch. result = cli.run(project=project, args=["source", "fetch", element_name]) result.assert_success() # Assert that we are now buildable because the source is # now cached. assert cli.get_element_state(project, element_name) == "buildable" @pytest.mark.datafiles(DATA_DIR) @pytest.mark.parametrize("ref_storage", ["inline", "project.refs"]) def test_track_junction_included(cli, tmpdir, datafiles, ref_storage, kind): project = str(datafiles) element_path = os.path.join(project, "elements") subproject_path = os.path.join(project, "files", "sub-project") junction_path = os.path.join(element_path, "junction.bst") update_project_configuration(project, {"ref-storage": ref_storage, "(@)": ["junction.bst:test.yml"]}) generate_junction(str(tmpdir.join("junction_repo")), subproject_path, junction_path, store_ref=False) result = cli.run(project=project, args=["source", "track", "junction.bst"]) result.assert_success()