Qinusty pushed to branch Qinusty/600-recursive-variables at BuildStream / buildstream
Commits:
-
ddbf7bc2
by Jürg Billeter at 2018-08-28T08:56:06Z
-
ffc556ff
by Valentin David at 2018-08-28T09:01:05Z
-
dd90d53d
by Jürg Billeter at 2018-08-28T09:01:05Z
-
6a5a8e7a
by Tristan Van Berkom at 2018-08-28T11:29:22Z
-
6047a575
by Tristan Van Berkom at 2018-08-28T13:41:36Z
-
7535fda8
by Jürg Billeter at 2018-08-28T14:25:35Z
-
c16c0801
by Josh Smith at 2018-08-29T09:47:05Z
-
fc5498a7
by Josh Smith at 2018-08-29T10:54:47Z
9 changed files:
- buildstream/_exceptions.py
- buildstream/_variables.py
- buildstream/element.py
- dev-requirements.txt
- tests/format/variables.py
- + tests/format/variables/cyclic_variables/cyclic.bst
- + tests/format/variables/cyclic_variables/project.conf
- tests/frontend/workspace.py
- tests/testutils/runcli.py
Changes:
| ... | ... | @@ -217,6 +217,9 @@ class LoadErrorReason(Enum): |
| 217 | 217 |
# A recursive include has been encountered.
|
| 218 | 218 |
RECURSIVE_INCLUDE = 21
|
| 219 | 219 |
|
| 220 |
+ # A recursive variable has been encountered
|
|
| 221 |
+ RECURSIVE_VARIABLE = 22
|
|
| 222 |
+ |
|
| 220 | 223 |
|
| 221 | 224 |
# LoadError
|
| 222 | 225 |
#
|
| ... | ... | @@ -132,6 +132,12 @@ class Variables(): |
| 132 | 132 |
value = _yaml.node_get(variables, str, key)
|
| 133 | 133 |
|
| 134 | 134 |
resolved_var, item_unmatched = self._subst(value, variables)
|
| 135 |
+ if _wrap_variable(key) in resolved_var:
|
|
| 136 |
+ raise LoadError(LoadErrorReason.RECURSIVE_VARIABLE,
|
|
| 137 |
+ "{}: ".format(_yaml.node_get_provenance(variables, key)) +
|
|
| 138 |
+ ("Variable '{}' references itself recursively through" +
|
|
| 139 |
+ " another variable declariation.").format(key))
|
|
| 140 |
+ |
|
| 135 | 141 |
resolved[key] = resolved_var
|
| 136 | 142 |
unmatched += item_unmatched
|
| 137 | 143 |
|
| ... | ... | @@ -168,8 +174,12 @@ class Variables(): |
| 168 | 174 |
# Helper function to fetch information about the node referring to a variable
|
| 169 | 175 |
#
|
| 170 | 176 |
def _find_references(self, varname):
|
| 171 |
- fullname = '%{' + varname + '}'
|
|
| 177 |
+ fullname = _wrap_variable(varname)
|
|
| 172 | 178 |
for key, value in _yaml.node_items(self.original):
|
| 173 | 179 |
if fullname in value:
|
| 174 | 180 |
provenance = _yaml.node_get_provenance(self.original, key)
|
| 175 | 181 |
yield (key, provenance)
|
| 182 |
+ |
|
| 183 |
+ |
|
| 184 |
+def _wrap_variable(var):
|
|
| 185 |
+ return "%{" + var + "}"
|
| ... | ... | @@ -1101,9 +1101,12 @@ class Element(Plugin): |
| 1101 | 1101 |
# until the full cache query below.
|
| 1102 | 1102 |
if (not self.__assemble_scheduled and not self.__assemble_done and
|
| 1103 | 1103 |
not self.__cached_success(keystrength=_KeyStrength.WEAK) and
|
| 1104 |
- not self._pull_pending() and self._is_required()):
|
|
| 1105 |
- self._schedule_assemble()
|
|
| 1106 |
- return
|
|
| 1104 |
+ not self._pull_pending()):
|
|
| 1105 |
+ # For uncached workspaced elements, assemble is required
|
|
| 1106 |
+ # even if we only need the cache key
|
|
| 1107 |
+ if self._is_required() or self._get_workspace():
|
|
| 1108 |
+ self._schedule_assemble()
|
|
| 1109 |
+ return
|
|
| 1107 | 1110 |
|
| 1108 | 1111 |
if self.__strict_cache_key is None:
|
| 1109 | 1112 |
dependencies = [
|
| ... | ... | @@ -1126,13 +1129,17 @@ class Element(Plugin): |
| 1126 | 1129 |
self.__weak_cached = self.__artifacts.contains(self, self.__weak_cache_key)
|
| 1127 | 1130 |
|
| 1128 | 1131 |
if (not self.__assemble_scheduled and not self.__assemble_done and
|
| 1129 |
- not self._cached_success() and not self._pull_pending() and self._is_required()):
|
|
| 1132 |
+ not self._cached_success() and not self._pull_pending()):
|
|
| 1130 | 1133 |
# Workspaced sources are considered unstable if a build is pending
|
| 1131 | 1134 |
# as the build will modify the contents of the workspace.
|
| 1132 | 1135 |
# Determine as early as possible if a build is pending to discard
|
| 1133 | 1136 |
# unstable cache keys.
|
| 1134 |
- self._schedule_assemble()
|
|
| 1135 |
- return
|
|
| 1137 |
+ |
|
| 1138 |
+ # For uncached workspaced elements, assemble is required
|
|
| 1139 |
+ # even if we only need the cache key
|
|
| 1140 |
+ if self._is_required() or self._get_workspace():
|
|
| 1141 |
+ self._schedule_assemble()
|
|
| 1142 |
+ return
|
|
| 1136 | 1143 |
|
| 1137 | 1144 |
if self.__cache_key is None:
|
| 1138 | 1145 |
# Calculate strong cache key
|
| ... | ... | @@ -1430,7 +1437,6 @@ class Element(Plugin): |
| 1430 | 1437 |
# in a subprocess.
|
| 1431 | 1438 |
#
|
| 1432 | 1439 |
def _schedule_assemble(self):
|
| 1433 |
- assert self._is_required()
|
|
| 1434 | 1440 |
assert not self.__assemble_scheduled
|
| 1435 | 1441 |
self.__assemble_scheduled = True
|
| 1436 | 1442 |
|
| ... | ... | @@ -1438,6 +1444,8 @@ class Element(Plugin): |
| 1438 | 1444 |
for dep in self.dependencies(Scope.BUILD, recurse=False):
|
| 1439 | 1445 |
dep._set_required()
|
| 1440 | 1446 |
|
| 1447 |
+ self._set_required()
|
|
| 1448 |
+ |
|
| 1441 | 1449 |
# Invalidate workspace key as the build modifies the workspace directory
|
| 1442 | 1450 |
workspace = self._get_workspace()
|
| 1443 | 1451 |
if workspace:
|
| ... | ... | @@ -1661,6 +1669,10 @@ class Element(Plugin): |
| 1661 | 1669 |
# (bool): Whether a pull operation is pending
|
| 1662 | 1670 |
#
|
| 1663 | 1671 |
def _pull_pending(self):
|
| 1672 |
+ if self._get_workspace():
|
|
| 1673 |
+ # Workspace builds are never pushed to artifact servers
|
|
| 1674 |
+ return False
|
|
| 1675 |
+ |
|
| 1664 | 1676 |
if self.__strong_cached:
|
| 1665 | 1677 |
# Artifact already in local cache
|
| 1666 | 1678 |
return False
|
| ... | ... | @@ -8,3 +8,4 @@ pytest-env |
| 8 | 8 |
pytest-pep8
|
| 9 | 9 |
pytest-pylint
|
| 10 | 10 |
pytest-xdist
|
| 11 |
+pytest-timeout
|
| 1 | 1 |
import os
|
| 2 | 2 |
import pytest
|
| 3 |
+import sys
|
|
| 3 | 4 |
from buildstream import _yaml
|
| 4 | 5 |
from buildstream._exceptions import ErrorDomain, LoadErrorReason
|
| 5 | 6 |
from tests.testutils.runcli import cli
|
| ... | ... | @@ -72,3 +73,20 @@ def test_missing_variable(cli, datafiles, tmpdir): |
| 72 | 73 |
])
|
| 73 | 74 |
result.assert_main_error(ErrorDomain.LOAD,
|
| 74 | 75 |
LoadErrorReason.UNRESOLVED_VARIABLE)
|
| 76 |
+ |
|
| 77 |
+@pytest.mark.timeout(3, method="signal")
|
|
| 78 |
+@pytest.mark.datafiles(os.path.join(DATA_DIR, 'cyclic_variables'))
|
|
| 79 |
+def test_cyclic_variables(cli, datafiles):
|
|
| 80 |
+ print_warning("Performing cyclic test, if this test times out it will " +
|
|
| 81 |
+ "exit the test sequence")
|
|
| 82 |
+ project = os.path.join(datafiles.dirname, datafiles.basename)
|
|
| 83 |
+ result = cli.run(project=project, silent=True, args=[
|
|
| 84 |
+ "build", "cyclic.bst"
|
|
| 85 |
+ ])
|
|
| 86 |
+ result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.RECURSIVE_VARIABLE)
|
|
| 87 |
+ |
|
| 88 |
+ |
|
| 89 |
+def print_warning(msg):
|
|
| 90 |
+ RED, END = "\033[91m", "\033[0m"
|
|
| 91 |
+ print(("\n{}{}{}").format(RED, msg, END), file=sys.stderr)
|
|
| 92 |
+ |
| 1 |
+kind: manual
|
|
| 2 |
+ |
|
| 3 |
+variables:
|
|
| 4 |
+ a: "%{prefix}/a"
|
|
| 5 |
+ prefix: "%{a}/some_prefix/"
|
|
| \ No newline at end of file |
| 1 |
+name: test
|
| ... | ... | @@ -780,3 +780,73 @@ def test_inconsitent_pipeline_message(cli, tmpdir, datafiles, kind): |
| 780 | 780 |
'build', element_name
|
| 781 | 781 |
])
|
| 782 | 782 |
result.assert_main_error(ErrorDomain.PIPELINE, "inconsistent-pipeline-workspaced")
|
| 783 |
+ |
|
| 784 |
+ |
|
| 785 |
+@pytest.mark.datafiles(DATA_DIR)
|
|
| 786 |
+@pytest.mark.parametrize("strict", [("strict"), ("non-strict")])
|
|
| 787 |
+def test_cache_key_workspace_in_dependencies(cli, tmpdir, datafiles, strict):
|
|
| 788 |
+ checkout = os.path.join(str(tmpdir), 'checkout')
|
|
| 789 |
+ element_name, project, workspace = open_workspace(cli, os.path.join(str(tmpdir), 'repo-a'),
|
|
| 790 |
+ datafiles, 'git', False)
|
|
| 791 |
+ |
|
| 792 |
+ element_path = os.path.join(project, 'elements')
|
|
| 793 |
+ back_dep_element_name = 'workspace-test-back-dep.bst'
|
|
| 794 |
+ |
|
| 795 |
+ # Write out our test target
|
|
| 796 |
+ element = {
|
|
| 797 |
+ 'kind': 'compose',
|
|
| 798 |
+ 'depends': [
|
|
| 799 |
+ {
|
|
| 800 |
+ 'filename': element_name,
|
|
| 801 |
+ 'type': 'build'
|
|
| 802 |
+ }
|
|
| 803 |
+ ]
|
|
| 804 |
+ }
|
|
| 805 |
+ _yaml.dump(element,
|
|
| 806 |
+ os.path.join(element_path,
|
|
| 807 |
+ back_dep_element_name))
|
|
| 808 |
+ |
|
| 809 |
+ # Modify workspace
|
|
| 810 |
+ shutil.rmtree(os.path.join(workspace, 'usr', 'bin'))
|
|
| 811 |
+ os.makedirs(os.path.join(workspace, 'etc'))
|
|
| 812 |
+ with open(os.path.join(workspace, 'etc', 'pony.conf'), 'w') as f:
|
|
| 813 |
+ f.write("PONY='pink'")
|
|
| 814 |
+ |
|
| 815 |
+ # Configure strict mode
|
|
| 816 |
+ strict_mode = True
|
|
| 817 |
+ if strict != 'strict':
|
|
| 818 |
+ strict_mode = False
|
|
| 819 |
+ cli.configure({
|
|
| 820 |
+ 'projects': {
|
|
| 821 |
+ 'test': {
|
|
| 822 |
+ 'strict': strict_mode
|
|
| 823 |
+ }
|
|
| 824 |
+ }
|
|
| 825 |
+ })
|
|
| 826 |
+ |
|
| 827 |
+ # Build artifact with dependency's modified workspace
|
|
| 828 |
+ assert cli.get_element_state(project, element_name) == 'buildable'
|
|
| 829 |
+ assert cli.get_element_key(project, element_name) == "{:?<64}".format('')
|
|
| 830 |
+ assert cli.get_element_state(project, back_dep_element_name) == 'waiting'
|
|
| 831 |
+ assert cli.get_element_key(project, back_dep_element_name) == "{:?<64}".format('')
|
|
| 832 |
+ result = cli.run(project=project, args=['build', back_dep_element_name])
|
|
| 833 |
+ result.assert_success()
|
|
| 834 |
+ assert cli.get_element_state(project, element_name) == 'cached'
|
|
| 835 |
+ assert cli.get_element_key(project, element_name) != "{:?<64}".format('')
|
|
| 836 |
+ assert cli.get_element_state(project, back_dep_element_name) == 'cached'
|
|
| 837 |
+ assert cli.get_element_key(project, back_dep_element_name) != "{:?<64}".format('')
|
|
| 838 |
+ result = cli.run(project=project, args=['build', back_dep_element_name])
|
|
| 839 |
+ result.assert_success()
|
|
| 840 |
+ |
|
| 841 |
+ # Checkout the result
|
|
| 842 |
+ result = cli.run(project=project, args=[
|
|
| 843 |
+ 'checkout', back_dep_element_name, checkout
|
|
| 844 |
+ ])
|
|
| 845 |
+ result.assert_success()
|
|
| 846 |
+ |
|
| 847 |
+ # Check that the pony.conf from the modified workspace exists
|
|
| 848 |
+ filename = os.path.join(checkout, 'etc', 'pony.conf')
|
|
| 849 |
+ assert os.path.exists(filename)
|
|
| 850 |
+ |
|
| 851 |
+ # Check that the original /usr/bin/hello is not in the checkout
|
|
| 852 |
+ assert not os.path.exists(os.path.join(checkout, 'usr', 'bin', 'hello'))
|
| ... | ... | @@ -94,14 +94,28 @@ class Result(): |
| 94 | 94 |
# error_reason (any): The reason field of the error which occurred
|
| 95 | 95 |
# fail_message (str): An optional message to override the automatic
|
| 96 | 96 |
# assertion error messages
|
| 97 |
+ # debug (bool): If true, prints information regarding the exit state of the result()
|
|
| 97 | 98 |
# Raises:
|
| 98 | 99 |
# (AssertionError): If any of the assertions fail
|
| 99 | 100 |
#
|
| 100 | 101 |
def assert_main_error(self,
|
| 101 | 102 |
error_domain,
|
| 102 | 103 |
error_reason,
|
| 103 |
- fail_message=''):
|
|
| 104 |
- |
|
| 104 |
+ fail_message='',
|
|
| 105 |
+ *, debug=False):
|
|
| 106 |
+ if debug:
|
|
| 107 |
+ print(
|
|
| 108 |
+ """
|
|
| 109 |
+ Exit code: {}
|
|
| 110 |
+ Exception: {}
|
|
| 111 |
+ Domain: {}
|
|
| 112 |
+ Reason: {}
|
|
| 113 |
+ """.format(
|
|
| 114 |
+ self.exit_code,
|
|
| 115 |
+ self.exception,
|
|
| 116 |
+ self.exception.domain,
|
|
| 117 |
+ self.exception.reason
|
|
| 118 |
+ ))
|
|
| 105 | 119 |
assert self.exit_code == -1, fail_message
|
| 106 | 120 |
assert self.exc is not None, fail_message
|
| 107 | 121 |
assert self.exception is not None, fail_message
|
