Lachlan pushed to branch lachlan/pickle-yaml-test-list-composite at BuildStream / buildstream
Commits:
- 
3ca487b8
by Chandan Singh at 2018-10-08T18:25:39Z
 - 
49df3d75
by Chandan Singh at 2018-10-08T18:58:54Z
 - 
d827dfae
by Angelos Evripiotis at 2018-10-09T09:12:45Z
 - 
9590e8ae
by Angelos Evripiotis at 2018-10-09T09:12:45Z
 - 
6d02e269
by Angelos Evripiotis at 2018-10-09T09:12:45Z
 - 
3ed26a47
by Angelos Evripiotis at 2018-10-09T09:12:45Z
 - 
5b22d850
by Angelos Evripiotis at 2018-10-09T09:12:45Z
 - 
ac0776f8
by Tristan Van Berkom at 2018-10-09T09:36:33Z
 - 
885bd946
by Tristan Van Berkom at 2018-10-09T09:39:24Z
 - 
120d8c73
by Tristan Van Berkom at 2018-10-09T09:45:23Z
 - 
37e65e47
by Jonathan Maw at 2018-10-09T10:51:32Z
 - 
043701f3
by Jonathan Maw at 2018-10-09T10:51:32Z
 - 
25a13793
by Jonathan Maw at 2018-10-09T10:51:32Z
 - 
6c4c30b8
by Jonathan Maw at 2018-10-09T10:51:32Z
 - 
34af73c5
by Jonathan Maw at 2018-10-09T10:51:32Z
 - 
d080cc57
by Jonathan Maw at 2018-10-09T10:51:32Z
 - 
e081c52a
by Lachlan Mackenzie at 2018-10-09T10:51:32Z
 - 
963cabb0
by Lachlan Mackenzie at 2018-10-09T10:51:32Z
 - 
65b9c212
by Lachlan Mackenzie at 2018-10-09T10:51:32Z
 - 
dcfa4a7d
by Jonathan Maw at 2018-10-09T10:51:32Z
 
9 changed files:
- CONTRIBUTING.rst
 - buildstream/_loader/loadelement.py
 - buildstream/_loader/loader.py
 - buildstream/_project.py
 - buildstream/_yaml.py
 - + buildstream/_yamlcache.py
 - contrib/bst-docker-import
 - + tests/frontend/yamlcache.py
 - tests/yaml/yaml.py
 
Changes:
| ... | ... | @@ -14,7 +14,7 @@ if no issue already exists. | 
| 14 | 14 | 
 | 
| 15 | 15 | 
 For policies on how to submit an issue and how to use our project labels,
 | 
| 16 | 16 | 
 we recommend that you read the `policies guide
 | 
| 17 | 
-<https://gitlab.com/BuildStream/nosoftware/alignment/blob/master/BuildStream_policies.md>`_
 | 
|
| 17 | 
+<https://gitlab.com/BuildStream/nosoftware/alignment/blob/master/BuildStream_policies.md>`_.
 | 
|
| 18 | 18 | 
 | 
| 19 | 19 | 
 | 
| 20 | 20 | 
 .. _contributing_fixing_bugs:
 | 
| ... | ... | @@ -270,6 +270,11 @@ comments and docstrings. | 
| 270 | 270 | 
     *Since: 1.2*
 | 
