Repository: reanahub/reana-client

Issue:
cli: `mv` command displaying all the workspace files
Currently, when the user inputs a non-existing file as the target, the `mv` command displays all the workspace files of the given workflow. This is a problem in a realistic workspace, since this list will be very big.
For example:
```
$ reana-client mv -w root6-roofit.4 bad bad
==> ERROR: Source file(s) bad/* does not exist in workspace ['code/gendata.C', 'code/fitdata.C', 'results/plot.png', 'results/data.root']
$ reana-client mv bad bad -w h4l 2>&1 | wc -c
57767
```

As you can see, the latter is huge and too big to include in this issue. Instead, we should follow the way `reana-client-go` handled this and just print the workflow name, like this:
```
$ reana-client mv -w my-roofit-analysis bad bad
==> ERROR: source file(s) bad does not exist in my-roofit-analysis workspace
```

Discussion:

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 9de9304..040a0c2 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -13,6 +13,7 @@ Version 0.9.0 (UNRELEASED)
 - Changes ``list`` command to allow displaying the duration of workflows with the ``--include-duration`` option.
 - Changes ``status`` command to allow displaying the duration of workflows with the ``--include-duration`` option.
 - Changes REANA specification loading and validation functionality by porting some of the logic to ``reana-commons``.
+- Changes ``mv`` command to allow moving files while a workflow is running.
 - Fixes ``validate --environment`` command to detect illegal white space characters in image names.
 - Fixes ``list`` command to highlight the workflow specified in ``REANA_WORKON`` correctly.
 
