# Pylint doesn't play well with fixtures and dependency injection from pytest # pylint: disable=redefined-outer-name import os import re import shutil import itertools import pytest from buildstream import _yaml from buildstream.testing import create_repo from buildstream.testing import cli # pylint: disable=unused-import from buildstream._exceptions import ErrorDomain from tests.testutils import generate_junction from . import configure_project # Project directory DATA_DIR = os.path.join( os.path.dirname(os.path.realpath(__file__)), "project", ) def create_element(repo, name, path, dependencies, ref=None): element = { 'kind': 'import', 'sources': [ repo.source_config(ref=ref) ], 'depends': dependencies } _yaml.roundtrip_dump(element, os.path.join(path, name)) @pytest.mark.datafiles(os.path.join(DATA_DIR)) @pytest.mark.parametrize("strict", [True, False], ids=["strict", "no-strict"]) @pytest.mark.parametrize("ref_storage", [('inline'), ('project.refs')]) @pytest.mark.parametrize("track_targets,exceptions,tracked", [ # Test with no exceptions (['0.bst'], [], ['0.bst', '2.bst', '3.bst', '4.bst', '5.bst', '6.bst', '7.bst']), (['3.bst'], [], ['3.bst', '4.bst', '5.bst', '6.bst']), (['2.bst', '3.bst'], [], ['2.bst', '3.bst', '4.bst', '5.bst', '6.bst', '7.bst']), # Test excepting '2.bst' (['0.bst'], ['2.bst'], ['0.bst', '3.bst', '4.bst', '5.bst', '6.bst']), (['3.bst'], ['2.bst'], []), (['2.bst', '3.bst'], ['2.bst'], ['3.bst', '4.bst', '5.bst', '6.bst']), # Test excepting '2.bst' and '3.bst' (['0.bst'], ['2.bst', '3.bst'], ['0.bst']), (['3.bst'], ['2.bst', '3.bst'], []), (['2.bst', '3.bst'], ['2.bst', '3.bst'], []) ]) def test_build_track(cli, datafiles, tmpdir, ref_storage, strict, track_targets, exceptions, tracked): project = str(datafiles) dev_files_path = os.path.join(project, 'files', 'dev-files') element_path = os.path.join(project, 'elements') repo = create_repo('git', str(tmpdir)) ref = repo.create(dev_files_path) configure_project(project, { 'ref-storage': ref_storage }) cli.configure({ 'projects': { 'test': { 'strict': strict } } }) create_elements = { '0.bst': [ '2.bst', '3.bst' ], '2.bst': [ '3.bst', '7.bst' ], '3.bst': [ '4.bst', '5.bst', '6.bst' ], '4.bst': [], '5.bst': [], '6.bst': [ '5.bst' ], '7.bst': [] } initial_project_refs = {} for element, dependencies in create_elements.items(): # Test the element inconsistency resolution by ensuring that # only elements that aren't tracked have refs if element in set(tracked): # Elements which should not have a ref set # create_element(repo, element, element_path, dependencies) elif ref_storage == 'project.refs': # Store a ref in project.refs # create_element(repo, element, element_path, dependencies) initial_project_refs[element] = [{'ref': ref}] else: # Store a ref in the element itself # create_element(repo, element, element_path, dependencies, ref=ref) # Generate initial project.refs if ref_storage == 'project.refs': project_refs = { 'projects': { 'test': initial_project_refs } } _yaml.roundtrip_dump(project_refs, os.path.join(project, 'project.refs')) args = ['build'] args += itertools.chain.from_iterable(zip(itertools.repeat('--track'), track_targets)) args += itertools.chain.from_iterable(zip(itertools.repeat('--track-except'), exceptions)) args += ['0.bst'] result = cli.run(project=project, silent=True, args=args) result.assert_success() # Assert that the main target 0.bst is cached assert cli.get_element_state(project, '0.bst') == 'cached' # Assert that we tracked exactly the elements we expected to tracked_elements = result.get_tracked_elements() assert set(tracked_elements) == set(tracked) # Delete element sources source_dir = os.path.join(project, 'cache', 'sources') shutil.rmtree(source_dir) source_protos = os.path.join(project, 'cache', 'source_protos') shutil.rmtree(source_protos) # Delete artifacts one by one and assert element states for target in set(tracked): cli.remove_artifact_from_cache(project, target) # Assert that it's tracked assert cli.get_element_state(project, target) == 'fetch needed' # 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')) # This tests a very specific scenario: # # o Local cache is empty # o Strict mode is disabled # o The build target has only build dependencies # o The build is launched with --track-all # # In this scenario, we have encountered bugs where BuildStream returns # successfully after tracking completes without ever pulling, fetching or # building anything. # @pytest.mark.datafiles(DATA_DIR) @pytest.mark.parametrize("strict", [True, False], ids=["strict", "no-strict"]) @pytest.mark.parametrize("ref_storage", [('inline'), ('project.refs')]) def test_build_track_all(cli, tmpdir, datafiles, strict, ref_storage): project = os.path.join(datafiles.dirname, datafiles.basename) subproject_path = os.path.join(project, 'files', 'sub-project') subproject_element_path = os.path.join(project, 'files', 'sub-project', 'elements') junction_path = os.path.join(project, 'elements', 'junction.bst') element_path = os.path.join(project, 'elements') dev_files_path = os.path.join(project, 'files', 'dev-files') configure_project(project, { 'ref-storage': ref_storage }) cli.configure({ 'projects': { 'test': { 'strict': strict } } }) # We need a repo for real trackable elements repo = create_repo('git', str(tmpdir)) ref = repo.create(dev_files_path) # Create a trackable element to depend on the cross junction element, # this one has it's ref resolved already create_element(repo, 'sub-target.bst', subproject_element_path, ['import-etc.bst'], ref=ref) # Create a trackable element to depend on the cross junction element create_element(repo, 'target.bst', element_path, [ { 'junction': 'junction.bst', 'filename': 'sub-target.bst' } ]) # Create a repo to hold the subproject and generate a junction element for it generate_junction(tmpdir, subproject_path, junction_path, store_ref=False) # Now create a compose element at the top level element = { 'kind': 'compose', 'depends': [ { 'filename': 'target.bst', 'type': 'build' } ] } _yaml.roundtrip_dump(element, os.path.join(element_path, 'composed.bst')) # Track the junction itself first. result = cli.run(project=project, args=['source', 'track', 'junction.bst']) result.assert_success() # Build it with --track-all result = cli.run(project=project, silent=True, args=['build', '--track-all', 'composed.bst']) result.assert_success() # Assert that the main target is cached as a result assert cli.get_element_state(project, 'composed.bst') == 'cached' @pytest.mark.datafiles(os.path.join(DATA_DIR)) @pytest.mark.parametrize("track_targets,exceptions,tracked", [ # Test with no exceptions (['0.bst'], [], ['0.bst', '2.bst', '3.bst', '4.bst', '5.bst', '6.bst', '7.bst']), (['3.bst'], [], ['3.bst', '4.bst', '5.bst', '6.bst']), (['2.bst', '3.bst'], [], ['2.bst', '3.bst', '4.bst', '5.bst', '6.bst', '7.bst']), # Test excepting '2.bst' (['0.bst'], ['2.bst'], ['0.bst', '3.bst', '4.bst', '5.bst', '6.bst']), (['3.bst'], ['2.bst'], []), (['2.bst', '3.bst'], ['2.bst'], ['3.bst', '4.bst', '5.bst', '6.bst']), # Test excepting '2.bst' and '3.bst' (['0.bst'], ['2.bst', '3.bst'], ['0.bst']), (['3.bst'], ['2.bst', '3.bst'], []), (['2.bst', '3.bst'], ['2.bst', '3.bst'], []) ]) def test_build_track_update(cli, datafiles, tmpdir, track_targets, exceptions, tracked): project = str(datafiles) dev_files_path = os.path.join(project, 'files', 'dev-files') element_path = os.path.join(project, 'elements') repo = create_repo('git', str(tmpdir)) ref = repo.create(dev_files_path) create_elements = { '0.bst': [ '2.bst', '3.bst' ], '2.bst': [ '3.bst', '7.bst' ], '3.bst': [ '4.bst', '5.bst', '6.bst' ], '4.bst': [], '5.bst': [], '6.bst': [ '5.bst' ], '7.bst': [] } for element, dependencies in create_elements.items(): # We set a ref for all elements, so that we ensure that we # only track exactly those elements that we want to track, # even if others can be tracked create_element(repo, element, element_path, dependencies, ref=ref) repo.add_commit() args = ['build'] args += itertools.chain.from_iterable(zip(itertools.repeat('--track'), track_targets)) args += itertools.chain.from_iterable(zip(itertools.repeat('--track-except'), exceptions)) args += ['0.bst'] result = cli.run(project=project, silent=True, args=args) tracked_elements = result.get_tracked_elements() assert set(tracked_elements) == set(tracked) @pytest.mark.datafiles(os.path.join(DATA_DIR)) @pytest.mark.parametrize("track_targets,exceptions", [ # Test tracking the main target element, but excepting some of its # children (['0.bst'], ['6.bst']), # Test only tracking a child element (['3.bst'], []), ]) def test_build_track_inconsistent(cli, datafiles, tmpdir, track_targets, exceptions): project = str(datafiles) dev_files_path = os.path.join(project, 'files', 'dev-files') element_path = os.path.join(project, 'elements') repo = create_repo('git', str(tmpdir)) repo.create(dev_files_path) create_elements = { '0.bst': [ '2.bst', '3.bst' ], '2.bst': [ '3.bst', '7.bst' ], '3.bst': [ '4.bst', '5.bst', '6.bst' ], '4.bst': [], '5.bst': [], '6.bst': [ '5.bst' ], '7.bst': [] } for element, dependencies in create_elements.items(): # We don't add refs so that all elements *have* to be tracked create_element(repo, element, element_path, dependencies) args = ['build'] args += itertools.chain.from_iterable(zip(itertools.repeat('--track'), track_targets)) args += itertools.chain.from_iterable(zip(itertools.repeat('--track-except'), exceptions)) args += ['0.bst'] result = cli.run(project=project, args=args, silent=True) result.assert_main_error(ErrorDomain.PIPELINE, "inconsistent-pipeline") # Assert that if a build element has a dependency in the tracking # queue it does not start building before tracking finishes. @pytest.mark.datafiles(os.path.join(DATA_DIR)) @pytest.mark.parametrize("strict", ['--strict', '--no-strict']) def test_build_track_track_first(cli, datafiles, tmpdir, strict): project = str(datafiles) dev_files_path = os.path.join(project, 'files', 'dev-files') element_path = os.path.join(project, 'elements') repo = create_repo('git', str(tmpdir)) ref = repo.create(dev_files_path) create_elements = { '0.bst': [ '1.bst' ], '1.bst': [], '2.bst': [ '0.bst' ] } for element, dependencies in create_elements.items(): # We set a ref so that 0.bst can already be built even if # 1.bst has not been tracked yet. create_element(repo, element, element_path, dependencies, ref=ref) repo.add_commit() # Build 1.bst and 2.bst first so we have an artifact for them args = [strict, 'build', '2.bst'] result = cli.run(args=args, project=project, silent=True) result.assert_success() # Test building 0.bst while tracking 1.bst cli.remove_artifact_from_cache(project, '0.bst') args = [strict, 'build', '--track', '1.bst', '2.bst'] result = cli.run(args=args, project=project, silent=True) result.assert_success() # Assert that 1.bst successfully tracks before 0.bst builds track_messages = re.finditer(r'\[track:1.bst\s*]', result.stderr) build_0 = re.search(r'\[\s*build:0.bst\s*] START', result.stderr).start() assert all(track_message.start() < build_0 for track_message in track_messages) # Assert that 2.bst is *only* rebuilt if we are in strict mode build_2 = re.search(r'\[\s*build:2.bst\s*] START', result.stderr) if strict == '--strict': assert build_2 is not None else: assert build_2 is None