| 271 | 271 | 
     """
 | 
| 272 | 272 | 
 | 
| 273 | 
+.. note::
 | 
|
| 274 | 
+  | 
|
| 275 | 
+   Python does not support docstrings on instance variables, but sphinx does
 | 
|
| 276 | 
+   pick them up and includes them in the generated documentation.
 | 
|
| 277 | 
+  | 
|
| 273 | 278 | 
 **Internal instance variable**::
 | 
| 274 | 279 | 
 | 
| 275 | 280 | 
   def __init__(self, context, element):
 | 
| ... | ... | @@ -404,7 +409,7 @@ on a Python class in BuildStream:: | 
| 404 | 409 | 
          # Implementation of the "frobbish" abstract method
 | 
| 405 | 410 | 
          # defined by the parent Bar class.
 | 
| 406 | 411 | 
          #
 | 
| 407 | 
-	 return True
 | 
|
| 412 | 
+         return True
 | 
|
| 408 | 413 | 
 | 
| 409 | 414 | 
       ################################################
 | 
| 410 | 415 | 
       #                 Public Methods               #
 | 
| ... | ... | @@ -445,7 +450,7 @@ on a Python class in BuildStream:: | 
| 445 | 450 | 
       # Returns:
 | 
| 446 | 451 | 
       #    (int): The count of this foo
 | 
| 447 | 452 | 
       #
 | 
| 448 | 
-      def set_count(self, count):
 | 
|
| 453 | 
+      def get_count(self, count):
 | 
|
| 449 | 454 | 
 | 
| 450 | 455 | 
           return self._count
 | 
| 451 | 456 | 
 | 
| ... | ... | @@ -459,7 +464,7 @@ on a Python class in BuildStream:: | 
| 459 | 464 | 
       #       Even though these are private implementation
 | 
| 460 | 465 | 
       #       details, they still MUST have API documenting
 | 
| 461 | 466 | 
       #       comments on them.
 | 
| 462 | 
-      
 | 
|
| 467 | 
+  | 
|
| 463 | 468 | 
       # _do_frobbing()
 | 
| 464 | 469 | 
       #
 | 
| 465 | 470 | 
       # Does the actual frobbing
 | 
| ... | ... | @@ -494,10 +499,10 @@ reference on how the PEP-8 defines public and non-public. | 
| 494 | 499 | 
 | 
| 495 | 500 | 
   A private symbol must be denoted by a leading underscore.
 | 
| 496 | 501 | 
 | 
| 497 | 
-* When a class can have subclasses (for example, the ``Sandbox`` or ``Platform``
 | 
|
| 502 | 
+* When a class can have subclasses, then private symbols should be denoted
 | 
|
| 503 | 
+  by two leading underscores. For example, the ``Sandbox`` or ``Platform``
 | 
|
| 498 | 504 | 
   classes which have various implementations, or the ``Element`` and ``Source``
 | 
| 499 | 
-  classes which plugins derive from), then private symbols should be denoted
 | 
|
| 500 | 
-  by two leading underscores.
 | 
|
| 505 | 
+  classes which plugins derive from.
 | 
|
| 501 | 506 | 
 | 
| 502 | 507 | 
   The double leading underscore naming convention invokes Python's name
 | 
| 503 | 508 | 
   mangling algorithm which helps prevent namespace collisions in the case
 | 
| ... | ... | @@ -536,7 +541,7 @@ In order to disambiguate between: | 
| 536 | 541 | 
 | 
| 537 | 542 | 
 * Symbols which are publicly accessible details of the ``Element`` class, can
 | 
| 538 | 543 | 
   be accessed by BuildStream internals, but must remain hidden from the
 | 
| 539 | 
-  *"Public API Surface"*
 | 
|
| 544 | 
+  *"Public API Surface"*.
 | 
|
| 540 | 545 | 
 | 
| 541 | 546 | 
 * Symbols which are private to the ``Element`` class, and cannot be accessed
 | 
| 542 | 547 | 
   from outside of the ``Element`` class at all.
 | 
| ... | ... | @@ -586,7 +591,7 @@ is found at ``_artifactcache/artifactcache.py``. | 
| 586 | 591 | 
 | 
| 587 | 592 | 
 Imports
 | 
| 588 | 593 | 
 ~~~~~~~
 | 
| 589 | 
-Module imports inside BuildStream are done with relative ``.`` notation
 | 
|
| 594 | 
+Module imports inside BuildStream are done with relative ``.`` notation:
 | 
|
| 590 | 595 | 
 | 
| 591 | 596 | 
 **Good**::
 | 
| 592 | 597 | 
 | 
| ... | ... | @@ -628,12 +633,12 @@ which exposes an instance variable is the only one in control of the value of th | 
| 628 | 633 | 
 variable.
 | 
| 629 | 634 | 
 | 
| 630 | 635 | 
 * If an instance variable is public and must be modified; then it must be
 | 
| 631 | 
-  modified using a :ref:`mutator <contributing_accessor_mutator>`
 | 
|
| 636 | 
+  modified using a :ref:`mutator <contributing_accessor_mutator>`.
 | 
|
| 632 | 637 | 
 | 
| 633 | 638 | 
 * Ideally for better encapsulation, all object state is declared as
 | 
| 634 | 639 | 
   :ref:`private instance variables <contributing_public_and_private>` and can
 | 
| 635 | 640 | 
   only be accessed by external classes via public :ref:`accessors and mutators
 | 
| 636 | 
-  <contributing_accessor_mutator>`
 | 
|
| 641 | 
+  <contributing_accessor_mutator>`.
 | 
|
| 637 | 642 | 
 | 
| 638 | 643 | 
 .. note::
 | 
| 639 | 644 | 
 | 
| ... | ... | @@ -720,10 +725,10 @@ In BuildStream, we use the term *"Abstract Method"*, to refer to | 
| 720 | 725 | 
 a method which **can** be overridden by a subclass, whereas it
 | 
| 721 | 726 | 
 is **illegal** to override any other method.
 | 
| 722 | 727 | 
 | 
| 723 | 
-* Abstract methods are allowed to have default implementations
 | 
|
| 728 | 
+* Abstract methods are allowed to have default implementations.
 | 
|
| 724 | 729 | 
 | 
| 725 | 730 | 
 * Subclasses are not allowed to redefine the calling signature
 | 
| 726 | 
-  of an abstract method, or redefine the API contract in any way
 | 
|
| 731 | 
+  of an abstract method, or redefine the API contract in any way.
 | 
|
| 727 | 732 | 
 | 
| 728 | 733 | 
 * Subclasses are not allowed to override any other methods.
 | 
| 729 | 734 | 
 | 
| ... | ... | @@ -798,7 +803,7 @@ BstError parameters | 
| 798 | 803 | 
 When raising ``BstError`` class exceptions, there are some common properties
 | 
| 799 | 804 | 
 which can be useful to know about:
 | 
| 800 | 805 | 
 | 
| 801 | 
-* **message:** The brief human readable error, will be formatted on one line in the frontend
 | 
|
| 806 | 
+* **message:** The brief human readable error, will be formatted on one line in the frontend.
 | 
|
| 802 | 807 | 
 | 
| 803 | 808 | 
 * **detail:** An optional detailed human readable message to accompany the **message** summary
 | 
| 804 | 809 | 
   of the error. This is often used to recommend the user some course of action, or to provide
 | 
| ... | ... | @@ -974,9 +979,9 @@ symbols to a minimum, this is important for both | 
| 974 | 979 | 
 | 
| 975 | 980 | 
 When anyone visits a file, there are two levels of comprehension:
 | 
| 976 | 981 | 
 | 
| 977 | 
-* What do I need to know in order to *use* this object
 | 
|
| 982 | 
+* What do I need to know in order to *use* this object.
 | 
|
| 978 | 983 | 
 | 
| 979 | 
-* What do I need to know in order to *modify* this object
 | 
|
| 984 | 
+* What do I need to know in order to *modify* this object.
 | 
|
| 980 | 985 | 
 | 
| 981 | 986 | 
 For the former, we want the reader to understand with as little effort
 | 
| 982 | 987 | 
 as possible, what the public API contract is for a given object and consequently,
 | 
| ... | ... | @@ -1001,9 +1006,9 @@ well documented and minimal. | 
| 1001 | 1006 | 
 | 
| 1002 | 1007 | 
 When adding new API to a given object for a new purpose, consider whether
 | 
| 1003 | 1008 | 
 the new API is in any way redundant with other API (should this value now
 | 
| 1004 | 
-go into the constructor, since we use it more than once ? could this
 | 
|
| 1009 | 
+go into the constructor, since we use it more than once? could this
 | 
|
| 1005 | 1010 | 
 value be passed along with another function, and the other function renamed,
 | 
| 1006 | 
-to better suit the new purposes of this module/object ?) and repurpose
 | 
|
| 1011 | 
+to better suit the new purposes of this module/object?) and repurpose
 | 
|
| 1007 | 1012 | 
 the outward facing API of an object as a whole every time.
 | 
| 1008 | 1013 | 
 | 
| 1009 | 1014 | 
 | 
| ... | ... | @@ -1183,7 +1188,7 @@ The BuildStream documentation style is as follows: | 
| 1183 | 1188 | 
 * Cross references should be of the form ``:role:`target```.
 | 
| 1184 | 1189 | 
 | 
| 1185 | 1190 | 
   * Explicit anchors can be declared as ``.. _anchor_name:`` on a line by itself.
 | 
| 1186 | 
-  
 | 
|
| 1191 | 
+  | 
|
| 1187 | 1192 | 
   * To cross reference arbitrary locations with, for example, the anchor ``anchor_name``,
 | 
| 1188 | 1193 | 
     always provide some explicit text in the link instead of deriving the text from
 | 
| 1189 | 1194 | 
     the target, e.g.: ``:ref:`Link text <anchor_name>```.
 | 
| ... | ... | @@ -1266,23 +1271,23 @@ Documentation Examples | 
| 1266 | 1271 | 
 The examples section of the documentation contains a series of standalone
 | 
| 1267 | 1272 | 
 examples, here are the criteria for an example addition.
 | 
| 1268 | 1273 | 
 | 
| 1269 | 
-* The example has a ``${name}``
 | 
|
| 1274 | 
+* The example has a ``${name}``.
 | 
|
| 1270 | 1275 | 
 | 
| 1271 | 
-* The example has a project users can copy and use
 | 
|
| 1276 | 
+* The example has a project users can copy and use.
 | 
|
| 1272 | 1277 | 
 | 
| 1273 | 
-  * This project is added in the directory ``doc/examples/${name}``
 | 
|
| 1278 | 
+  * This project is added in the directory ``doc/examples/${name}``.
 | 
|
| 1274 | 1279 | 
 | 
| 1275 | 
-* The example has a documentation component
 | 
|
| 1280 | 
+* The example has a documentation component.
 | 
|
| 1276 | 1281 | 
 | 
| 1277 | 1282 | 
   * This is added at ``doc/source/examples/${name}.rst``
 | 
| 1278 | 1283 | 
   * A reference to ``examples/${name}`` is added to the toctree in ``doc/source/examples.rst``
 | 
| 1279 | 1284 | 
   * This documentation discusses the project elements declared in the project and may
 | 
| 1280 | 
-    provide some BuildStream command examples
 | 
|
| 1281 | 
-  * This documentation links out to the reference manual at every opportunity
 | 
|
| 1285 | 
+    provide some BuildStream command examples.
 | 
|
| 1286 | 
+  * This documentation links out to the reference manual at every opportunity.
 | 
|
| 1282 | 1287 | 
 | 
| 1283 | 
-* The example has a CI test component
 | 
|
| 1288 | 
+* The example has a CI test component.
 | 
|
| 1284 | 1289 | 
 | 
| 1285 | 
-  * This is an integration test added at ``tests/examples/${name}``
 | 
|
| 1290 | 
+  * This is an integration test added at ``tests/examples/${name}``.
 | 
|
| 1286 | 1291 | 
   * This test runs BuildStream in the ways described in the example
 | 
| 1287 | 1292 | 
     and assert that we get the results which we advertize to users in
 | 
| 1288 | 1293 | 
     the said examples.
 | 
| ... | ... | @@ -1309,17 +1314,17 @@ The ``.run`` file format is just another YAML dictionary which consists of a | 
| 1309 | 1314 | 
 | 
| 1310 | 1315 | 
 Each *command* is a dictionary, the members of which are listed here:
 | 
| 1311 | 1316 | 
 | 
| 1312 | 
-* ``directory``: The input file relative project directory
 | 
|
| 1317 | 
+* ``directory``: The input file relative project directory.
 | 
|
| 1313 | 1318 | 
 | 
| 1314 | 
-* ``output``: The input file relative output html file to generate (optional)
 | 
|
| 1319 | 
+* ``output``: The input file relative output html file to generate (optional).
 | 
|
| 1315 | 1320 | 
 | 
| 1316 | 1321 | 
 * ``fake-output``: Don't really run the command, just pretend to and pretend
 | 
| 1317 | 1322 | 
   this was the output, an empty string will enable this too.
 | 
| 1318 | 1323 | 
 | 
| 1319 | 
-* ``command``: The command to run, without the leading ``bst``
 | 
|
| 1324 | 
+* ``command``: The command to run, without the leading ``bst``.
 | 
|
| 1320 | 1325 | 
 | 
| 1321 | 1326 | 
 * ``shell``: Specifying ``True`` indicates that ``command`` should be run as
 | 
| 1322 | 
-  a shell command from the project directory, instead of a bst command (optional)
 | 
|
| 1327 | 
+  a shell command from the project directory, instead of a bst command (optional).
 | 
|
| 1323 | 1328 | 
 | 
| 1324 | 1329 | 
 When adding a new ``.run`` file, one should normally also commit the new
 | 
| 1325 | 1330 | 
 resulting generated ``.html`` file(s) into the ``doc/source/sessions-stored/``
 | 
| ... | ... | @@ -1417,7 +1422,7 @@ a subdirectory beside your test in which to store data. | 
| 1417 | 1422 | 
 When creating a test that needs data, use the datafiles extension
 | 
| 1418 | 1423 | 
 to decorate your test case (again, examples exist in the existing
 | 
| 1419 | 1424 | 
 tests for this), documentation on the datafiles extension can
 | 
| 1420 | 
-be found here: https://pypi.python.org/pypi/pytest-datafiles
 | 
|
| 1425 | 
+be found here: https://pypi.python.org/pypi/pytest-datafiles.
 | 
|
| 1421 | 1426 | 
 | 
| 1422 | 1427 | 
 Tests that run a sandbox should be decorated with::
 | 
| 1423 | 1428 | 
 | 
| ... | ... | @@ -185,6 +185,6 @@ def _extract_depends_from_node(node, *, key=None): | 
| 185 | 185 | 
         output_deps.append(dependency)
 | 
| 186 | 186 | 
 | 
| 187 | 187 | 
     # Now delete the field, we dont want it anymore
 | 
| 188 | 
-    del node[key]
 | 
|
| 188 | 
+    node.pop(key, None)
 | 
|
| 189 | 189 | 
 | 
| 190 | 190 | 
     return output_deps
 | 
| ... | ... | @@ -29,6 +29,7 @@ from .. import _yaml | 
| 29 | 29 | 
 from ..element import Element
 | 
| 30 | 30 | 
 from .._profile import Topics, profile_start, profile_end
 | 
| 31 | 31 | 
 from .._includes import Includes
 | 
| 32 | 
+from .._yamlcache import YamlCache
 | 
|
| 32 | 33 | 
 | 
| 33 | 34 | 
 from .types import Symbol, Dependency
 | 
| 34 | 35 | 
 from .loadelement import LoadElement
 | 
| ... | ... | @@ -112,7 +113,14 @@ class Loader(): | 
| 112 | 113 | 
             profile_start(Topics.LOAD_PROJECT, target)
 | 
| 113 | 114 | 
             junction, name, loader = self._parse_name(target, rewritable, ticker,
 | 
| 114 | 115 | 
                                                       fetch_subprojects=fetch_subprojects)
 | 
| 115 | 
-            loader._load_file(name, rewritable, ticker, fetch_subprojects)
 | 
|
| 116 | 
+  | 
|
| 117 | 
+            # XXX This will need to be changed to the context's top-level project if this method
 | 
|
| 118 | 
+            # is ever used for subprojects
 | 
|
| 119 | 
+            top_dir = self.project.directory
 | 
|
| 120 | 
+  | 
|
| 121 | 
+            cache_file = YamlCache.get_cache_file(top_dir)
 | 
|
| 122 | 
+            with YamlCache.open(self._context, cache_file) as yaml_cache:
 | 
|
| 123 | 
+                loader._load_file(name, rewritable, ticker, fetch_subprojects, yaml_cache)
 | 
|
| 116 | 124 | 
             deps.append(Dependency(name, junction=junction))
 | 
| 117 | 125 | 
             profile_end(Topics.LOAD_PROJECT, target)
 | 
| 118 | 126 | 
 | 
| ... | ... | @@ -201,11 +209,12 @@ class Loader(): | 
| 201 | 209 | 
     #    rewritable (bool): Whether we should load in round trippable mode
 | 
| 202 | 210 | 
     #    ticker (callable): A callback to report loaded filenames to the frontend
 | 
| 203 | 211 | 
     #    fetch_subprojects (bool): Whether to fetch subprojects while loading
 | 
| 212 | 
+    #    yaml_cache (YamlCache): A yaml cache
 | 
|
| 204 | 213 | 
     #
 | 
| 205 | 214 | 
     # Returns:
 | 
| 206 | 215 | 
     #    (LoadElement): A loaded LoadElement
 | 
| 207 | 216 | 
     #
 | 
| 208 | 
-    def _load_file(self, filename, rewritable, ticker, fetch_subprojects):
 | 
|
| 217 | 
+    def _load_file(self, filename, rewritable, ticker, fetch_subprojects, yaml_cache=None):
 | 
|
| 209 | 218 | 
 | 
| 210 | 219 | 
         # Silently ignore already loaded files
 | 
| 211 | 220 | 
         if filename in self._elements:
 | 
| ... | ... | @@ -218,7 +227,8 @@ class Loader(): | 
| 218 | 227 | 
         # Load the data and process any conditional statements therein
 | 
| 219 | 228 | 
         fullpath = os.path.join(self._basedir, filename)
 | 
| 220 | 229 | 
         try:
 | 
| 221 | 
-            node = _yaml.load(fullpath, shortname=filename, copy_tree=rewritable, project=self.project)
 | 
|
| 230 | 
+            node = _yaml.load(fullpath, shortname=filename, copy_tree=rewritable,
 | 
|
| 231 | 
+                              project=self.project, yaml_cache=yaml_cache)
 | 
|
| 222 | 232 | 
         except LoadError as e:
 | 
| 223 | 233 | 
             if e.reason == LoadErrorReason.MISSING_FILE:
 | 
| 224 | 234 | 
                 # If we can't find the file, try to suggest plausible
 | 
| ... | ... | @@ -261,13 +271,13 @@ class Loader(): | 
| 261 | 271 | 
         # Load all dependency files for the new LoadElement
 | 
| 262 | 272 | 
         for dep in element.deps:
 | 
| 263 | 273 | 
             if dep.junction:
 | 
| 264 | 
-                self._load_file(dep.junction, rewritable, ticker, fetch_subprojects)
 | 
|
| 274 | 
+                self._load_file(dep.junction, rewritable, ticker, fetch_subprojects, yaml_cache)
 | 
|
| 265 | 275 | 
                 loader = self._get_loader(dep.junction, rewritable=rewritable, ticker=ticker,
 | 
| 266 | 276 | 
                                           fetch_subprojects=fetch_subprojects)
 | 
| 267 | 277 | 
             else:
 | 
| 268 | 278 | 
                 loader = self
 | 
| 269 | 279 | 
 | 
| 270 | 
-            dep_element = loader._load_file(dep.name, rewritable, ticker, fetch_subprojects)
 | 
|
| 280 | 
+            dep_element = loader._load_file(dep.name, rewritable, ticker, fetch_subprojects, yaml_cache)
 | 
|
| 271 | 281 | 
 | 
| 272 | 282 | 
             if _yaml.node_get(dep_element.node, str, Symbol.KIND) == 'junction':
 | 
| 273 | 283 | 
                 raise LoadError(LoadErrorReason.INVALID_DATA,
 | 
| ... | ... | @@ -19,6 +19,7 @@ | 
| 19 | 19 | 
 #        Tiago Gomes <tiago gomes codethink co uk>
 | 
| 20 | 20 | 
 | 
| 21 | 21 | 
 import os
 | 
| 22 | 
+import hashlib
 | 
|
| 22 | 23 | 
 from collections import Mapping, OrderedDict
 | 
| 23 | 24 | 
 from pluginbase import PluginBase
 | 
| 24 | 25 | 
 from . import utils
 | 
| ... | ... | @@ -183,20 +183,32 @@ class CompositeTypeError(CompositeError): | 
| 183 | 183 | 
 #    shortname (str): The filename in shorthand for error reporting (or None)
 | 
| 184 | 184 | 
 #    copy_tree (bool): Whether to make a copy, preserving the original toplevels
 | 
| 185 | 185 | 
 #                      for later serialization
 | 
| 186 | 
+#    yaml_cache (YamlCache): A yaml cache to consult rather than parsing
 | 
|
| 186 | 187 | 
 #
 | 
| 187 | 188 | 
 # Returns (dict): A loaded copy of the YAML file with provenance information
 | 
| 188 | 189 | 
 #
 | 
| 189 | 190 | 
 # Raises: LoadError
 | 
| 190 | 191 | 
 #
 | 
| 191 | 
-def load(filename, shortname=None, copy_tree=False, *, project=None):
 | 
|
| 192 | 
+def load(filename, shortname=None, copy_tree=False, *, project=None, yaml_cache=None):
 | 
|
| 192 | 193 | 
     if not shortname:
 | 
| 193 | 194 | 
         shortname = filename
 | 
| 194 | 195 | 
 | 
| 195 | 196 | 
     file = ProvenanceFile(filename, shortname, project)
 | 
| 196 | 197 | 
 | 
| 197 | 198 | 
     try:
 | 
| 199 | 
+        data = None
 | 
|
| 198 | 200 | 
         with open(filename) as f:
 | 
| 199 | 
-            return load_data(f, file, copy_tree=copy_tree)
 | 
|
| 201 | 
+            contents = f.read()
 | 
|
| 202 | 
+        if yaml_cache:
 | 
|
| 203 | 
+            data, key = yaml_cache.get(project, filename, contents, copy_tree)
 | 
|
| 204 | 
+  | 
|
| 205 | 
+        if not data:
 | 
|
| 206 | 
+            data = load_data(contents, file, copy_tree=copy_tree)
 | 
|
| 207 | 
+  | 
|
| 208 | 
+        if yaml_cache:
 | 
|
| 209 | 
+            yaml_cache.put_from_key(project, filename, key, data)
 | 
|
| 210 | 
+  | 
|
| 211 | 
+        return data
 | 
|
| 200 | 212 | 
     except FileNotFoundError as e:
 | 
| 201 | 213 | 
         raise LoadError(LoadErrorReason.MISSING_FILE,
 | 
| 202 | 214 | 
                         "Could not find file at {}".format(filename)) from e
 | 
| 1 | 
+#
 | 
|
| 2 | 
+#  Copyright 2018 Bloomberg Finance LP
 | 
|
| 3 | 
+#
 | 
|
| 4 | 
+#  This program is free software; you can redistribute it and/or
 | 
|
| 5 | 
+#  modify it under the terms of the GNU Lesser General Public
 | 
|
| 6 | 
+#  License as published by the Free Software Foundation; either
 | 
|
| 7 | 
+#  version 2 of the License, or (at your option) any later version.
 | 
|
| 8 | 
+#
 | 
|
| 9 | 
+#  This library is distributed in the hope that it will be useful,
 | 
|
| 10 | 
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
|
| 11 | 
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 | 
|
| 12 | 
+#  Lesser General Public License for more details.
 | 
|
| 13 | 
+#
 | 
|
| 14 | 
+#  You should have received a copy of the GNU Lesser General Public
 | 
|
| 15 | 
+#  License along with this library. If not, see <http://www.gnu.org/licenses/>.
 | 
|
| 16 | 
+#
 | 
|
| 17 | 
+#  Authors:
 | 
|
| 18 | 
+#        Jonathan Maw <jonathan maw codethink co uk>
 | 
|
| 19 | 
+  | 
|
| 20 | 
+import os
 | 
|
| 21 | 
+import pickle
 | 
|
| 22 | 
+import hashlib
 | 
|
| 23 | 
+import io
 | 
|
| 24 | 
+  | 
|
| 25 | 
+import sys
 | 
|
| 26 | 
+  | 
|
| 27 | 
+from contextlib import contextmanager
 | 
|
| 28 | 
+from collections import namedtuple
 | 
|
| 29 | 
+  | 
|
| 30 | 
+from ._cachekey import generate_key
 | 
|
| 31 | 
+from ._context import Context
 | 
|
| 32 | 
+from . import utils, _yaml
 | 
|
| 33 | 
+  | 
|
| 34 | 
+  | 
|
| 35 | 
+YAML_CACHE_FILENAME = "yaml_cache.pickle"
 | 
|
| 36 | 
+  | 
|
| 37 | 
+  | 
|
| 38 | 
+# YamlCache()
 | 
|
| 39 | 
+#
 | 
|
| 40 | 
+# A cache that wraps around the loading of yaml in projects.
 | 
|
| 41 | 
+#
 | 
|
| 42 | 
+# The recommended way to use a YamlCache is:
 | 
|
| 43 | 
+#   with YamlCache.open(context) as yamlcache:
 | 
|
| 44 | 
+#     # Load all the yaml
 | 
|
| 45 | 
+#     ...
 | 
|
| 46 | 
+#
 | 
|
| 47 | 
+# Args:
 | 
|
| 48 | 
+#    context (Context): The invocation Context
 | 
|
| 49 | 
+#
 | 
|
| 50 | 
+class YamlCache():
 | 
|
| 51 | 
+  | 
|
| 52 | 
+    def __init__(self, context):
 | 
|
| 53 | 
+        self._project_caches = {}
 | 
|
| 54 | 
+        self._context = context
 | 
|
| 55 | 
+    ##################
 | 
|
| 56 | 
+    # Public Methods #
 | 
|
| 57 | 
+    ##################
 | 
|
| 58 | 
+  | 
|
| 59 | 
+    # is_cached():
 | 
|
| 60 | 
+    #
 | 
|
| 61 | 
+    # Checks whether a file is cached.
 | 
|
| 62 | 
+    #
 | 
|
| 63 | 
+    # Args:
 | 
|
| 64 | 
+    #    project (Project): The project this file is in.
 | 
|
| 65 | 
+    #    filepath (str): The path to the file, *relative to the project's directory*.
 | 
|
| 66 | 
+    #
 | 
|
| 67 | 
+    # Returns:
 | 
|
| 68 | 
+    #    (bool): Whether the file is cached.
 | 
|
| 69 | 
+    def is_cached(self, project, filepath):
 | 
|
| 70 | 
+        cache_path = self._get_filepath(project, filepath)
 | 
|
| 71 | 
+        project_name = project.name if project else ""
 | 
|
| 72 | 
+        try:
 | 
|
| 73 | 
+            project_cache = self._project_caches[project_name]
 | 
|
| 74 | 
+            if cache_path in project_cache.elements:
 | 
|
| 75 | 
+                return True
 | 
|
| 76 | 
+        except KeyError:
 | 
|
| 77 | 
+            pass
 | 
|
| 78 | 
+        return False
 | 
|
| 79 | 
+  | 
|
| 80 | 
+    # open():
 | 
|
| 81 | 
+    #
 | 
|
| 82 | 
+    # Return an instance of the YamlCache which writes to disk when it leaves scope.
 | 
|
| 83 | 
+    #
 | 
|
| 84 | 
+    # Args:
 | 
|
| 85 | 
+    #    context (Context): The context.
 | 
|
| 86 | 
+    #    cachefile (str): The path to the cache file.
 | 
|
| 87 | 
+    #
 | 
|
| 88 | 
+    # Returns:
 | 
|
| 89 | 
+    #    (YamlCache): A YamlCache.
 | 
|
| 90 | 
+    @staticmethod
 | 
|
| 91 | 
+    @contextmanager
 | 
|
| 92 | 
+    def open(context, cachefile):
 | 
|
| 93 | 
+        # Try to load from disk first
 | 
|
| 94 | 
+        cache = None
 | 
|
| 95 | 
+        if os.path.exists(cachefile):
 | 
|
| 96 | 
+            try:
 | 
|
| 97 | 
+                with open(cachefile, "rb") as f:
 | 
|
| 98 | 
+                    cache = BstUnpickler(f, context).load()
 | 
|
| 99 | 
+            except pickle.UnpicklingError as e:
 | 
|
| 100 | 
+                sys.stderr.write("Failed to load YamlCache, {}\n".format(e))
 | 
|
| 101 | 
+  | 
|
| 102 | 
+        # Failed to load from disk, create a new one
 | 
|
| 103 | 
+        if not cache:
 | 
|
| 104 | 
+            cache = YamlCache(context)
 | 
|
| 105 | 
+  | 
|
| 106 | 
+        yield cache
 | 
|
| 107 | 
+  | 
|
| 108 | 
+        cache._write(cachefile)
 | 
|
| 109 | 
+  | 
|
| 110 | 
+    # get_cache_file():
 | 
|
| 111 | 
+    #
 | 
|
| 112 | 
+    # Retrieves a path to the yaml cache file.
 | 
|
| 113 | 
+    #
 | 
|
| 114 | 
+    # Returns:
 | 
|
| 115 | 
+    #   (str): The path to the cache file
 | 
|
| 116 | 
+    @staticmethod
 | 
|
| 117 | 
+    def get_cache_file(top_dir):
 | 
|
| 118 | 
+        return os.path.join(top_dir, ".bst", YAML_CACHE_FILENAME)
 | 
|
| 119 | 
+  | 
|
| 120 | 
+    # get():
 | 
|
| 121 | 
+    #
 | 
|
| 122 | 
+    # Gets a parsed file from the cache.
 | 
|
| 123 | 
+    #
 | 
|
| 124 | 
+    # Args:
 | 
|
| 125 | 
+    #    project (Project) or None: The project this file is in, if it exists.
 | 
|
| 126 | 
+    #    filepath (str): The absolute path to the file.
 | 
|
| 127 | 
+    #    contents (str): The contents of the file to be cached
 | 
|
| 128 | 
+    #    copy_tree (bool): Whether the data should make a copy when it's being generated
 | 
|
| 129 | 
+    #                      (i.e. exactly as when called in yaml)
 | 
|
| 130 | 
+    #
 | 
|
| 131 | 
+    # Returns:
 | 
|
| 132 | 
+    #    (decorated dict): The parsed yaml from the cache, or None if the file isn't in the cache.
 | 
|
| 133 | 
+    #    (str):            The key used to look up the parsed yaml in the cache
 | 
|
| 134 | 
+    def get(self, project, filepath, contents, copy_tree):
 | 
|
| 135 | 
+        key = self._calculate_key(contents, copy_tree)
 | 
|
| 136 | 
+        data = self._get(project, filepath, key)
 | 
|
| 137 | 
+        return data, key
 | 
|
| 138 | 
+  | 
|
| 139 | 
+    # put():
 | 
|
| 140 | 
+    #
 | 
|
| 141 | 
+    # Puts a parsed file into the cache.
 | 
|
| 142 | 
+    #
 | 
|
| 143 | 
+    # Args:
 | 
|
| 144 | 
+    #    project (Project): The project this file is in.
 | 
|
| 145 | 
+    #    filepath (str): The path to the file.
 | 
|
| 146 | 
+    #    contents (str): The contents of the file that has been cached
 | 
|
| 147 | 
+    #    copy_tree (bool): Whether the data should make a copy when it's being generated
 | 
|
| 148 | 
+    #                      (i.e. exactly as when called in yaml)
 | 
|
| 149 | 
+    #    value (decorated dict): The data to put into the cache.
 | 
|
| 150 | 
+    def put(self, project, filepath, contents, copy_tree, value):
 | 
|
| 151 | 
+        key = self._calculate_key(contents, copy_tree)
 | 
|
| 152 | 
+        self.put_from_key(project, filepath, key, value)
 | 
|
| 153 | 
+  | 
|
| 154 | 
+    # put_from_key():
 | 
|
| 155 | 
+    #
 | 
|
| 156 | 
+    # Put a parsed file into the cache when given a key.
 | 
|
| 157 | 
+    #
 | 
|
| 158 | 
+    # Args:
 | 
|
| 159 | 
+    #    project (Project): The project this file is in.
 | 
|
| 160 | 
+    #    filepath (str): The path to the file.
 | 
|
| 161 | 
+    #    key (str): The key to the file within the cache. Typically, this is the
 | 
|
| 162 | 
+    #               value of `calculate_key()` with the file's unparsed contents
 | 
|
| 163 | 
+    #               and any relevant metadata passed in.
 | 
|
| 164 | 
+    #    value (decorated dict): The data to put into the cache.
 | 
|
| 165 | 
+    def put_from_key(self, project, filepath, key, value):
 | 
|
| 166 | 
+        cache_path = self._get_filepath(project, filepath)
 | 
|
| 167 | 
+        project_name = project.name if project else ""
 | 
|
| 168 | 
+        try:
 | 
|
| 169 | 
+            project_cache = self._project_caches[project_name]
 | 
|
| 170 | 
+        except KeyError:
 | 
|
| 171 | 
+            project_cache = self._project_caches[project_name] = CachedProject({})
 | 
|
| 172 | 
+  | 
|
| 173 | 
+        project_cache.elements[cache_path] = CachedYaml(key, value)
 | 
|
| 174 | 
+  | 
|
| 175 | 
+  | 
|
| 176 | 
+    ###################
 | 
|
| 177 | 
+    # Private Methods #
 | 
|
| 178 | 
+    ###################
 | 
|
| 179 | 
+  | 
|
| 180 | 
+    # Writes the yaml cache to the specified path.
 | 
|
| 181 | 
+    #
 | 
|
| 182 | 
+    # Args:
 | 
|
| 183 | 
+    #    path (str): The path to the cache file.
 | 
|
| 184 | 
+    def _write(self, path):
 | 
|
| 185 | 
+        parent_dir = os.path.dirname(path)
 | 
|
| 186 | 
+        os.makedirs(parent_dir, exist_ok=True)
 | 
|
| 187 | 
+        with open(path, "wb") as f:
 | 
|
| 188 | 
+            BstPickler(f).dump(self)
 | 
|
| 189 | 
+  | 
|
| 190 | 
+    # _get_filepath():
 | 
|
| 191 | 
+    #
 | 
|
| 192 | 
+    # Returns a file path relative to a project if passed, or the original path if
 | 
|
| 193 | 
+    # the project is None
 | 
|
| 194 | 
+    #
 | 
|
| 195 | 
+    # Args:
 | 
|
| 196 | 
+    #    project (Project) or None: The project the filepath exists within
 | 
|
| 197 | 
+    #    full_path (str): The path that the returned path is based on
 | 
|
| 198 | 
+    #
 | 
|
| 199 | 
+    # Returns:
 | 
|
| 200 | 
+    #    (str): The path to the file, relative to a project if it exists
 | 
|
| 201 | 
+    def _get_filepath(self, project, full_path):
 | 
|
| 202 | 
+        if project:
 | 
|
| 203 | 
+            assert full_path.startswith(project.directory)
 | 
|
| 204 | 
+            filepath = os.path.relpath(full_path, project.directory)
 | 
|
| 205 | 
+        else:
 | 
|
| 206 | 
+            filepath = full_path
 | 
|
| 207 | 
+        return full_path
 | 
|
| 208 | 
+  | 
|
| 209 | 
+    # _calculate_key():
 | 
|
| 210 | 
+    #
 | 
|
| 211 | 
+    # Calculates a key for putting into the cache.
 | 
|
| 212 | 
+    #
 | 
|
| 213 | 
+    # Args:
 | 
|
| 214 | 
+    #    (basic object)... : Any number of strictly-ordered basic objects
 | 
|
| 215 | 
+    #
 | 
|
| 216 | 
+    # Returns:
 | 
|
| 217 | 
+    #   (str): A key made out of every arg passed in
 | 
|
| 218 | 
+    @staticmethod
 | 
|
| 219 | 
+    def _calculate_key(*args):
 | 
|
| 220 | 
+        string = pickle.dumps(args)
 | 
|
| 221 | 
+        return hashlib.sha1(string).hexdigest()
 | 
|
| 222 | 
+  | 
|
| 223 | 
+    # _get():
 | 
|
| 224 | 
+    #
 | 
|
| 225 | 
+    # Gets a parsed file from the cache when given a key.
 | 
|
| 226 | 
+    #
 | 
|
| 227 | 
+    # Args:
 | 
|
| 228 | 
+    #    project (Project): The project this file is in.
 | 
|
| 229 | 
+    #    filepath (str): The path to the file.
 | 
|
| 230 | 
+    #    key (str): The key to the file within the cache. Typically, this is the
 | 
|
| 231 | 
+    #               value of `calculate_key()` with the file's unparsed contents
 | 
|
| 232 | 
+    #               and any relevant metadata passed in.
 | 
|
| 233 | 
+    #
 | 
|
| 234 | 
+    # Returns:
 | 
|
| 235 | 
+    #    (decorated dict): The parsed yaml from the cache, or None if the file isn't in the cache.
 | 
|
| 236 | 
+    def _get(self, project, filepath, key):
 | 
|
| 237 | 
+        cache_path = self._get_filepath(project, filepath)
 | 
|
| 238 | 
+        project_name = project.name if project else ""
 | 
|
| 239 | 
+        try:
 | 
|
| 240 | 
+            project_cache = self._project_caches[project_name]
 | 
|
| 241 | 
+            try:
 | 
|
| 242 | 
+                cachedyaml = project_cache.elements[cache_path]
 | 
|
| 243 | 
+                if cachedyaml._key == key:
 | 
|
| 244 | 
+                    # We've unpickled the YamlCache, but not the specific file
 | 
|
| 245 | 
+                    if cachedyaml._contents is None:
 | 
|
| 246 | 
+                        cachedyaml._contents = BstUnpickler.loads(cachedyaml._pickled_contents, self._context)
 | 
|
| 247 | 
+                    return cachedyaml._contents
 | 
|
| 248 | 
+            except KeyError:
 | 
|
| 249 | 
+                pass
 | 
|
| 250 | 
+        except KeyError:
 | 
|
| 251 | 
+            pass
 | 
|
| 252 | 
+        return None
 | 
|
| 253 | 
+  | 
|
| 254 | 
+  | 
|
| 255 | 
+CachedProject = namedtuple('CachedProject', ['elements'])
 | 
|
| 256 | 
+  | 
|
| 257 | 
+  | 
|
| 258 | 
+class CachedYaml():
 | 
|
| 259 | 
+    def __init__(self, key, contents):
 | 
|
| 260 | 
+        self._key = key
 | 
|
| 261 | 
+        self.set_contents(contents)
 | 
|
| 262 | 
+  | 
|
| 263 | 
+    # Sets the contents of the CachedYaml.
 | 
|
| 264 | 
+    #
 | 
|
| 265 | 
+    # Args:
 | 
|
| 266 | 
+    #    contents (provenanced dict): The contents to put in the cache.
 | 
|
| 267 | 
+    #
 | 
|
| 268 | 
+    def set_contents(self, contents):
 | 
|
| 269 | 
+        self._contents = contents
 | 
|
| 270 | 
+        self._pickled_contents = BstPickler.dumps(contents)
 | 
|
| 271 | 
+  | 
|
| 272 | 
+    # Pickling helper method, prevents 'contents' from being serialised
 | 
|
| 273 | 
+    def __getstate__(self):
 | 
|
| 274 | 
+        data = self.__dict__.copy()
 | 
|
| 275 | 
+        data['_contents'] = None
 | 
|
| 276 | 
+        return data
 | 
|
| 277 | 
+  | 
|
| 278 | 
+  | 
|
| 279 | 
+# In _yaml.load, we have a ProvenanceFile that stores the project the file
 | 
|
| 280 | 
+# came from. Projects can't be pickled, but it's always going to be the same
 | 
|
| 281 | 
+# project between invocations (unless the entire project is moved but the
 | 
|
| 282 | 
+# file stayed in the same place)
 | 
|
| 283 | 
+class BstPickler(pickle.Pickler):
 | 
|
| 284 | 
+    def persistent_id(self, obj):
 | 
|
| 285 | 
+        if isinstance(obj, _yaml.ProvenanceFile):
 | 
|
| 286 | 
+            if obj.project:
 | 
|
| 287 | 
+                # ProvenanceFile's project object cannot be stored as it is.
 | 
|
| 288 | 
+                project_tag = obj.project.name
 | 
|
| 289 | 
+                # ProvenanceFile's filename must be stored relative to the
 | 
|
| 290 | 
+                # project, as the project dir may move.
 | 
|
| 291 | 
+                name = os.path.relpath(obj.name, obj.project.directory)
 | 
|
| 292 | 
+            else:
 | 
|
| 293 | 
+                project_tag = None
 | 
|
| 294 | 
+                name = obj.name
 | 
|
| 295 | 
+            return ("ProvenanceFile", name, obj.shortname, project_tag)
 | 
|
| 296 | 
+        elif isinstance(obj, Context):
 | 
|
| 297 | 
+            return ("Context",)
 | 
|
| 298 | 
+        else:
 | 
|
| 299 | 
+            return None
 | 
|
| 300 | 
+  | 
|
| 301 | 
+    @staticmethod
 | 
|
| 302 | 
+    def dumps(obj):
 | 
|
| 303 | 
+        stream = io.BytesIO()
 | 
|
| 304 | 
+        BstPickler(stream).dump(obj)
 | 
|
| 305 | 
+        stream.seek(0)
 | 
|
| 306 | 
+        return stream.read()
 | 
|
| 307 | 
+  | 
|
| 308 | 
+  | 
|
| 309 | 
+class BstUnpickler(pickle.Unpickler):
 | 
|
| 310 | 
+    def __init__(self, file, context):
 | 
|
| 311 | 
+        super().__init__(file)
 | 
|
| 312 | 
+        self._context = context
 | 
|
| 313 | 
+  | 
|
| 314 | 
+    def persistent_load(self, pid):
 | 
|
| 315 | 
+        if pid[0] == "ProvenanceFile":
 | 
|
| 316 | 
+            _, tagged_name, shortname, project_tag = pid
 | 
|
| 317 | 
+  | 
|
| 318 | 
+            if project_tag is not None:
 | 
|
| 319 | 
+                for p in self._context.get_projects():
 | 
|
| 320 | 
+                    if project_tag == p.name:
 | 
|
| 321 | 
+                        project = p
 | 
|
| 322 | 
+                        break
 | 
|
| 323 | 
+  | 
|
| 324 | 
+                name = os.path.join(project.directory, tagged_name)
 | 
|
| 325 | 
+  | 
|
| 326 | 
+                if not project:
 | 
|
| 327 | 
+                    projects = [p.name for p in self._context.get_projects()]
 | 
|
| 328 | 
+                    raise pickle.UnpicklingError("No project with name {} found in {}"
 | 
|
| 329 | 
+                                                 .format(key_id, projects))
 | 
|
| 330 | 
+            else:
 | 
|
| 331 | 
+                project = None
 | 
|
| 332 | 
+                name = tagged_name
 | 
|
| 333 | 
+  | 
|
| 334 | 
+            return _yaml.ProvenanceFile(name, shortname, project)
 | 
|
| 335 | 
+        elif pid[0] == "Context":
 | 
|
| 336 | 
+            return self._context
 | 
|
| 337 | 
+        else:
 | 
|
| 338 | 
+            raise pickle.UnpicklingError("Unsupported persistent object, {}".format(pid))
 | 
|
| 339 | 
+  | 
|
| 340 | 
+    @staticmethod
 | 
|
| 341 | 
+    def loads(text, context):
 | 
|
| 342 | 
+        stream = io.BytesIO()
 | 
|
| 343 | 
+        stream.write(bytes(text))
 | 
|
| 344 | 
+        stream.seek(0)
 | 
|
| 345 | 
+        return BstUnpickler(stream, context).load()
 | 
| ... | ... | @@ -93,10 +93,10 @@ echo "INFO: Checking out $element ..." >&2 | 
| 93 | 93 | 
 $bst_cmd checkout --tar "$element" "$checkout_tar" || die "Failed to checkout $element"
 | 
| 94 | 94 | 
 echo "INFO: Successfully checked out $element" >&2
 | 
| 95 | 95 | 
 | 
| 96 | 
-echo "INFO: Importing Docker image ..."
 | 
|
| 96 | 
+echo "INFO: Importing Docker image ..." >&2
 | 
|
| 97 | 97 | 
 "${docker_import_cmd[@]}" "$checkout_tar" "$docker_image_tag" || die "Failed to import Docker image from tarball"
 | 
| 98 | 
-echo "INFO: Successfully import Docker image $docker_image_tag"
 | 
|
| 98 | 
+echo "INFO: Successfully import Docker image $docker_image_tag" >&2
 | 
|
| 99 | 99 | 
 | 
| 100 | 
-echo "INFO: Cleaning up ..."
 | 
|
| 100 | 
+echo "INFO: Cleaning up ..." >&2
 | 
|
| 101 | 101 | 
 rm "$checkout_tar" || die "Failed to remove $checkout_tar"
 | 
| 102 | 
-echo "INFO: Clean up finished"
 | 
|
| 102 | 
+echo "INFO: Clean up finished" >&2
 | 
| 1 | 
+import os
 | 
|
| 2 | 
+import pytest
 | 
|
| 3 | 
+import hashlib
 | 
|
| 4 | 
+import tempfile
 | 
|
| 5 | 
+from ruamel import yaml
 | 
|
| 6 | 
+  | 
|
| 7 | 
+from tests.testutils import cli, generate_junction, create_element_size, create_repo
 | 
|
| 8 | 
+from buildstream import _yaml
 | 
|
| 9 | 
+from buildstream._yamlcache import YamlCache
 | 
|
| 10 | 
+from buildstream._project import Project
 | 
|
| 11 | 
+from buildstream._context import Context
 | 
|
| 12 | 
+from contextlib import contextmanager
 | 
|
| 13 | 
+  | 
|
| 14 | 
+  | 
|
| 15 | 
+def generate_project(tmpdir, ref_storage, with_junction, name="test"):
 | 
|
| 16 | 
+    if with_junction == 'junction':
 | 
|
| 17 | 
+        subproject_dir = generate_project(
 | 
|
| 18 | 
+            tmpdir, ref_storage,
 | 
|
| 19 | 
+            'no-junction', name='test-subproject'
 | 
|
| 20 | 
+        )
 | 
|
| 21 | 
+  | 
|
| 22 | 
+    project_dir = os.path.join(tmpdir, name)
 | 
|
| 23 | 
+    os.makedirs(project_dir)
 | 
|
| 24 | 
+    # project.conf
 | 
|
| 25 | 
+    project_conf_path = os.path.join(project_dir, 'project.conf')
 | 
|
| 26 | 
+    elements_path = 'elements'
 | 
|
| 27 | 
+    project_conf = {
 | 
|
| 28 | 
+        'name': name,
 | 
|
| 29 | 
+        'element-path': elements_path,
 | 
|
| 30 | 
+        'ref-storage': ref_storage,
 | 
|
| 31 | 
+    }
 | 
|
| 32 | 
+    _yaml.dump(project_conf, project_conf_path)
 | 
|
| 33 | 
+  | 
|
| 34 | 
+    # elements
 | 
|
| 35 | 
+    if with_junction == 'junction':
 | 
|
| 36 | 
+        junction_name = 'junction.bst'
 | 
|
| 37 | 
+        junction_dir = os.path.join(project_dir, elements_path)
 | 
|
| 38 | 
+        junction_path = os.path.join(project_dir, elements_path, junction_name)
 | 
|
| 39 | 
+        os.makedirs(junction_dir)
 | 
|
| 40 | 
+        generate_junction(tmpdir, subproject_dir, junction_path)
 | 
|
| 41 | 
+        element_depends = [{'junction': junction_name, 'filename': 'test.bst'}]
 | 
|
| 42 | 
+    else:
 | 
|
| 43 | 
+        element_depends = []
 | 
|
| 44 | 
+  | 
|
| 45 | 
+    element_name = 'test.bst'
 | 
|
| 46 | 
+    create_element_size(element_name, project_dir, elements_path, element_depends, 1)
 | 
|
| 47 | 
+  | 
|
| 48 | 
+    return project_dir
 | 
|
| 49 | 
+  | 
|
| 50 | 
+  | 
|
| 51 | 
+@contextmanager
 | 
|
| 52 | 
+def with_yamlcache(project_dir):
 | 
|
| 53 | 
+    context = Context()
 | 
|
| 54 | 
+    project = Project(project_dir, context)
 | 
|
| 55 | 
+    cache_file = YamlCache.get_cache_file(project_dir)
 | 
|
| 56 | 
+    with YamlCache.open(context, cache_file) as yamlcache:
 | 
|
| 57 | 
+        yield yamlcache, project
 | 
|
| 58 | 
+  | 
|
| 59 | 
+  | 
|
| 60 | 
+def yamlcache_key(yamlcache, in_file, copy_tree=False):
 | 
|
| 61 | 
+    with open(in_file) as f:
 | 
|
| 62 | 
+        key = yamlcache._calculate_key(f.read(), copy_tree)
 | 
|
| 63 | 
+    return key
 | 
|
| 64 | 
+  | 
|
| 65 | 
+  | 
|
| 66 | 
+def modified_file(input_file, tmpdir):
 | 
|
| 67 | 
+    with open(input_file) as f:
 | 
|
| 68 | 
+        data = f.read()
 | 
|
| 69 | 
+    assert 'variables' not in data
 | 
|
| 70 | 
+    data += '\nvariables: {modified: True}\n'
 | 
|
| 71 | 
+    _, temppath = tempfile.mkstemp(dir=tmpdir, text=True)
 | 
|
| 72 | 
+    with open(temppath, 'w') as f:
 | 
|
| 73 | 
+        f.write(data)
 | 
|
| 74 | 
+  | 
|
| 75 | 
+    return temppath
 | 
|
| 76 | 
+  | 
|
| 77 | 
+  | 
|
| 78 | 
+@pytest.mark.parametrize('ref_storage', ['inline', 'project.refs'])
 | 
|
| 79 | 
+@pytest.mark.parametrize('with_junction', ['no-junction', 'junction'])
 | 
|
| 80 | 
+@pytest.mark.parametrize('move_project', ['move', 'no-move'])
 | 
|
| 81 | 
+def test_yamlcache_used(cli, tmpdir, ref_storage, with_junction, move_project):
 | 
|
| 82 | 
+    # Generate the project
 | 
|
| 83 | 
+    project = generate_project(str(tmpdir), ref_storage, with_junction)
 | 
|
| 84 | 
+    if with_junction == 'junction':
 | 
|
| 85 | 
+        result = cli.run(project=project, args=['fetch', '--track', 'junction.bst'])
 | 
|
| 86 | 
+        result.assert_success()
 | 
|
| 87 | 
+  | 
|
| 88 | 
+    # bst show to put it in the cache
 | 
|
| 89 | 
+    result = cli.run(project=project, args=['show', 'test.bst'])
 | 
|
| 90 | 
+    result.assert_success()
 | 
|
| 91 | 
+  | 
|
| 92 | 
+    element_path = os.path.join(project, 'elements', 'test.bst')
 | 
|
| 93 | 
+    with with_yamlcache(project) as (yc, prj):
 | 
|
| 94 | 
+        # Check that it's in the cache
 | 
|
| 95 | 
+        assert yc.is_cached(prj, element_path)
 | 
|
| 96 | 
+  | 
|
| 97 | 
+        # *Absolutely* horrible cache corruption to check it's being used
 | 
|
| 98 | 
+        # Modifying the data from the cache is fraught with danger,
 | 
|
| 99 | 
+        # so instead I'll load a modified version of the original file
 | 
|
| 100 | 
+        temppath = modified_file(element_path, str(tmpdir))
 | 
|
| 101 | 
+        contents = _yaml.load(temppath, copy_tree=False, project=prj)
 | 
|
| 102 | 
+        key = yamlcache_key(yc, element_path)
 | 
|
| 103 | 
+        yc.put_from_key(prj, element_path, key, contents)
 | 
|
| 104 | 
+  | 
|
| 105 | 
+    # Show that a variable has been added
 | 
|
| 106 | 
+    result = cli.run(project=project, args=['show', '--format', '%{vars}', 'test.bst'])
 | 
|
| 107 | 
+    result.assert_success()
 | 
|
| 108 | 
+    data = yaml.safe_load(result.output)
 | 
|
| 109 | 
+    assert 'modified' in data
 | 
|
| 110 | 
+    assert data['modified'] == 'True'
 | 
|
| 111 | 
+  | 
|
| 112 | 
+  | 
|
| 113 | 
+@pytest.mark.parametrize('ref_storage', ['inline', 'project.refs'])
 | 
|
| 114 | 
+@pytest.mark.parametrize('with_junction', ['junction', 'no-junction'])
 | 
|
| 115 | 
+@pytest.mark.parametrize('move_project', ['move', 'no-move'])
 | 
|
| 116 | 
+def test_yamlcache_changed_file(cli, ref_storage, with_junction, move_project):
 | 
|
| 117 | 
+    # i.e. a file is cached, the file is changed, loading the file (with cache) returns new data
 | 
|
| 118 | 
+    # inline and junction can only be changed by opening a workspace
 | 
|
| 119 | 
+    pass
 | 
| 1 | 1 | 
 import os
 | 
| 2 | 2 | 
 import pytest
 | 
| 3 | 
+import tempfile
 | 
|
| 3 | 4 | 
 from collections import Mapping
 | 
| 4 | 5 | 
 | 
| 5 | 6 | 
 from buildstream import _yaml
 | 
| 6 | 7 | 
 from buildstream._exceptions import LoadError, LoadErrorReason
 | 
| 8 | 
+from buildstream._context import Context
 | 
|
| 9 | 
+from buildstream._yamlcache import YamlCache
 | 
|
| 7 | 10 | 
 | 
| 8 | 11 | 
 DATA_DIR = os.path.join(
 | 
| 9 | 12 | 
     os.path.dirname(os.path.realpath(__file__)),
 | 
| ... | ... | @@ -150,6 +153,21 @@ def test_composite_preserve_originals(datafiles): | 
| 150 | 153 | 
     assert(_yaml.node_get(orig_extra, str, 'old') == 'new')
 | 
| 151 | 154 | 
 | 
| 152 | 155 | 
 | 
| 156 | 
+def load_yaml_file(filename, *, cache_path, shortname=None, from_cache='raw'):
 | 
|
| 157 | 
+  | 
|
| 158 | 
+    temppath = tempfile.mkstemp(dir=str(cache_path), text=True)
 | 
|
| 159 | 
+    context = Context()
 | 
|
| 160 | 
+  | 
|
| 161 | 
+    with YamlCache.open(context, str(temppath)) as yc:
 | 
|
| 162 | 
+        if from_cache == 'raw':
 | 
|
| 163 | 
+            return _yaml.load(filename, shortname)
 | 
|
| 164 | 
+        elif from_cache == 'cached':
 | 
|
| 165 | 
+            _yaml.load(filename, shortname, yaml_cache=yc)
 | 
|
| 166 | 
+            return _yaml.load(filename, shortname, yaml_cache=yc)
 | 
|
| 167 | 
+        else:
 | 
|
| 168 | 
+            assert False
 | 
|
| 169 | 
+  | 
|
| 170 | 
+  | 
|
| 153 | 171 | 
 # Tests for list composition
 | 
| 154 | 172 | 
 #
 | 
| 155 | 173 | 
 # Each test composits a filename on top of basics.yaml, and tests
 | 
| ... | ... | @@ -165,6 +183,7 @@ def test_composite_preserve_originals(datafiles): | 
| 165 | 183 | 
 #    prov_col: The expected provenance column of "mood"
 | 
| 166 | 184 | 
 #
 | 
| 167 | 185 | 
 @pytest.mark.datafiles(os.path.join(DATA_DIR))
 | 
| 186 | 
+@pytest.mark.parametrize('caching', [('raw'), ('cached')])
 | 
|
| 168 | 187 | 
 @pytest.mark.parametrize("filename,index,length,mood,prov_file,prov_line,prov_col", [
 | 
| 169 | 188 | 
 | 
| 170 | 189 | 
     # Test results of compositing with the (<) prepend directive
 | 
| ... | ... | @@ -195,14 +214,15 @@ def test_composite_preserve_originals(datafiles): | 
| 195 | 214 | 
     ('implicitoverwrite.yaml', 0, 2, 'overwrite1', 'implicitoverwrite.yaml', 4, 8),
 | 
| 196 | 215 | 
     ('implicitoverwrite.yaml', 1, 2, 'overwrite2', 'implicitoverwrite.yaml', 6, 8),
 | 
| 197 | 216 | 
 ])
 | 
| 198 | 
-def test_list_composition(datafiles, filename,
 | 
|
| 217 | 
+def test_list_composition(datafiles, filename, tmpdir,
 | 
|
| 199 | 218 | 
                           index, length, mood,
 | 
| 200 | 
-                          prov_file, prov_line, prov_col):
 | 
|
| 201 | 
-    base = os.path.join(datafiles.dirname, datafiles.basename, 'basics.yaml')
 | 
|
| 202 | 
-    overlay = os.path.join(datafiles.dirname, datafiles.basename, filename)
 | 
|
| 219 | 
+                          prov_file, prov_line, prov_col, caching):
 | 
|
| 220 | 
+    base_file = os.path.join(datafiles.dirname, datafiles.basename, 'basics.yaml')
 | 
|
| 221 | 
+    overlay_file = os.path.join(datafiles.dirname, datafiles.basename, filename)
 | 
|
| 222 | 
+  | 
|
| 223 | 
+    base = load_yaml_file(base_file, cache_path=tmpdir, shortname='basics.yaml', from_cache=caching)
 | 
|
| 224 | 
+    overlay = load_yaml_file(overlay_file, cache_path=tmpdir, shortname=filename, from_cache=caching)
 | 
|
| 203 | 225 | 
 | 
| 204 | 
-    base = _yaml.load(base, shortname='basics.yaml')
 | 
|
| 205 | 
-    overlay = _yaml.load(overlay, shortname=filename)
 | 
|
| 206 | 226 | 
     _yaml.composite_dict(base, overlay)
 | 
| 207 | 227 | 
 | 
| 208 | 228 | 
     children = _yaml.node_get(base, list, 'children')
 | 
| ... | ... | @@ -254,6 +274,7 @@ def test_list_deletion(datafiles): | 
| 254 | 274 | 
 #    prov_col: The expected provenance column of "mood"
 | 
| 255 | 275 | 
 #
 | 
| 256 | 276 | 
 @pytest.mark.datafiles(os.path.join(DATA_DIR))
 | 
| 277 | 
+@pytest.mark.parametrize('caching', [('raw'), ('cached')])
 | 
|
| 257 | 278 | 
 @pytest.mark.parametrize("filename1,filename2,index,length,mood,prov_file,prov_line,prov_col", [
 | 
| 258 | 279 | 
 | 
| 259 | 280 | 
     # Test results of compositing literal list with (>) and then (<)
 | 
| ... | ... | @@ -310,9 +331,9 @@ def test_list_deletion(datafiles): | 
| 310 | 331 | 
     ('listoverwrite.yaml', 'listprepend.yaml', 2, 4, 'overwrite1', 'listoverwrite.yaml', 5, 10),
 | 
| 311 | 332 | 
     ('listoverwrite.yaml', 'listprepend.yaml', 3, 4, 'overwrite2', 'listoverwrite.yaml', 7, 10),
 | 
| 312 | 333 | 
 ])
 | 
| 313 | 
-def test_list_composition_twice(datafiles, filename1, filename2,
 | 
|
| 334 | 
+def test_list_composition_twice(datafiles, tmpdir, filename1, filename2,
 | 
|
| 314 | 335 | 
                                 index, length, mood,
 | 
| 315 | 
-                                prov_file, prov_line, prov_col):
 | 
|
| 336 | 
+                                prov_file, prov_line, prov_col, caching):
 | 
|
| 316 | 337 | 
     file_base = os.path.join(datafiles.dirname, datafiles.basename, 'basics.yaml')
 | 
| 317 | 338 | 
     file1 = os.path.join(datafiles.dirname, datafiles.basename, filename1)
 | 
| 318 | 339 | 
     file2 = os.path.join(datafiles.dirname, datafiles.basename, filename2)
 | 
| ... | ... | @@ -320,9 +341,9 @@ def test_list_composition_twice(datafiles, filename1, filename2, | 
| 320 | 341 | 
     #####################
 | 
| 321 | 342 | 
     # Round 1 - Fight !
 | 
| 322 | 343 | 
     #####################
 | 
| 323 | 
-    base = _yaml.load(file_base, shortname='basics.yaml')
 | 
|
| 324 | 
-    overlay1 = _yaml.load(file1, shortname=filename1)
 | 
|
| 325 | 
-    overlay2 = _yaml.load(file2, shortname=filename2)
 | 
|
| 344 | 
+    base = load_yaml_file(file_base, cache_path=tmpdir, shortname='basics.yaml', from_cache=caching)
 | 
|
| 345 | 
+    overlay1 = load_yaml_file(file1, cache_path=tmpdir, shortname=filename1, from_cache=caching)
 | 
|
| 346 | 
+    overlay2 = load_yaml_file(file2, cache_path=tmpdir, shortname=filename2, from_cache=caching)
 | 
|
| 326 | 347 | 
 | 
| 327 | 348 | 
     _yaml.composite_dict(base, overlay1)
 | 
| 328 | 349 | 
     _yaml.composite_dict(base, overlay2)
 | 
| ... | ... | @@ -337,9 +358,9 @@ def test_list_composition_twice(datafiles, filename1, filename2, | 
| 337 | 358 | 
     #####################
 | 
| 338 | 359 | 
     # Round 2 - Fight !
 | 
| 339 | 360 | 
     #####################
 | 
| 340 | 
-    base = _yaml.load(file_base, shortname='basics.yaml')
 | 
|
| 341 | 
-    overlay1 = _yaml.load(file1, shortname=filename1)
 | 
|
| 342 | 
-    overlay2 = _yaml.load(file2, shortname=filename2)
 | 
|
| 361 | 
+    base = load_yaml_file(file_base, cache_path=tmpdir, shortname='basics.yaml', from_cache=caching)
 | 
|
| 362 | 
+    overlay1 = load_yaml_file(file1, cache_path=tmpdir, shortname=filename1, from_cache=caching)
 | 
|
| 363 | 
+    overlay2 = load_yaml_file(file2, cache_path=tmpdir, shortname=filename2, from_cache=caching)
 | 
|
| 343 | 364 | 
 | 
| 344 | 365 | 
     _yaml.composite_dict(overlay1, overlay2)
 | 
| 345 | 366 | 
     _yaml.composite_dict(base, overlay1)
 | 
