From f52ddfb40cd063e77722e435afcb0f4a3f565059 Mon Sep 17 00:00:00 2001
From: William Salmon <will.salmon@codethink.co.uk>
Date: Fri, 2 Nov 2018 15:49:12 +0000
Subject: [PATCH] Workspace CLI update

This is to update the workspace CLI to as agreed on the mailing list
https://mail.gnome.org/archives/buildstream-list/2018-September/msg00046.html

This patch also introduces the default workspace directory.
---
 buildstream/_context.py                    |   7 +-
 buildstream/_frontend/cli.py               |  37 ++++---
 buildstream/_stream.py                     | 105 ++++++++++++--------
 buildstream/data/userconfig.yaml           |   3 +
 buildstream/utils.py                       |  14 +++
 doc/sessions/developing.run                |   2 +-
 tests/examples/developing.py               |   4 +-
 tests/examples/junctions.py                |   2 +-
 tests/frontend/buildcheckout.py            |   2 +-
 tests/frontend/cross_junction_workspace.py |   2 +-
 tests/frontend/workspace.py                | 108 ++++++++++++++++++++-
 tests/integration/shell.py                 |   2 +-
 tests/integration/workspace.py             |  12 +--
 tests/plugins/filter.py                    |   8 +-
 14 files changed, 230 insertions(+), 78 deletions(-)

diff --git a/buildstream/_context.py b/buildstream/_context.py
index 876b74712f..788c513b46 100644
--- a/buildstream/_context.py
+++ b/buildstream/_context.py
@@ -59,6 +59,9 @@ class Context():
         # The directory where build sandboxes will be created
         self.builddir = None
 
+        # Default root location for workspaces
+        self.workspacedir = None
+
         # The local binary artifact cache directory
         self.artifactdir = None
 
@@ -160,10 +163,10 @@ class Context():
         _yaml.node_validate(defaults, [
             'sourcedir', 'builddir', 'artifactdir', 'logdir',
             'scheduler', 'artifacts', 'logging', 'projects',
-            'cache'
+            'cache', 'workspacedir',
         ])
 
-        for directory in ['sourcedir', 'builddir', 'artifactdir', 'logdir']:
+        for directory in ['sourcedir', 'builddir', 'artifactdir', 'logdir', 'workspacedir']:
             # Allow the ~ tilde expansion and any environment variables in
             # path specification in the config files.
             #
diff --git a/buildstream/_frontend/cli.py b/buildstream/_frontend/cli.py
index 85632959fe..3e796fe009 100644
--- a/buildstream/_frontend/cli.py
+++ b/buildstream/_frontend/cli.py
@@ -6,6 +6,7 @@ from .. import _yaml
 from .._exceptions import BstError, LoadError, AppError
 from .._versions import BST_FORMAT_VERSION
 from .complete import main_bashcomplete, complete_path, CompleteUnhandled
+from ..utils import DirectoryDescription
 
 
 ##################################################################
@@ -678,28 +679,36 @@ def workspace():
 @click.option('--no-checkout', default=False, is_flag=True,
               help="Do not checkout the source, only link to the given directory")
 @click.option('--force', '-f', default=False, is_flag=True,
-              help="Overwrite files existing in checkout directory")
+              help="The workspace will be created even if the directory in which it will be created is not empty " +
+              "or if a workspace for that element already exists")
 @click.option('--track', 'track_', default=False, is_flag=True,
               help="Track and fetch new source references before checking out the workspace")
-@click.argument('element',
-                type=click.Path(readable=False))
-@click.argument('directory', type=click.Path(file_okay=False))
+@click.option('--directory', type=click.Path(file_okay=False), default=None,
+              help="Only for use when a single Element is given: Set the directory to use to create the workspace")
+@click.argument('elements', nargs=-1, type=click.Path(readable=False))
 @click.pass_obj
