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
|