[Notes] [Git][BuildStream/buildstream][juerg/command-batching] 12 commits: _frontend/app.py: Set correct element-path in interactive bst-init



Title: GitLab

Jürg Billeter pushed to branch juerg/command-batching at BuildStream / buildstream

Commits:

7 changed files:

Changes:

  • NEWS
    ... ... @@ -39,6 +39,8 @@ buildstream 1.3.1
    39 39
         instead of just a specially-formatted build-root with a `root` and `scratch`
    
    40 40
         subdirectory.
    
    41 41
     
    
    42
    +  o Add sandbox API for command batching.
    
    43
    +
    
    42 44
     
    
    43 45
     =================
    
    44 46
     buildstream 1.1.5
    

  • buildstream/_frontend/app.py
    ... ... @@ -305,7 +305,6 @@ class App():
    305 305
             directory = self._main_options['directory']
    
    306 306
             directory = os.path.abspath(directory)
    
    307 307
             project_path = os.path.join(directory, 'project.conf')
    
    308
    -        elements_path = os.path.join(directory, element_path)
    
    309 308
     
    
    310 309
             try:
    
    311 310
                 # Abort if the project.conf already exists, unless `--force` was specified in `bst init`
    
    ... ... @@ -335,6 +334,7 @@ class App():
    335 334
                     raise AppError("Error creating project directory {}: {}".format(directory, e)) from e
    
    336 335
     
    
    337 336
                 # Create the elements sub-directory if it doesnt exist
    
    337
    +            elements_path = os.path.join(directory, element_path)
    
    338 338
                 try:
    
    339 339
                     os.makedirs(elements_path, exist_ok=True)
    
    340 340
                 except IOError as e:
    

  • buildstream/buildelement.py
    ... ... @@ -217,7 +217,8 @@ class BuildElement(Element):
    217 217
             # once they are all staged and ready
    
    218 218
             with self.timed_activity("Integrating sandbox"):
    
    219 219
                 for dep in self.dependencies(Scope.BUILD):
    
    220
    -                dep.integrate(sandbox)
    
    220
    +                dep.integrate(sandbox, queue=True)
    
    221
    +            sandbox.run_queue(0)
    
    221 222
     
    
    222 223
             # Stage sources in the build root
    
    223 224
             self.stage_sources(sandbox, self.get_variable('build-root'))
    
    ... ... @@ -230,9 +231,11 @@ class BuildElement(Element):
    230 231
                 if not commands or command_name == 'configure-commands':
    
    231 232
                     continue
    
    232 233
     
    
    233
    -            with self.timed_activity("Running {}".format(command_name)):
    
    234
    +            with sandbox.queue_context_manager(self.timed_activity("Running {}".format(command_name))):
    
    234 235
                     for cmd in commands:
    
    235
    -                    self.__run_command(sandbox, cmd, command_name)
    
    236
    +                    self.__queue_command(sandbox, cmd, command_name)
    
    237
    +
    
    238
    +        sandbox.run_queue(SandboxFlags.ROOT_READ_ONLY)
    
    236 239
     
    
    237 240
             # %{install-root}/%{build-root} should normally not be written
    
    238 241
             # to - if an element later attempts to stage to a location
    
    ... ... @@ -254,9 +257,9 @@ class BuildElement(Element):
    254 257
         def prepare(self, sandbox):
    
    255 258
             commands = self.__commands['configure-commands']
    
    256 259
             if commands:
    
    257
    -            with self.timed_activity("Running configure-commands"):
    
    260
    +            with sandbox.queue_context_manager(self.timed_activity("Running configure-commands")):
    
    258 261
                     for cmd in commands:
    
    259
    -                    self.__run_command(sandbox, cmd, 'configure-commands')
    
    262
    +                    self.__queue_command(sandbox, cmd, 'configure-commands')
    
    260 263
     
    
    261 264
         def generate_script(self):
    
    262 265
             script = ""
    
    ... ... @@ -281,14 +284,18 @@ class BuildElement(Element):
    281 284
     
    
    282 285
             return commands
    
    283 286
     
    
    284
    -    def __run_command(self, sandbox, cmd, cmd_name):
    
    285
    -        self.status("Running {}".format(cmd_name), detail=cmd)
    
    287
    +    def __queue_command(self, sandbox, cmd, cmd_name):
    
    288
    +        def start_cb():
    
    289
    +            self.status("Running {}".format(cmd_name), detail=cmd)
    
    290
    +
    
    291
    +        def complete_cb(exitcode):
    
    292
    +            if exitcode != 0:
    
    293
    +                raise ElementError("Command '{}' failed with exitcode {}".format(cmd, exitcode),
    
    294
    +                                   collect=self.get_variable('install-root'))
    
    286 295
     
    
    287 296
             # Note the -e switch to 'sh' means to exit with an error
    
    288 297
             # if any untested command fails.
    
    289 298
             #
    
    290
    -        exitcode = sandbox.run(['sh', '-c', '-e', cmd + '\n'],
    
    291
    -                               SandboxFlags.ROOT_READ_ONLY)
    
    292
    -        if exitcode != 0:
    
    293
    -            raise ElementError("Command '{}' failed with exitcode {}".format(cmd, exitcode),
    
    294
    -                               collect=self.get_variable('install-root'))
    299
    +        sandbox.queue(['sh', '-c', '-e', cmd + '\n'],
    
    300
    +                      start_callback=start_cb,
    
    301
    +                      complete_callback=complete_cb)

  • buildstream/element.py
    ... ... @@ -79,6 +79,7 @@ import copy
    79 79
     from collections import OrderedDict
    
    80 80
     from collections.abc import Mapping
    
    81 81
     from contextlib import contextmanager
    
    82
    +from functools import partial
    
    82 83
     import tempfile
    
    83 84
     import shutil
    
    84 85
     
    
    ... ... @@ -753,11 +754,12 @@ class Element(Plugin):
    753 754
                     detail += "  " + "  ".join(["/" + f + "\n" for f in value])
    
    754 755
                 self.warn("Ignored files", detail=detail)
    
    755 756
     
    
    756
    -    def integrate(self, sandbox):
    
    757
    +    def integrate(self, sandbox, *, queue=False):
    
    757 758
             """Integrate currently staged filesystem against this artifact.
    
    758 759
     
    
    759 760
             Args:
    
    760 761
                sandbox (:class:`.Sandbox`): The build sandbox
    
    762
    +           queue (bool): Whether to queue commands or immediately run them
    
    761 763
     
    
    762 764
             This modifies the sysroot staged inside the sandbox so that
    
    763 765
             the sysroot is *integrated*. Only an *integrated* sandbox
    
    ... ... @@ -768,14 +770,33 @@ class Element(Plugin):
    768 770
             bstdata = self.get_public_data('bst')
    
    769 771
             environment = self.get_environment()
    
    770 772
     
    
    773
    +        if not queue:
    
    774
    +            # Element.integrate() must be called with queue=True if there are
    
    775
    +            # commands pending. Otherwise these commands are run implicitly
    
    776
    +            # by the call to run_queue() at the end of this method, which is
    
    777
    +            # likely not expected by the plugin.
    
    778
    +            assert not sandbox._has_commands_queued()
    
    779
    +
    
    771 780
             if bstdata is not None:
    
    772 781
                 commands = self.node_get_member(bstdata, list, 'integration-commands', [])
    
    773 782
                 for i in range(len(commands)):
    
    774 783
                     cmd = self.node_subst_list_element(bstdata, 'integration-commands', [i])
    
    775
    -                self.status("Running integration command", detail=cmd)
    
    776
    -                exitcode = sandbox.run(['sh', '-e', '-c', cmd], 0, env=environment, cwd='/')
    
    777
    -                if exitcode != 0:
    
    778
    -                    raise ElementError("Command '{}' failed with exitcode {}".format(cmd, exitcode))
    
    784
    +
    
    785
    +                def start_cb(cmd):
    
    786
    +                    self.status("Running integration command", detail=cmd)
    
    787
    +
    
    788
    +                def complete_cb(cmd, exitcode):
    
    789
    +                    if exitcode != 0:
    
    790
    +                        raise ElementError("Command '{}' failed with exitcode {}".format(cmd, exitcode))
    
    791
    +
    
    792
    +                sandbox.queue(['sh', '-e', '-c', cmd], env=environment, cwd='/',
    
    793
    +                              start_callback=partial(start_cb, cmd),
    
    794
    +                              complete_callback=partial(complete_cb, cmd))
    
    795
    +
    
    796
    +        if not queue:
    
    797
    +            # The caller does not allow keeping pending commands in the queue.
    
    798
    +            # Immediately run the pending commands.
    
    799
    +            sandbox.run_queue(0)
    
    779 800
     
    
    780 801
         def stage_sources(self, sandbox, directory):
    
    781 802
             """Stage this element's sources to a directory in the sandbox
    
    ... ... @@ -2093,7 +2114,15 @@ class Element(Plugin):
    2093 2114
                 self.prepare(sandbox)
    
    2094 2115
     
    
    2095 2116
                 if workspace:
    
    2096
    -                workspace.prepared = True
    
    2117
    +                def mark_workspace_prepared():
    
    2118
    +                    workspace.prepared = True
    
    2119
    +
    
    2120
    +                if sandbox._has_commands_queued():
    
    2121
    +                    # Defer workspace.prepared setting until the queued
    
    2122
    +                    # prepare commands have been executed.
    
    2123
    +                    sandbox.queue(None, start_callback=mark_workspace_prepared)
    
    2124
    +                else:
    
    2125
    +                    mark_workspace_prepared()
    
    2097 2126
     
    
    2098 2127
         def __is_cached(self, keystrength):
    
    2099 2128
             if keystrength is None:
    

  • buildstream/sandbox/_sandboxremote.py
    ... ... @@ -19,11 +19,14 @@
    19 19
     #        Jim MacArthur <jim macarthur codethink co uk>
    
    20 20
     
    
    21 21
     import os
    
    22
    +import shlex
    
    23
    +from collections import deque
    
    22 24
     from urllib.parse import urlparse
    
    23 25
     
    
    24 26
     import grpc
    
    25 27
     
    
    26 28
     from . import Sandbox
    
    29
    +from .sandbox import _QueueEntryType
    
    27 30
     from ..storage._filebaseddirectory import FileBasedDirectory
    
    28 31
     from ..storage._casbaseddirectory import CasBasedDirectory
    
    29 32
     from .._protos.build.bazel.remote.execution.v2 import remote_execution_pb2, remote_execution_pb2_grpc
    
    ... ... @@ -189,6 +192,8 @@ class SandboxRemote(Sandbox):
    189 192
             self._set_virtual_directory(new_dir)
    
    190 193
     
    
    191 194
         def run(self, command, flags, *, cwd=None, env=None):
    
    195
    +        stdout, stderr = self._get_output()
    
    196
    +
    
    192 197
             # Upload sources
    
    193 198
             upload_vdir = self.get_virtual_directory()
    
    194 199
     
    
    ... ... @@ -241,13 +246,90 @@ class SandboxRemote(Sandbox):
    241 246
     
    
    242 247
             action_result = execution_response.result
    
    243 248
     
    
    249
    +        if stdout:
    
    250
    +            if action_result.stdout_raw:
    
    251
    +                stdout.write(str(action_result.stdout_raw, 'utf-8', errors='ignore'))
    
    252
    +        if stderr:
    
    253
    +            if action_result.stderr_raw:
    
    254
    +                stderr.write(str(action_result.stderr_raw, 'utf-8', errors='ignore'))
    
    255
    +
    
    244 256
             if action_result.exit_code != 0:
    
    245 257
                 # A normal error during the build: the remote execution system
    
    246 258
                 # has worked correctly but the command failed.
    
    247
    -            # action_result.stdout and action_result.stderr also contains
    
    248
    -            # build command outputs which we ignore at the moment.
    
    249 259
                 return action_result.exit_code
    
    250 260
     
    
    251 261
             self.process_job_output(action_result.output_directories, action_result.output_files)
    
    252 262
     
    
    253 263
             return 0
    
    264
    +
    
    265
    +    def run_queue(self, flags):
    
    266
    +        # If a command fails, all remaining commands in the queue must be discarded.
    
    267
    +        queue = self._queue
    
    268
    +        self._queue = []
    
    269
    +
    
    270
    +        script = ""
    
    271
    +        i = 0
    
    272
    +        first = None
    
    273
    +        for entry in queue:
    
    274
    +            if entry.type == _QueueEntryType.COMMAND and entry.command:
    
    275
    +                if first is None:
    
    276
    +                    # First command in queue
    
    277
    +                    first = entry
    
    278
    +                else:
    
    279
    +                    if entry.cwd != cwd:
    
    280
    +                        script += "mkdir -p {}\n".format(entry.cwd)
    
    281
    +                        script += "cd {}\n".format(entry.cwd)
    
    282
    +                    for key in env.keys():
    
    283
    +                        if key not in entry.env:
    
    284
    +                            script += "unset {}\n".format(key)
    
    285
    +                    for key, value in entry.env.items():
    
    286
    +                        if key not in env or env[key] != value:
    
    287
    +                            script += "export {}={}\n".format(key, value)
    
    288
    +                cwd = entry.cwd
    
    289
    +                env = entry.env
    
    290
    +
    
    291
    +                cmdline = ' '.join(shlex.quote(cmd) for cmd in entry.command)
    
    292
    +                script += "({})\n".format(cmdline)
    
    293
    +                script += "RETVAL=$?\n"
    
    294
    +                script += "if [ $RETVAL -ne 0 ] ; then\n"
    
    295
    +                # Report failing command and exit code to stderr (and then back to client)
    
    296
    +                script += "  echo -e '\nbst-command-failure:' {} $RETVAL >&2\n".format(i)
    
    297
    +                script += "  exit 1\n"
    
    298
    +                script += "fi\n"
    
    299
    +            i += 1
    
    300
    +
    
    301
    +        if first:
    
    302
    +            exit_code = self.run(['sh', '-c', script], flags, cwd=first.cwd, env=first.env)
    
    303
    +        else:
    
    304
    +            exit_code = 0
    
    305
    +
    
    306
    +        if exit_code != 0:
    
    307
    +            # TODO get failed command and exit code from stderr
    
    308
    +            failed_command = 0
    
    309
    +            command_exit_code = 1
    
    310
    +
    
    311
    +        i = 0
    
    312
    +        # Stack of pending context manager exit methods
    
    313
    +        exit_stack = deque()
    
    314
    +        try:
    
    315
    +            for entry in queue:
    
    316
    +                if entry.type == _QueueEntryType.COMMAND:
    
    317
    +                    entry.start_callback()
    
    318
    +                    if exit_code == 0 or i < failed_command:
    
    319
    +                        # Command succeeded
    
    320
    +                        entry.complete_callback(0)
    
    321
    +                    else:
    
    322
    +                        # Command failed
    
    323
    +                        entry.complete_callback(command_exit_code)
    
    324
    +                        break
    
    325
    +                elif entry.type == _QueueEntryType.CONTEXT_MANAGER_ENTER:
    
    326
    +                    entry.cm.__enter__()
    
    327
    +                    exit_stack.append(entry.cm.__exit__)
    
    328
    +                elif entry.type == _QueueEntryType.CONTEXT_MANAGER_EXIT:
    
    329
    +                    exit_cb = exit_stack.pop()
    
    330
    +                    exit_cb(None, None, None)
    
    331
    +                i += 1
    
    332
    +        finally:
    
    333
    +            while exit_stack:
    
    334
    +                exit_cb = exit_stack.pop()
    
    335
    +                exit_cb(None, None, None)

  • buildstream/sandbox/sandbox.py
    ... ... @@ -29,6 +29,9 @@ See also: :ref:`sandboxing`.
    29 29
     """
    
    30 30
     
    
    31 31
     import os
    
    32
    +from collections import deque, namedtuple
    
    33
    +from contextlib import contextmanager
    
    34
    +
    
    32 35
     from .._exceptions import ImplError, BstError
    
    33 36
     from ..storage._filebaseddirectory import FileBasedDirectory
    
    34 37
     from ..storage._casbaseddirectory import CasBasedDirectory
    
    ... ... @@ -71,6 +74,22 @@ class SandboxFlags():
    71 74
         """
    
    72 75
     
    
    73 76
     
    
    77
    +# _QueueEntryType()
    
    78
    +#
    
    79
    +# A type for sandbox queue entries.
    
    80
    +#
    
    81
    +class _QueueEntryType():
    
    82
    +
    
    83
    +    # A queued command.
    
    84
    +    COMMAND = 1
    
    85
    +
    
    86
    +    # A queued context manager enter.
    
    87
    +    CONTEXT_MANAGER_ENTER = 2
    
    88
    +
    
    89
    +    # A queued context manager exit.
    
    90
    +    CONTEXT_MANAGER_EXIT = 3
    
    91
    +
    
    92
    +
    
    74 93
     class Sandbox():
    
    75 94
         """Sandbox()
    
    76 95
     
    
    ... ... @@ -121,6 +140,9 @@ class Sandbox():
    121 140
             # directory via get_directory.
    
    122 141
             self._never_cache_vdirs = False
    
    123 142
     
    
    143
    +        # Queued commands
    
    144
    +        self._queue = []
    
    145
    +
    
    124 146
         def get_directory(self):
    
    125 147
             """Fetches the sandbox root directory
    
    126 148
     
    
    ... ... @@ -237,6 +259,97 @@ class Sandbox():
    237 259
             raise ImplError("Sandbox of type '{}' does not implement run()"
    
    238 260
                             .format(type(self).__name__))
    
    239 261
     
    
    262
    +    def queue(self, command, *, cwd=None, env=None, start_callback=None, complete_callback=None):
    
    263
    +        """Queue a command to be run in the sandbox.
    
    264
    +
    
    265
    +        If the command fails, commands queued later will not be executed.
    
    266
    +        The callbacks are not guaranteed to be invoked in real time.
    
    267
    +
    
    268
    +        Args:
    
    269
    +            command (list): The command to run in the sandboxed environment, as a list
    
    270
    +                            of strings starting with the binary to run.
    
    271
    +            cwd (str): The sandbox relative working directory in which to run the command.
    
    272
    +            env (dict): A dictionary of string key, value pairs to set as environment
    
    273
    +                        variables inside the sandbox environment.
    
    274
    +            start_callback (callable): Called when the command starts.
    
    275
    +            complete_callback (callble): Called when the command completes
    
    276
    +                                         with the exit code as argument.
    
    277
    +
    
    278
    +        .. note::
    
    279
    +
    
    280
    +           The optional *cwd* argument will default to the value set with
    
    281
    +           :func:`~buildstream.sandbox.Sandbox.set_work_directory`
    
    282
    +
    
    283
    +        *Since: 1.4*
    
    284
    +        """
    
    285
    +        entry = namedtuple('QueueEntry', ['type', 'command', 'cwd', 'env', 'start_callback', 'complete_callback'])
    
    286
    +        entry.type = _QueueEntryType.COMMAND
    
    287
    +        entry.command = command
    
    288
    +        entry.cwd = self._get_work_directory(cwd=cwd)
    
    289
    +        entry.env = self._get_environment(cwd=cwd, env=env)
    
    290
    +        entry.start_callback = start_callback
    
    291
    +        entry.complete_callback = complete_callback
    
    292
    +        self._queue.append(entry)
    
    293
    +
    
    294
    +    @contextmanager
    
    295
    +    def queue_context_manager(self, cm):
    
    296
    +        """Queue entering and exiting a context manager
    
    297
    +
    
    298
    +        Args:
    
    299
    +           cm: A context manager
    
    300
    +
    
    301
    +        *Since: 1.4*
    
    302
    +        """
    
    303
    +        entry = namedtuple('QueueEntry', ['type', 'cm'])
    
    304
    +        entry.type = _QueueEntryType.CONTEXT_MANAGER_ENTER
    
    305
    +        entry.cm = cm
    
    306
    +        self._queue.append(entry)
    
    307
    +        yield
    
    308
    +        entry = namedtuple('QueueEntry', ['type'])
    
    309
    +        entry.type = _QueueEntryType.CONTEXT_MANAGER_EXIT
    
    310
    +        self._queue.append(entry)
    
    311
    +
    
    312
    +    def run_queue(self, flags):
    
    313
    +        """Run a command in the sandbox.
    
    314
    +
    
    315
    +        Args:
    
    316
    +            flags (:class:`.SandboxFlags`): The flags for running this command.
    
    317
    +
    
    318
    +        Raises:
    
    319
    +            (:class:`.ProgramNotFoundError`): If a host tool which the given sandbox
    
    320
    +                                              implementation requires is not found.
    
    321
    +
    
    322
    +        *Since: 1.4*
    
    323
    +        """
    
    324
    +        # If a command fails, all remaining commands in the queue must be discarded.
    
    325
    +        queue = self._queue
    
    326
    +        self._queue = []
    
    327
    +
    
    328
    +        # Stack of pending context manager exit methods
    
    329
    +        exit_stack = deque()
    
    330
    +        try:
    
    331
    +            for entry in queue:
    
    332
    +                if entry.type == _QueueEntryType.COMMAND:
    
    333
    +                    if entry.start_callback:
    
    334
    +                        entry.start_callback()
    
    335
    +                    if entry.command:
    
    336
    +                        # pylint: disable=assignment-from-no-return
    
    337
    +                        exit_code = self.run(entry.command, flags, cwd=entry.cwd, env=entry.env)
    
    338
    +                    if entry.complete_callback:
    
    339
    +                        entry.complete_callback(exit_code)
    
    340
    +                    if exit_code != 0:
    
    341
    +                        break
    
    342
    +                elif entry.type == _QueueEntryType.CONTEXT_MANAGER_ENTER:
    
    343
    +                    entry.cm.__enter__()
    
    344
    +                    exit_stack.append(entry.cm.__exit__)
    
    345
    +                elif entry.type == _QueueEntryType.CONTEXT_MANAGER_EXIT:
    
    346
    +                    exit_cb = exit_stack.pop()
    
    347
    +                    exit_cb(None, None, None)
    
    348
    +        finally:
    
    349
    +            while exit_stack:
    
    350
    +                exit_cb = exit_stack.pop()
    
    351
    +                exit_cb(None, None, None)
    
    352
    +
    
    240 353
         ################################################
    
    241 354
         #               Private methods                #
    
    242 355
         ################################################
    
    ... ... @@ -385,3 +498,12 @@ class Sandbox():
    385 498
                     return True
    
    386 499
     
    
    387 500
             return False
    
    501
    +
    
    502
    +    # _has_commands_queued()
    
    503
    +    #
    
    504
    +    # Returns whether the sandbox has a non-empty queue of pending commands.
    
    505
    +    #
    
    506
    +    # Returns:
    
    507
    +    #    (bool): Whether the command queue is non-empty.
    
    508
    +    def _has_commands_queued(self):
    
    509
    +        return len(self._queue) > 0

  • tests/frontend/init.py
    ... ... @@ -3,6 +3,7 @@ import pytest
    3 3
     from tests.testutils import cli
    
    4 4
     
    
    5 5
     from buildstream import _yaml
    
    6
    +from buildstream._frontend.app import App
    
    6 7
     from buildstream._exceptions import ErrorDomain, LoadErrorReason
    
    7 8
     from buildstream._versions import BST_FORMAT_VERSION
    
    8 9
     
    
    ... ... @@ -98,3 +99,34 @@ def test_bad_element_path(cli, tmpdir, element_path):
    98 99
             'init', '--project-name', 'foo', '--element-path', element_path
    
    99 100
         ])
    
    100 101
         result.assert_main_error(ErrorDomain.APP, 'invalid-element-path')
    
    102
    +
    
    103
    +
    
    104
    +@pytest.mark.parametrize("element_path", [('foo'), ('foo/bar')])
    
    105
    +def test_element_path_interactive(cli, tmp_path, monkeypatch, element_path):
    
    106
    +    project = tmp_path
    
    107
    +    project_conf_path = project.joinpath('project.conf')
    
    108
    +
    
    109
    +    class DummyInteractiveApp(App):
    
    110
    +        def __init__(self, *args, **kwargs):
    
    111
    +            super().__init__(*args, **kwargs)
    
    112
    +            self.interactive = True
    
    113
    +
    
    114
    +        @classmethod
    
    115
    +        def create(cls, *args, **kwargs):
    
    116
    +            return DummyInteractiveApp(*args, **kwargs)
    
    117
    +
    
    118
    +        def _init_project_interactive(self, *args, **kwargs):
    
    119
    +            return ('project_name', '0', element_path)
    
    120
    +
    
    121
    +    monkeypatch.setattr(App, 'create', DummyInteractiveApp.create)
    
    122
    +
    
    123
    +    result = cli.run(project=str(project), args=['init'])
    
    124
    +    result.assert_success()
    
    125
    +
    
    126
    +    full_element_path = project.joinpath(element_path)
    
    127
    +    assert full_element_path.exists()
    
    128
    +
    
    129
    +    project_conf = _yaml.load(str(project_conf_path))
    
    130
    +    assert project_conf['name'] == 'project_name'
    
    131
    +    assert project_conf['format-version'] == '0'
    
    132
    +    assert project_conf['element-path'] == element_path



  • [Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]