-def workspace_open(app, no_checkout, force, track_, element, directory):
+def workspace_open(app, no_checkout, force, track_, directory, elements):
     """Open a workspace for manual source modification"""
-
-    if os.path.exists(directory):
-
-        if not os.path.isdir(directory):
-            click.echo("Checkout directory is not a directory: {}".format(directory), err=True)
+    directories = []
+    if directory is not None:
+        if len(elements) > 1:
+            click.echo("Directory option can only be used if a single element is given", err=True)
             sys.exit(-1)
+        if os.path.exists(directory):
+            if not os.path.isdir(directory):
+                click.echo("Directory path is not a directory: {}".format(directory), err=True)
+                sys.exit(-1)
 
-        if not (no_checkout or force) and os.listdir(directory):
-            click.echo("Checkout directory is not empty: {}".format(directory), err=True)
-            sys.exit(-1)
+            if not (no_checkout or force) and os.listdir(directory):
+                click.echo("Directory path is not empty: {}".format(directory), err=True)
+                sys.exit(-1)
+        directories.append(DirectoryDescription(directory, use_default=False))
+    else:
+        for element in elements:
+            directories.append(DirectoryDescription(element.rstrip('.bst')))
 
     with app.initialized():
-        app.stream.workspace_open(element, directory,
+        app.stream.workspace_open(elements, directories,
                                   no_checkout=no_checkout,
                                   track_first=track_,
                                   force=force)
diff --git a/buildstream/_stream.py b/buildstream/_stream.py
index 6e2e8b25b2..d0f9a56b98 100644
--- a/buildstream/_stream.py
+++ b/buildstream/_stream.py
@@ -448,44 +448,29 @@ class Stream():
     # Open a project workspace
     #
     # Args:
-    #    target (str): The target element to open the workspace for
-    #    directory (str): The directory to stage the source in
+    #    target (list): List of target elements to open workspaces for
+    #    directory (list): List of DirectoryDescription objects to stage the source in
     #    no_checkout (bool): Whether to skip checking out the source
     #    track_first (bool): Whether to track and fetch first
     #    force (bool): Whether to ignore contents in an existing directory
     #
-    def workspace_open(self, target, directory, *,
+    def workspace_open(self, targets, directories, *,
                        no_checkout,
                        track_first,
                        force):
+        # This function is a little funny but it is trying to be as atomic as possible.
 
         if track_first:
-            track_targets = (target,)
+            track_targets = targets
         else:
             track_targets = ()
 
-        elements, track_elements = self._load((target,), track_targets,
+        elements, track_elements = self._load(targets, track_targets,
                                               selection=PipelineSelection.REDIRECT,
                                               track_selection=PipelineSelection.REDIRECT)
-        target = elements[0]
-        directory = os.path.abspath(directory)
-
-        if not list(target.sources()):
-            build_depends = [x.name for x in target.dependencies(Scope.BUILD, recurse=False)]
-            if not build_depends:
-                raise StreamError("The given element has no sources")
-            detail = "Try opening a workspace on one of its dependencies instead:\n"
-            detail += "  \n".join(build_depends)
-            raise StreamError("The given element has no sources", detail=detail)
 
         workspaces = self._context.get_workspaces()
 
-        # Check for workspace config
-        workspace = workspaces.get_workspace(target._get_full_name())
-        if workspace and not force:
-            raise StreamError("Workspace '{}' is already defined at: {}"
-                              .format(target.name, workspace.get_absolute_path()))
-
         # If we're going to checkout, we need at least a fetch,
         # if we were asked to track first, we're going to fetch anyway.
         #
@@ -495,29 +480,69 @@ class Stream():
                 track_elements = elements
             self._fetch(elements, track_elements=track_elements)
 
-        if not no_checkout and target._get_consistency() != Consistency.CACHED:
-            raise StreamError("Could not stage uncached source. " +
-                              "Use `--track` to track and " +
-                              "fetch the latest version of the " +
-                              "source.")
+        expanded_directories = []
+        #  To try to be more atomic, loop through the elements and raise any errors we can early
+        for target, directory_obj in zip(elements, directories):
+
+            if not list(target.sources()):
+                build_depends = [x.name for x in target.dependencies(Scope.BUILD, recurse=False)]
+                if not build_depends:
+                    raise StreamError("The element {}  has no sources".format(target.name))
+                detail = "Try opening a workspace on one of its dependencies instead:\n"
+                detail += "  \n".join(build_depends)
+                raise StreamError("The element {} has no sources".format(target.name), detail=detail)
+
+            # Check for workspace config
+            workspace = workspaces.get_workspace(target._get_full_name())
+            if workspace and not force:
+                raise StreamError("Workspace '{}' is already defined at: {}"
+                                  .format(target.name, workspace.get_absolute_path()))
+
+            if not no_checkout and target._get_consistency() != Consistency.CACHED:
+                raise StreamError("Could not stage uncached source. For {} ".format(target.name) +
+                                  "Use `--track` to track and " +
+                                  "fetch the latest version of the " +
+                                  "source.")
+
+            if directory_obj.use_default:
+                directory = os.path.abspath(os.path.join(self._context.workspacedir, directory_obj.directory))
+            else:
+                directory = directory_obj.directory
 
-        if workspace:
-            workspaces.delete_workspace(target._get_full_name())
-            workspaces.save_config()
-            shutil.rmtree(directory)
-        try:
-            os.makedirs(directory, exist_ok=True)
-        except OSError as e:
-            raise StreamError("Failed to create workspace directory: {}".format(e)) from e
+            expanded_directories.append(directory)
 
-        workspaces.create_workspace(target._get_full_name(), directory)
+        # So far this function has tried to catch as many issues as possible with out making any changes
+        # Now it dose the bits that can not be made atomic.
+        targetGenerator = zip(elements, expanded_directories)
+        for target, directory in targetGenerator:
+            self._message(MessageType.INFO, "Creating workspace for element {}"
+                          .format(target.name))
 
-        if not no_checkout:
-            with target.timed_activity("Staging sources to {}".format(directory)):
-                target._open_workspace()
+            workspace = workspaces.get_workspace(target._get_full_name())
+            if workspace:
+                workspaces.delete_workspace(target._get_full_name())
+                workspaces.save_config()
+                shutil.rmtree(directory)
+            try:
+                os.makedirs(directory, exist_ok=True)
+            except OSError as e:
+                todo_elements = " ".join([str(target.name) for target, directory_dict in targetGenerator])
+                if todo_elements:
+                    # This output should make creating the remaining workspaces as easy as possible.
+                    todo_elements = "\nDid not try to create workspaces for " + todo_elements
+                raise StreamError("Failed to create workspace directory: {}".format(e) + todo_elements) from e
 
-        workspaces.save_config()
-        self._message(MessageType.INFO, "Saved workspace configuration")
+            workspaces.create_workspace(target._get_full_name(), directory)
+
+            if not no_checkout:
+                with target.timed_activity("Staging sources to {}".format(directory)):
+                    target._open_workspace()
+
+            # Saving the workspace once it is set up means that if the next workspace fails to be created before
+            # the configuration gets saved. The successfully created workspace still gets saved.
+            workspaces.save_config()
+            self._message(MessageType.INFO, "Added element {} to the workspace configuration"
+                          .format(target._get_full_name()))
 
     # workspace_close
     #
diff --git a/buildstream/data/userconfig.yaml b/buildstream/data/userconfig.yaml
index efe419cfc6..b111525675 100644
--- a/buildstream/data/userconfig.yaml
+++ b/buildstream/data/userconfig.yaml
@@ -22,6 +22,9 @@ artifactdir: ${XDG_CACHE_HOME}/buildstream/artifacts
 # Location to store build logs
 logdir: ${XDG_CACHE_HOME}/buildstream/logs
 
+# Default root location for workspaces, blank for no default set.
+workspacedir: .
+
 #
 #    Cache
 #
diff --git a/buildstream/utils.py b/buildstream/utils.py
index dc66f3e623..81d1622fb2 100644
--- a/buildstream/utils.py
+++ b/buildstream/utils.py
@@ -105,6 +105,20 @@ class FileListResult():
         return ret
 
 
+class DirectoryDescription():
+    """
+    This object is used to keep information about directories in a nice tidy object.
+    """
+    def __init__(self, directory, *, use_default=True):
+        """
+        Args:
+            directory (str): The path to the directory this object describes
+            use_default (bool): Weather to process the directory so it is in the default folder.
+        """
+        self.directory = directory
+        self.use_default = use_default
+
+
 def list_relative_paths(directory, *, list_dirs=True):
     """A generator for walking directory relative paths
 
diff --git a/doc/sessions/developing.run b/doc/sessions/developing.run
index 10beb2ad03..55d9a3a6a8 100644
--- a/doc/sessions/developing.run
+++ b/doc/sessions/developing.run
@@ -7,7 +7,7 @@ commands:
 # Capture workspace open output 
 - directory: ../examples/developing/
   output: ../source/sessions/developing-workspace-open.html
-  command: workspace open hello.bst workspace_hello
+  command: workspace open hello.bst --directory workspace_hello
 
 # Catpure output from workspace list
 - directory: ../examples/developing/
diff --git a/tests/examples/developing.py b/tests/examples/developing.py
index bca6ac61d7..d598290373 100644
--- a/tests/examples/developing.py
+++ b/tests/examples/developing.py
@@ -55,7 +55,7 @@ def test_open_workspace(cli, tmpdir, datafiles):
     project = os.path.join(datafiles.dirname, datafiles.basename)
     workspace_dir = os.path.join(str(tmpdir), "workspace_hello")
 
-    result = cli.run(project=project, args=['workspace', 'open', '-f', 'hello.bst', workspace_dir])
+    result = cli.run(project=project, args=['workspace', 'open', '-f', '--directory', workspace_dir, 'hello.bst', ])
     result.assert_success()
 
     result = cli.run(project=project, args=['workspace', 'list'])
@@ -72,7 +72,7 @@ def test_make_change_in_workspace(cli, tmpdir, datafiles):
     project = os.path.join(datafiles.dirname, datafiles.basename)
     workspace_dir = os.path.join(str(tmpdir), "workspace_hello")
 
-    result = cli.run(project=project, args=['workspace', 'open', '-f', 'hello.bst', workspace_dir])
+    result = cli.run(project=project, args=['workspace', 'open', '-f', '--directory', workspace_dir, 'hello.bst'])
     result.assert_success()
 
     result = cli.run(project=project, args=['workspace', 'list'])
diff --git a/tests/examples/junctions.py b/tests/examples/junctions.py
index d2a6538847..bcb4177ad8 100644
--- a/tests/examples/junctions.py
+++ b/tests/examples/junctions.py
@@ -44,7 +44,7 @@ def test_open_cross_junction_workspace(cli, tmpdir, datafiles):
     workspace_dir = os.path.join(str(tmpdir), "workspace_hello_junction")
 
     result = cli.run(project=project,
-                     args=['workspace', 'open', 'hello-junction.bst:hello.bst', workspace_dir])
+                     args=['workspace', 'open', '--directory', workspace_dir, 'hello-junction.bst:hello.bst'])
     result.assert_success()
 
     result = cli.run(project=project,
diff --git a/tests/frontend/buildcheckout.py b/tests/frontend/buildcheckout.py
index a0b4617624..159af2d744 100644
--- a/tests/frontend/buildcheckout.py
+++ b/tests/frontend/buildcheckout.py
@@ -509,7 +509,7 @@ def test_build_checkout_workspaced_junction(cli, tmpdir, datafiles):
 
     # Now open a workspace on the junction
     #
-    result = cli.run(project=project, args=['workspace', 'open', 'junction.bst', workspace])
+    result = cli.run(project=project, args=['workspace', 'open', '--directory', workspace, 'junction.bst'])
     result.assert_success()
     filename = os.path.join(workspace, 'files', 'etc-files', 'etc', 'animal.conf')
 
diff --git a/tests/frontend/cross_junction_workspace.py b/tests/frontend/cross_junction_workspace.py
index eb2bc2eb8b..fb2b34c434 100644
--- a/tests/frontend/cross_junction_workspace.py
+++ b/tests/frontend/cross_junction_workspace.py
@@ -47,7 +47,7 @@ def open_cross_junction(cli, tmpdir):
     workspace = tmpdir.join("workspace")
 
     element = 'sub.bst:data.bst'
-    args = ['workspace', 'open', element, str(workspace)]
+    args = ['workspace', 'open', '--directory', str(workspace), element]
     result = cli.run(project=project, args=args)
     result.assert_success()
 
diff --git a/tests/frontend/workspace.py b/tests/frontend/workspace.py
index f620d4c133..62653a8b63 100644
--- a/tests/frontend/workspace.py
+++ b/tests/frontend/workspace.py
@@ -25,6 +25,7 @@
 #
 
 import os
+import stat
 import pytest
 import shutil
 import subprocess
@@ -182,6 +183,103 @@ def test_open_bzr_customize(cli, tmpdir, datafiles):
     assert(expected_output_str in str(output))
 
 
+@pytest.mark.datafiles(DATA_DIR)
+def test_open_multi(cli, tmpdir, datafiles):
+
+    workspace_object = WorkspaceCreater(cli, tmpdir, datafiles)
+    workspaces = workspace_object.open_workspaces(repo_kinds, False)
+
+    for (elname, workspace), kind in zip(workspaces, repo_kinds):
+        assert kind in elname
+        workspace_lsdir = os.listdir(workspace)
+        if kind == 'git':
+            assert('.git' in workspace_lsdir)
+        elif kind == 'bzr':
+            assert('.bzr' in workspace_lsdir)
+        else:
+            assert not ('.git' in workspace_lsdir)
+            assert not ('.bzr' in workspace_lsdir)
+
+
+@pytest.mark.datafiles(DATA_DIR)
+def test_open_multi_unwritable(cli, tmpdir, datafiles):
+    workspace_object = WorkspaceCreater(cli, tmpdir, datafiles)
+
+    element_tuples = workspace_object.create_workspace_elements(repo_kinds, False, repo_kinds)
+    os.makedirs(workspace_object.workspace_cmd, exist_ok=True)
+
+    # Now open the workspace, this should have the effect of automatically
+    # tracking & fetching the source from the repo.
+    args = ['workspace', 'open']
+    args.extend([element_name for element_name, workspace_dir_suffix in element_tuples])
+    cli.configure({'workspacedir': workspace_object.workspace_cmd})
+
+    cwdstat = os.stat(workspace_object.workspace_cmd)
+    try:
+        os.chmod(workspace_object.workspace_cmd, cwdstat.st_mode - stat.S_IWRITE)
+        result = workspace_object.cli.run(project=workspace_object.project_path, args=args)
+    finally:
+        # Using this finally to make sure we always put thing back how they should be.
+        os.chmod(workspace_object.workspace_cmd, cwdstat.st_mode)
+
+    result.assert_main_error(ErrorDomain.STREAM, None)
+    # Normally we avoid checking stderr in favour of using the mechine readable result.assert_main_error
+    # But Tristan was very keen that the names of the elements left needing workspaces were present in the out put
+    assert (" ".join([element_name for element_name, workspace_dir_suffix in element_tuples[1:]]) in result.stderr)
+
+
+@pytest.mark.datafiles(DATA_DIR)
+def test_open_multi_with_directory(cli, tmpdir, datafiles):
+    workspace_object = WorkspaceCreater(cli, tmpdir, datafiles)
+
+    element_tuples = workspace_object.create_workspace_elements(repo_kinds, False, repo_kinds)
+    os.makedirs(workspace_object.workspace_cmd, exist_ok=True)
+
+    # Now open the workspace, this should have the effect of automatically
+    # tracking & fetching the source from the repo.
+    args = ['workspace', 'open']
+    args.extend(['--directory', 'any/dir/should/fail'])
+
+    args.extend([element_name for element_name, workspace_dir_suffix in element_tuples])
+    result = workspace_object.cli.run(cwd=workspace_object.workspace_cmd, project=workspace_object.project_path,
+                                      args=args)
+
+    result.assert_main_error(ErrorDomain.CAS, None)
+    assert ("Directory option can only be used if a single element is given" in result.stderr)
+
+
+@pytest.mark.datafiles(DATA_DIR)
+def test_open_defaultlocation(cli, tmpdir, datafiles):
+    workspace_object = WorkspaceCreater(cli, tmpdir, datafiles)
+
+    ((element_name, workspace_dir), ) = workspace_object.create_workspace_elements(['git'], False, ['git'])
+    os.makedirs(workspace_object.workspace_cmd, exist_ok=True)
+
+    # Now open the workspace, this should have the effect of automatically
+    # tracking & fetching the source from the repo.
+    args = ['workspace', 'open']
+    args.append(element_name)
+
+    # In the other tests we set the cmd to workspace_object.workspace_cmd with the optional
+    # argument, cwd for the workspace_object.cli.run function. But hear we set the default
+    # workspace location to workspace_object.workspace_cmd and run the cli.run function with
+    # no cwd option so that it runs in the project directory.
+    cli.configure({'workspacedir': workspace_object.workspace_cmd})
+    result = workspace_object.cli.run(project=workspace_object.project_path,
+                                      args=args)
+
+    result.assert_success()
+
+    assert cli.get_element_state(workspace_object.project_path, element_name) == 'buildable'
+
+    # Check that the executable hello file is found in the workspace
+    # even though the cli.run function was not run with cwd = workspace_object.workspace_cmd
+    # the workspace should be created in there as we used the 'workspacedir' configuration
+    # option.
+    filename = os.path.join(workspace_dir, 'usr', 'bin', 'hello')
+    assert os.path.exists(filename)
+
+
 @pytest.mark.datafiles(DATA_DIR)
 @pytest.mark.parametrize("kind", repo_kinds)
 def test_open_track(cli, tmpdir, datafiles, kind):
@@ -204,7 +302,7 @@ def test_open_force(cli, tmpdir, datafiles, kind):
 
     # Now open the workspace again with --force, this should happily succeed
     result = cli.run(project=project, args=[
-        'workspace', 'open', '--force', element_name, workspace
+        'workspace', 'open', '--force', '--directory', workspace, element_name
     ])
     result.assert_success()
 
@@ -219,7 +317,7 @@ def test_open_force_open(cli, tmpdir, datafiles, kind):
 
     # Now open the workspace again with --force, this should happily succeed
     result = cli.run(project=project, args=[
-        'workspace', 'open', '--force', element_name, workspace
+        'workspace', 'open', '--force', '--directory', workspace, element_name
     ])
     result.assert_success()
 
@@ -250,7 +348,7 @@ def test_open_force_different_workspace(cli, tmpdir, datafiles, kind):
 
     # Now open the workspace again with --force, this should happily succeed
     result = cli.run(project=project, args=[
-        'workspace', 'open', '--force', element_name2, workspace
+        'workspace', 'open', '--force', '--directory', workspace, element_name2
     ])
 
     # Assert that the file in workspace 1 has been replaced
@@ -558,7 +656,7 @@ def test_buildable_no_ref(cli, tmpdir, datafiles):
     # Now open the workspace. We don't need to checkout the source though.
     workspace = os.path.join(str(tmpdir), 'workspace-no-ref')
     os.makedirs(workspace)
-    args = ['workspace', 'open', '--no-checkout', element_name, workspace]
+    args = ['workspace', 'open', '--no-checkout', '--directory', workspace, element_name]
     result = cli.run(project=project, args=args)
     result.assert_success()
 
@@ -820,7 +918,7 @@ def test_list_supported_workspace(cli, tmpdir, datafiles, workspace_cfg, expecte
                             element_name))
 
     # Make a change to the workspaces file
-    result = cli.run(project=project, args=['workspace', 'open', element_name, workspace])
+    result = cli.run(project=project, args=['workspace', 'open', '--directory', workspace, element_name])
     result.assert_success()
     result = cli.run(project=project, args=['workspace', 'close', '--remove-dir', element_name])
     result.assert_success()
diff --git a/tests/integration/shell.py b/tests/integration/shell.py
index 1db29a0c44..047569f2d0 100644
--- a/tests/integration/shell.py
+++ b/tests/integration/shell.py
@@ -278,7 +278,7 @@ def test_workspace_visible(cli, tmpdir, datafiles):
 
     # Open a workspace on our build failing element
     #
-    res = cli.run(project=project, args=['workspace', 'open', element_name, workspace])
+    res = cli.run(project=project, args=['workspace', 'open', '--directory', workspace, element_name])
     assert res.exit_code == 0
 
     # Ensure the dependencies of our build failing element are built
diff --git a/tests/integration/workspace.py b/tests/integration/workspace.py
index bcbcd674bf..4418510627 100644
--- a/tests/integration/workspace.py
+++ b/tests/integration/workspace.py
@@ -23,7 +23,7 @@ def test_workspace_mount(cli, tmpdir, datafiles):
     workspace = os.path.join(cli.directory, 'workspace')
     element_name = 'workspace/workspace-mount.bst'
 
-    res = cli.run(project=project, args=['workspace', 'open', element_name, workspace])
+    res = cli.run(project=project, args=['workspace', 'open', '--directory', workspace, element_name])
     assert res.exit_code == 0
 
     res = cli.run(project=project, args=['build', element_name])
@@ -39,7 +39,7 @@ def test_workspace_commanddir(cli, tmpdir, datafiles):
     workspace = os.path.join(cli.directory, 'workspace')
     element_name = 'workspace/workspace-commanddir.bst'
 
-    res = cli.run(project=project, args=['workspace', 'open', element_name, workspace])
+    res = cli.run(project=project, args=['workspace', 'open', '--directory', workspace, element_name])
     assert res.exit_code == 0
 
     res = cli.run(project=project, args=['build', element_name])
@@ -75,7 +75,7 @@ def test_workspace_updated_dependency(cli, tmpdir, datafiles):
     _yaml.dump(dependency, os.path.join(element_path, dep_name))
 
     # First open the workspace
-    res = cli.run(project=project, args=['workspace', 'open', element_name, workspace])
+    res = cli.run(project=project, args=['workspace', 'open', '--directory', workspace, element_name])
     assert res.exit_code == 0
 
     # We build the workspaced element, so that we have an artifact
@@ -130,7 +130,7 @@ def test_workspace_update_dependency_failed(cli, tmpdir, datafiles):
     _yaml.dump(dependency, os.path.join(element_path, dep_name))
 
     # First open the workspace
-    res = cli.run(project=project, args=['workspace', 'open', element_name, workspace])
+    res = cli.run(project=project, args=['workspace', 'open', '--directory', workspace, element_name])
     assert res.exit_code == 0
 
     # We build the workspaced element, so that we have an artifact
@@ -205,7 +205,7 @@ def test_updated_dependency_nested(cli, tmpdir, datafiles):
     _yaml.dump(dependency, os.path.join(element_path, dep_name))
 
     # First open the workspace
-    res = cli.run(project=project, args=['workspace', 'open', element_name, workspace])
+    res = cli.run(project=project, args=['workspace', 'open', '--directory', workspace, element_name])
     assert res.exit_code == 0
 
     # We build the workspaced element, so that we have an artifact
@@ -258,7 +258,7 @@ def test_incremental_configure_commands_run_only_once(cli, tmpdir, datafiles):
     _yaml.dump(element, os.path.join(element_path, element_name))
 
     # We open a workspace on the above element
-    res = cli.run(project=project, args=['workspace', 'open', element_name, workspace])
+    res = cli.run(project=project, args=['workspace', 'open', '--directory', workspace, element_name])
     res.assert_success()
 
     # Then we build, and check whether the configure step succeeded
diff --git a/tests/plugins/filter.py b/tests/plugins/filter.py
index 559815a8b3..cec7fa3f92 100644
--- a/tests/plugins/filter.py
+++ b/tests/plugins/filter.py
@@ -108,7 +108,7 @@ def test_filter_forbid_also_rdep(datafiles, cli):
 def test_filter_workspace_open(datafiles, cli, tmpdir):
     project = os.path.join(datafiles.dirname, datafiles.basename)
     workspace_dir = os.path.join(tmpdir.dirname, tmpdir.basename, "workspace")
-    result = cli.run(project=project, args=['workspace', 'open', 'deps-permitted.bst', workspace_dir])
+    result = cli.run(project=project, args=['workspace', 'open', '--directory', workspace_dir, 'deps-permitted.bst'])
     result.assert_success()
     assert os.path.exists(os.path.join(workspace_dir, "foo"))
     assert os.path.exists(os.path.join(workspace_dir, "bar"))
@@ -120,7 +120,7 @@ def test_filter_workspace_build(datafiles, cli, tmpdir):
     project = os.path.join(datafiles.dirname, datafiles.basename)
     tempdir = os.path.join(tmpdir.dirname, tmpdir.basename)
     workspace_dir = os.path.join(tempdir, "workspace")
-    result = cli.run(project=project, args=['workspace', 'open', 'output-orphans.bst', workspace_dir])
+    result = cli.run(project=project, args=['workspace', 'open', '--directory', workspace_dir, 'output-orphans.bst'])
     result.assert_success()
     src = os.path.join(workspace_dir, "foo")
     dst = os.path.join(workspace_dir, "quux")
@@ -138,7 +138,7 @@ def test_filter_workspace_close(datafiles, cli, tmpdir):
     project = os.path.join(datafiles.dirname, datafiles.basename)
     tempdir = os.path.join(tmpdir.dirname, tmpdir.basename)
     workspace_dir = os.path.join(tempdir, "workspace")
-    result = cli.run(project=project, args=['workspace', 'open', 'output-orphans.bst', workspace_dir])
+    result = cli.run(project=project, args=['workspace', 'open', '--directory', workspace_dir, 'output-orphans.bst'])
     result.assert_success()
     src = os.path.join(workspace_dir, "foo")
     dst = os.path.join(workspace_dir, "quux")
@@ -158,7 +158,7 @@ def test_filter_workspace_reset(datafiles, cli, tmpdir):
     project = os.path.join(datafiles.dirname, datafiles.basename)
     tempdir = os.path.join(tmpdir.dirname, tmpdir.basename)
     workspace_dir = os.path.join(tempdir, "workspace")
-    result = cli.run(project=project, args=['workspace', 'open', 'output-orphans.bst', workspace_dir])
+    result = cli.run(project=project, args=['workspace', 'open', '--directory', workspace_dir, 'output-orphans.bst'])
     result.assert_success()
     src = os.path.join(workspace_dir, "foo")
     dst = os.path.join(workspace_dir, "quux")
-- 
GitLab