diff --git a/reana_client/cli/files.py b/reana_client/cli/files.py
index b81a3bb..4a73db5 100644
--- a/reana_client/cli/files.py
+++ b/reana_client/cli/files.py
@@ -105,10 +105,10 @@ def get_files(
     ``--workflow`` or ``-w``. The SOURCE argument is optional and specifies a
     pattern matching files and directories.
 
-    Examples: \n
-    \t $ reana-client ls --workflow myanalysis.42 \n
-    \t $ reana-client ls --workflow myanalysis.42 --human-readable \n
-    \t $ reana-client ls --workflow myanalysis.42 'data/*root*' \n
+    Examples:\n
+    \t $ reana-client ls --workflow myanalysis.42\n
+    \t $ reana-client ls --workflow myanalysis.42 --human-readable\n
+    \t $ reana-client ls --workflow myanalysis.42 'data/*root*'\n
     \t $ reana-client ls --workflow myanalysis.42 --filter name=hello
     """  # noqa: W605
     import tablib
@@ -211,9 +211,9 @@ def download_files(
     are downloaded. You can also specify the individual files you would like
     to download, see examples below.
 
-    Examples: \n
-    \t $ reana-client download # download all output files \n
-    \t $ reana-client download mydata.tmp outputs/myplot.png \n
+    Examples:\n
+    \t $ reana-client download # download all output files\n
+    \t $ reana-client download mydata.tmp outputs/myplot.png\n
     \t $ reana-client download -o - data.txt # write data.txt to stdout
     """
     from reana_client.api.client import download_file, get_workflow_specification
@@ -324,8 +324,8 @@ def upload_files(  # noqa: C901
     behaviour is to upload all input files and directories specified in the
     reana.yaml file.
 
-    Examples: \n
-    \t $ reana-client upload -w myanalysis.42 \n
+    Examples:\n
+    \t $ reana-client upload -w myanalysis.42\n
     \t $ reana-client upload -w myanalysis.42 code/mycode.py
     """
     from reana_client.api.client import get_workflow_specification, upload_to_server
@@ -452,7 +452,7 @@ def delete_files(ctx, workflow, filenames, access_token):  # noqa: D301
     Note that you can use glob to remove similar files.
 
     Examples:\n
-    \t $ reana-client rm -w myanalysis.42 data/mydata.csv \n
+    \t $ reana-client rm -w myanalysis.42 data/mydata.csv\n
     \t $ reana-client rm -w myanalysis.42 'data/*root*'
     """  # noqa: W605
     from reana_client.api.client import delete_file
@@ -508,7 +508,8 @@ def delete_files(ctx, workflow, filenames, access_token):  # noqa: D301
 def move_files(ctx, source, target, workflow, access_token):  # noqa: D301
     """Move files within workspace.
 
-    The ``mv`` command allow to move the files within workspace.
+    The ``mv`` command allows to move files within a workspace. Note that the
+    workflow might fail if files are moved during its execution.
 
     Examples:\n
     \t $ reana-client mv data/input.txt input/input.txt
@@ -519,34 +520,17 @@ def move_files(ctx, source, target, workflow, access_token):  # noqa: D301
     for p in ctx.params:
         logging.debug("{param}: {value}".format(param=p, value=ctx.params[p]))
 
-    if workflow:
-        try:
-            current_status = get_workflow_status(workflow, access_token).get("status")
-            if current_status == "running":
-                display_message(
-                    "File(s) could not be moved for running workflow",
-                    msg_type="error",
-                )
-                sys.exit(1)
-            files = list_files(workflow, access_token)
-            current_files = [file["name"] for file in files]
-            if not any(source in item for item in current_files):
-                display_message(
-                    "Source file(s) {} does not exist in "
-                    "workspace {}".format(source, current_files),
-                    msg_type="error",
-                )
-                sys.exit(1)
-            mv_files(source, target, workflow, access_token)
-            display_message(
-                "{} was successfully moved to {}.".format(source, target),
-                msg_type="success",
-            )
-        except Exception as e:
-            logging.debug(traceback.format_exc())
-            logging.debug(str(e))
-            display_message("Something went wrong. {}".format(e), msg_type="error")
-            sys.exit(1)
+    try:
+        mv_files(source, target, workflow, access_token)
+        display_message(
+            "{} was successfully moved to {}.".format(source, target),
+            msg_type="success",
+        )
+    except Exception as e:
+        logging.debug(traceback.format_exc())
+        logging.debug(str(e))
+        display_message("Something went wrong. {}".format(e), msg_type="error")
+        sys.exit(1)
 
 
 @files_group.command("du")
@@ -572,9 +556,9 @@ def workflow_disk_usage(
 
     The ``du`` command allows to chech the disk usage of given workspace.
 
-    Examples: \n
-    \t $ reana-client du -w myanalysis.42 -s \n
-    \t $ reana-client du -w myanalysis.42 -s --human-readable \n
+    Examples:\n
+    \t $ reana-client du -w myanalysis.42 -s\n
+    \t $ reana-client du -w myanalysis.42 -s --human-readable\n
     \t $ reana-client du -w myanalysis.42 --filter name=data/
     """
     from reana_client.api.client import get_workflow_disk_usage
diff --git a/reana_client/cli/ping.py b/reana_client/cli/ping.py
index 9a179ed..a2fbbb9 100644
--- a/reana_client/cli/ping.py
+++ b/reana_client/cli/ping.py
@@ -34,7 +34,7 @@ def ping(ctx, access_token):  # noqa: D301
 
     The ``ping`` command allows to test connection to REANA server.
 
-    Examples: \n
+    Examples:\n
     \t $ reana-client ping
     """
     try:
@@ -81,7 +81,7 @@ def version(ctx):  # noqa: D301
 
     The ``version`` command shows REANA client version.
 
-    Examples: \n
+    Examples:\n
     \t $ reana-client version
     """
     display_message(__version__)
@@ -106,7 +106,7 @@ def info(ctx, access_token: str, output_format: str):  # noqa: D301
     Lists all the available workspaces. It also returns the default workspace
     defined by the admin.
 
-    Examples: \n
+    Examples:\n
     \t $ reana-client info
     """
     try:
diff --git a/reana_client/cli/quotas.py b/reana_client/cli/quotas.py
index 28b7d84..7e9d120 100644
--- a/reana_client/cli/quotas.py
+++ b/reana_client/cli/quotas.py
@@ -63,7 +63,7 @@ def quota_show(
 
     The ``quota-show`` command displays quota usage for the user.
 
-    Examples: \n
+    Examples:\n
     \t $ reana-client quota-show --resource disk --report limit\n
     \t $ reana-client quota-show --resource disk --report usage\n
     \t $ reana-client quota-show --resource disk\n
diff --git a/reana_client/cli/secrets.py b/reana_client/cli/secrets.py
index 315d44c..79defa2 100644
--- a/reana_client/cli/secrets.py
+++ b/reana_client/cli/secrets.py
@@ -57,11 +57,11 @@ def secrets_group(ctx):
 def secrets_add(env, file, overwrite, access_token):  # noqa: D301
     """Add secrets from literal string or from file.
 
-    Examples: \n
-    \t $ reana-client secrets-add --env PASSWORD=password \n
-    \t $ reana-client secrets-add --file ~/.keytab \n
-    \t $ reana-client secrets-add --env USER=reanauser \n
-    \t                            --env PASSWORD=password \n
+    Examples:\n
+    \t $ reana-client secrets-add --env PASSWORD=password\n
+    \t $ reana-client secrets-add --file ~/.keytab\n
+    \t $ reana-client secrets-add --env USER=reanauser\n
+    \t                            --env PASSWORD=password\n
     \t                            --file ~/.keytab
     """
     from reana_client.api.client import add_secrets
@@ -104,7 +104,7 @@ def secrets_add(env, file, overwrite, access_token):  # noqa: D301
 def secrets_delete(secrets, access_token):  # noqa: D301
     """Delete user secrets by name.
 
-     Examples: \n
+     Examples:\n
     \t $ reana-client secrets-delete PASSWORD
     """
     from reana_client.api.client import delete_secrets
@@ -140,7 +140,7 @@ def secrets_delete(secrets, access_token):  # noqa: D301
 def secrets_list(access_token):  # noqa: D301
     """List user secrets.
 
-    Examples: \n
+    Examples:\n
     \t $ reana-client secrets-list
     """
     from reana_client.api.client import list_secrets
diff --git a/reana_client/cli/workflow.py b/reana_client/cli/workflow.py
index 290b164..d1327b7 100644
--- a/reana_client/cli/workflow.py
+++ b/reana_client/cli/workflow.py
@@ -182,9 +182,9 @@ def workflows_list(  # noqa: C901
     interactive sessions, you need to pass the ``--sessions`` command-line
     option.
 
-    Example: \n
-    \t $ reana-client list --all \n
-    \t $ reana-client list --sessions \n
+    Example:\n
+    \t $ reana-client list --all\n
+    \t $ reana-client list --sessions\n
     \t $ reana-client list --verbose --bytes
     """
     import tablib
@@ -359,7 +359,7 @@ def workflow_create(ctx, file, name, skip_validation, access_token):  # noqa: D3
     working directory, or supplied via command-line -f option, see examples
     below.
 
-    Examples: \n
+    Examples:\n
     \t $ reana-client create\n
     \t $ reana-client create -w myanalysis\n
     \t $ reana-client create -w myanalysis -f myreana.yaml\n
@@ -444,8 +444,8 @@ def workflow_start(
     options can be repetitive. For example, to disable caching for the Serial
     workflow engine, you can set ``-o CACHE=off``.
 
-    Examples: \n
-    \t $ reana-client start -w myanalysis.42 -p sleeptime=10 -p myparam=4 \n
+    Examples:\n
+    \t $ reana-client start -w myanalysis.42 -p sleeptime=10 -p myparam=4\n
     \t $ reana-client start -w myanalysis.42 -p myparam1=myvalue1 -o CACHE=off
     """
     from reana_client.utils import get_api_url
@@ -576,8 +576,8 @@ def workflow_restart(
     ``-o`` or ``--options``.  The input parameters and operational options can be
     repetitive.
 
-    Examples: \n
-    \t $ reana-client restart -w myanalysis.42 -p sleeptime=10 -p myparam=4 \n
+    Examples:\n
+    \t $ reana-client restart -w myanalysis.42 -p sleeptime=10 -p myparam=4\n
     \t $ reana-client restart -w myanalysis.42 -p myparam=myvalue\n
     \t $ reana-client restart -w myanalysis.42 -o TARGET=gendata\n
     \t $ reana-client restart -w myanalysis.42 -o FROM=fitdata
@@ -692,8 +692,8 @@ def workflow_status(  # noqa: C901
     be created, queued, running, failed, etc. You can increase verbosity or
     filter retrieved information by passing appropriate command-line options.
 
-    Examples: \n
-    \t $ reana-client status -w myanalysis.42 \n
+    Examples:\n
+    \t $ reana-client status -w myanalysis.42\n
     \t $ reana-client status -w myanalysis.42 -v --json
     """
     import tablib
@@ -839,7 +839,7 @@ def workflow_logs(
     only finished steps of the workflow are returned, the logs of the currently
     processed step is not returned until it is finished.
 
-    Examples: \n
+    Examples:\n
     \t $ reana-client logs -w myanalysis.42
     \t $ reana-client logs -w myanalysis.42 -s 1st_step
     """
@@ -980,7 +980,7 @@ def workflow_validate(
     The ``validate`` command allows to check syntax and validate the reana.yaml
     workflow specification file.
 
-    Examples: \n
+    Examples:\n
     \t $ reana-client validate -f reana.yaml
     """
     if server_capabilities:
@@ -1041,7 +1041,7 @@ def workflow_stop(ctx, workflow, force_stop, access_token):  # noqa: D301
     should be therefore used with care, only if you are absolutely sure that
     there is no point in continuing the running the workflow.
 
-    Example: \n
+    Example:\n
     \t $ reana-client stop -w myanalysis.42 --force
     """
     from reana_client.api.client import stop_workflow
@@ -1134,8 +1134,8 @@ def workflow_run(
     The ``run`` command allows to create a new workflow, upload its input files
     and start it in one command.
 
-    Examples: \n
-    \t $ reana-client run -w myanalysis-test-small -p myparam=mysmallvalue \n
+    Examples:\n
+    \t $ reana-client run -w myanalysis-test-small -p myparam=mysmallvalue\n
     \t $ reana-client run -w myanalysis-test-big -p myparam=mybigvalue
     """
     # set context parameters for subcommand
@@ -1193,8 +1193,8 @@ def workflow_delete(
     Note that the workspace will always be deleted, even when ``--include-workspace`` is not specified.
     Note also that you can remove all past runs of a workflow by specifying ``--include-all-runs`` flag.
 
-    Example: \n
-    \t $ reana-client delete -w myanalysis.42 \n
+    Example:\n
+    \t $ reana-client delete -w myanalysis.42\n
     \t $ reana-client delete -w myanalysis.42 --include-all-runs
     """
     from reana_client.api.client import delete_workflow
@@ -1263,8 +1263,8 @@ def workflow_diff(
     workflow_b, which must be provided as arguments. The output will show the
     difference in workflow run parameters, the generated files, the logs, etc.
 
-    Examples: \n
-    \t $ reana-client diff myanalysis.42 myotheranalysis.43 \n
+    Examples:\n
+    \t $ reana-client diff myanalysis.42 myotheranalysis.43\n
     \t $ reana-client diff myanalysis.42 myotheranalysis.43 --brief
     """
     from reana_client.api.client import diff_workflows


Test patch:
diff --git a/tests/test_cli_files.py b/tests/test_cli_files.py
index e898ab2..0579325 100644
--- a/tests/test_cli_files.py
+++ b/tests/test_cli_files.py
@@ -17,7 +17,6 @@ import zipfile
 from click.testing import CliRunner
 from mock import Mock, patch
 from pytest_reana.test_utils import make_mock_api_client
-
 from reana_client.cli import cli
 
 
@@ -427,31 +426,36 @@ def test_delete_non_existing_file():
                 assert message in result.output
 
 
-def test_move_file_running_workflow():
-    """Test move files when workflow is running."""
-    status_code = 200
+def test_move_files():
+    """Test move files."""
     reana_token = "000000"
-    src_file = "file11"
+    workflow = "mytest.1"
+    source = "file1"
     target = "file2"
-    response = {"status": "running", "logs": "", "name": "mytest.1"}
-    message = "File(s) could not be moved for running workflow"
+
     mock_http_response = Mock()
-    mock_http_response.status_code = status_code
-    mock_http_response.raw_bytes = str(response).encode()
-    mock_response = response
-    env = {"REANA_SERVER_URL": "localhost"}
-    runner = CliRunner(env=env)
+    mock_http_response.status_code = 200
+    mock_http_response.raw_bytes = "{}".encode()
+    mock_client = Mock()
+    mock_result = mock_client.api.move_files.return_value
+    mock_result.result.return_value = ({}, mock_http_response)
+
+    runner = CliRunner(env={"REANA_SERVER_URL": "localhost"})
     with runner.isolation():
-        with patch(
-            "reana_client.api.client.current_rs_api_client",
-            make_mock_api_client("reana-server")(mock_response, mock_http_response),
-        ):
+        with patch("reana_client.api.client.current_rs_api_client", mock_client):
             result = runner.invoke(
                 cli,
-                ["mv", "-t", reana_token, "--workflow", "mytest.1", src_file, target],
+                ["mv", "-t", reana_token, "--workflow", workflow, source, target],
             )
-            assert result.exit_code == 1
-            assert message in result.output
+
+            mock_client.api.move_files.assert_called_once_with(
+                source=source,
+                target=target,
+                workflow_id_or_name=workflow,
+                access_token=reana_token,
+            )
+            assert result.exit_code == 0
+            assert "successfully" in result.output
 
 
 def test_list_files_filter():