Repository: reanahub/reana-client (2022-02-01 ~ 2022-05-24)

problem_statement: add `reana-client cat`
sometimes it's convenient to be able to just dump the contents of a file to stdout (e.g. a JSON file) for this a `reana-client cat`  would be nice to have
date: 2022-02-01
patch: diff --git a/CHANGES.rst b/CHANGES.rst
index ba99c03..87d5e15 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,6 +1,11 @@
 Changes
 =======
 
+Version 0.8.2 (UNRELEASED)
+--------------------------
+
+- Changes ``download`` command to add the possibility to write files to the standard output via ``-o -`` option.
+
 Version 0.8.1 (2022-02-15)
 --------------------------
 
diff --git a/reana_client/api/client.py b/reana_client/api/client.py
index f03ddcc..f88e75d 100644
--- a/reana_client/api/client.py
+++ b/reana_client/api/client.py
@@ -402,7 +402,8 @@ def download_file(workflow, file_name, access_token):
     :param workflow: name or id which identifies the workflow.
     :param file_name: file name or path to the file requested.
     :param access_token: access token of the current user.
-    :return: Tuple containing file binary content and filename.
+    :return: Tuple containing file binary content, filename and whether
+        the returned file is a zip archive containing multiple files.
     """
     try:
         from reana_client.utils import get_api_url
@@ -421,8 +422,13 @@ def download_file(workflow, file_name, access_token):
             value, params = cgi.parse_header(content_disposition)
             file_name = params.get("filename", "downloaded_file")
 
+        # A zip archive is downloaded if multiple files are requested
+        multiple_files_zipped = (
+            http_response.headers.get("Content-Type") == "application/zip"
+        )
+
         if http_response.status_code == 200:
-            return http_response.content, file_name
+            return http_response.content, file_name, multiple_files_zipped
         else:
             raise Exception(
                 "Error {status_code} {reason} {message}".format(
diff --git a/reana_client/cli/files.py b/reana_client/cli/files.py
index 31b72f7..244bfd6 100644
--- a/reana_client/cli/files.py
+++ b/reana_client/cli/files.py
@@ -7,11 +7,13 @@
 # under the terms of the MIT License; see LICENSE file for more details.
 """REANA client output related commands."""
 
+import io
 import json
 import logging
 import os
 import sys
 import traceback
+import zipfile
 
 import click
 from reana_commons.utils import click_table_printer
@@ -28,7 +30,7 @@ from reana_client.cli.utils import (
     parse_filter_parameters,
     parse_format_parameters,
 )
-from reana_client.config import JSON, URL
+from reana_client.config import JSON, STD_OUTPUT_CHAR, URL
 from reana_client.errors import FileDeletionError, FileUploadError
 
 FILES_BLACKLIST = (".git/", "/.git/")
@@ -191,7 +193,8 @@ def get_files(
     "-o",
     "--output-directory",
     default=os.getcwd(),
-    help="Path to the directory where files will be downloaded.",
+    help="Path to the directory where files will be downloaded. "
+    "If ``-`` is specified as path, the files will be written to the standard output.",
 )
 @add_access_token_options
 @click.pass_context
@@ -207,10 +210,26 @@ def download_files(
 
     Examples: \n
     \t $ reana-client download # download all output files \n
-    \t $ reana-client download mydata.tmp outputs/myplot.png
+    \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
 
+    def display_files_content(binary_file: bytes, multiple_files_zipped: bool) -> None:
+        """Write file(s) content to the standard output.
+
+        :param binary_file: The file(s) to print to the standard output.
+        :param multiple_files_zipped: Flag to determine if ``binary_file`` is a single
+            file or a zip archive containing multiple files.
+        """
+        if multiple_files_zipped:
+            with zipfile.ZipFile(io.BytesIO(binary_file)) as zip_file:
+                for entry in zip_file.infolist():
+                    if not entry.is_dir():
+                        click.echo(zip_file.read(entry), nl=False)
+        else:
+            click.echo(binary_file, nl=False)
+
     logging.debug("command: {}".format(ctx.command_path.replace(" ", ".")))
     for p in ctx.params:
         logging.debug("{param}: {value}".format(param=p, value=ctx.params[p]))
@@ -237,7 +256,7 @@ def download_files(
         download_failed = False
         for file_name in filenames:
             try:
-                binary_file, file_name = download_file(
+                binary_file, file_name, multiple_files_zipped = download_file(
                     workflow, file_name, access_token
                 )
 
@@ -247,16 +266,19 @@ def download_files(
                     )
                 )
 
-                outputs_file_path = os.path.join(output_directory, file_name)
-                if not os.path.exists(os.path.dirname(outputs_file_path)):
-                    os.makedirs(os.path.dirname(outputs_file_path))
+                if output_directory == STD_OUTPUT_CHAR:
+                    display_files_content(binary_file, multiple_files_zipped)
+                else:
+                    outputs_file_path = os.path.join(output_directory, file_name)
+                    if not os.path.exists(os.path.dirname(outputs_file_path)):
+                        os.makedirs(os.path.dirname(outputs_file_path))
 
-                with open(outputs_file_path, "wb") as f:
-                    f.write(binary_file)
-                display_message(
-                    "File {0} downloaded to {1}.".format(file_name, output_directory),
-                    msg_type="success",
-                )
+                    with open(outputs_file_path, "wb") as f:
+                        f.write(binary_file)
+                    display_message(
+                        f"File {file_name} downloaded to {output_directory}.",
+                        msg_type="success",
+                    )
             except OSError as e:
                 logging.debug(traceback.format_exc())
                 logging.debug(str(e))
diff --git a/reana_client/config.py b/reana_client/config.py
index a650aff..46e5eb1 100644
--- a/reana_client/config.py
+++ b/reana_client/config.py
@@ -82,3 +82,6 @@ JOB_STATUS_TO_MSG_COLOR = {
     "finished": "green",
     "running": "bright_blue",
 }
+
+STD_OUTPUT_CHAR = "-"
+"""Character used to refer to the standard output."""

test_patch: diff --git a/tests/test_cli_files.py b/tests/test_cli_files.py
index 6e2a3b9..99093e3 100644
--- a/tests/test_cli_files.py
+++ b/tests/test_cli_files.py
@@ -9,8 +9,10 @@
 """REANA client files tests."""
 
 import hashlib
+import io
 import json
 import os
+import zipfile
 
 from click.testing import CliRunner
 from mock import Mock, patch
@@ -114,7 +116,8 @@ def test_download_file():
     mock_http_response.status_code = status_code
     mock_http_response.content = str(response).encode()
     mock_http_response.headers = {
-        "Content-Disposition": "attachment; filename={}".format(file)
+        "Content-Disposition": "attachment; filename={}".format(file),
+        "Content-Type": "multipart/form-data",
     }
     mock_requests = Mock()
     mock_requests.get = Mock(return_value=mock_http_response)
@@ -136,6 +139,86 @@ def test_download_file():
             os.remove(file)
 
 
+def test_download_file_stdout():
+    """Test writing a single file to stdout."""
+    env = {"REANA_SERVER_URL": "localhost"}
+    filename = "dummy_file.txt"
+    file_content = "Content of file to download"
+    mock_http_response = Mock()
+    mock_http_response.status_code = 200
+    mock_http_response.content = file_content.encode()
+    mock_http_response.headers = {
+        "Content-Disposition": "attachment; filename={}".format(filename),
+        "Content-Type": "multipart/form-data",
+    }
+    mock_requests = Mock()
+    mock_requests.get = Mock(return_value=mock_http_response)
+
+    reana_token = "000000"
+    runner = CliRunner(env=env)
+    with runner.isolation():
+        with patch("reana_client.api.client.requests", mock_requests):
+            result = runner.invoke(
+                cli,
+                [
+                    "download",
+                    "-t",
+                    reana_token,
+                    "--workflow",
+                    "mytest.1",
+                    filename,
+                    "-o",
+                    "-",
+                ],
+            )
+            assert result.exit_code == 0
+            assert result.output == file_content
+
+
+def test_download_multiple_files_stdout():
+    """Test writing multiple files to stdout."""
+    env = {"REANA_SERVER_URL": "localhost"}
+    files = [
+        ("dir1/dummy1.txt", "Content of dummy1.txt"),
+        ("dir1/dummy2.txt", "Content of dummy2.txt"),
+    ]
+    # Create zip file
+    return_file = io.BytesIO()
+    with zipfile.ZipFile(return_file, "w", zipfile.ZIP_DEFLATED) as zip_file:
+        for filename, content in files:
+            zip_file.writestr(filename, content)
+
+    mock_http_response = Mock()
+    mock_http_response.status_code = 200
+    mock_http_response.content = return_file.getvalue()
+    mock_http_response.headers = {
+        "Content-Disposition": "attachment; filename=files.zip",
+        "Content-Type": "application/zip",
+    }
+    mock_requests = Mock()
+    mock_requests.get = Mock(return_value=mock_http_response)
+
+    reana_token = "000000"
+    runner = CliRunner(env=env)
+    with runner.isolation():
+        with patch("reana_client.api.client.requests", mock_requests):
+            result = runner.invoke(
+                cli,
+                [
+                    "download",
+                    "-t",
+                    reana_token,
+                    "--workflow",
+                    "mytest.1",
+                    "-o",
+                    "-",
+                    "dir1",
+                ],
+            )
+            assert result.exit_code == 0
+            assert result.output == "".join([content for _, content in files])
+
+
 def test_upload_file(create_yaml_workflow_schema):
     """Test upload file."""
     reana_token = "000000"


----

problem_statement: Improve log message when gitignore is detected
When `.gitignore` is detected, currently user sees log message below:

```[2022-05-17T14:32:25] reana-demo-root6-roofit: reana-client upload -w root6-roofit-yadage-kubernetes
==> SUCCESS: Detected .gitignore file. Some files might get ignored.
==> SUCCESS: File /code/gendata.C was successfully uploaded.
```

It feels a bit weird to see "SUCCESS" for gitignore detection to be on the same level as "SUCCESS" when uploading `gendata.C`. It might be better to say "INFO" for messages like Detected gitignore, reanaignore, etc...

https://github.com/reanahub/reana-client/blob/7655cb032e9d42f7775525d2f1c53382d865e92a/reana_client/cli/files.py#L341-L344
date: 2022-05-04
patch: diff --git a/CHANGES.rst b/CHANGES.rst
index 87d5e15..75867b4 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,10 +1,18 @@
 Changes
 =======
 
+Version 0.9.0 (UNRELEASED)
+--------------------------
+
+- Adds support for ``.gitignore`` during file upload. Files that match ``.gitignore`` will not be uploaded.
+- Changes REANA specification loading and validation functionality by porting some of the logic to ``reana-commons``.
+- Fixes ``validate --environment`` command to detect illegal white space characters in image names.
+
 Version 0.8.2 (UNRELEASED)
 --------------------------
 
 - Changes ``download`` command to add the possibility to write files to the standard output via ``-o -`` option.
+- Fixes ``start`` and ``run`` commands to correctly follow the execution of the workflow until termination.
 
 Version 0.8.1 (2022-02-15)
 --------------------------
diff --git a/reana_client/cli/files.py b/reana_client/cli/files.py
index 244bfd6..87b14db 100644
--- a/reana_client/cli/files.py
+++ b/reana_client/cli/files.py
@@ -14,8 +14,11 @@ import os
 import sys
 import traceback
 import zipfile
+from typing import List, Tuple
 
 import click
+import pathspec
+
 from reana_commons.utils import click_table_printer
 
 from reana_client.printer import display_message
@@ -31,7 +34,7 @@ from reana_client.cli.utils import (
     parse_format_parameters,
 )
 from reana_client.config import JSON, STD_OUTPUT_CHAR, URL
-from reana_client.errors import FileDeletionError, FileUploadError
+from reana_client.errors import FileDeletionError
 
 FILES_BLACKLIST = (".git/", "/.git/")
 
@@ -310,7 +313,9 @@ def download_files(
 @check_connection
 @add_access_token_options
 @click.pass_context
-def upload_files(ctx, workflow, filenames, access_token):  # noqa: D301
+def upload_files(  # noqa: C901
+    ctx, workflow: str, filenames: Tuple[str], access_token: str
+):  # noqa: D301
     """Upload files and directories to workspace.
 
     The ``upload`` command allows to upload workflow input files and
@@ -328,6 +333,28 @@ def upload_files(ctx, workflow, filenames, access_token):  # noqa: D301
     logging.debug("command: {}".format(ctx.command_path.replace(" ", ".")))
     for p in ctx.params:
         logging.debug("{param}: {value}".format(param=p, value=ctx.params[p]))
+
+    def _filter_files_by_gitignore(directories: List[str]) -> List[str]:
+        try:
+            with open(".gitignore") as f:
+                gitignore_spec = pathspec.PathSpec.from_lines("gitwildmatch", f)
+            display_message(
+                "Detected .gitignore file. Some files might get ignored.",
+                msg_type="info",
+            )
+        except FileNotFoundError:
+            return directories
+        else:
+            filtered_filenames = []
+            for directory_path in directories:
+                for root, dirs, files in os.walk(directory_path):
+                    if not gitignore_spec.match_file(root):
+                        for file_name in files:
+                            filename_full_path = os.path.join(root, file_name)
+                            if not gitignore_spec.match_file(filename_full_path):
+                                filtered_filenames.append(filename_full_path)
+            return filtered_filenames
+
     if not filenames:
         try:
             reana_spec = get_workflow_specification(workflow, access_token)[
@@ -343,53 +370,54 @@ def upload_files(ctx, workflow, filenames, access_token):  # noqa: D301
 
         if "inputs" in reana_spec:
             filenames = []
+
             filenames += [
                 os.path.join(os.getcwd(), f)
                 for f in reana_spec["inputs"].get("files") or []
             ]
-            filenames += [
+
+            directories_full_path = [
                 os.path.join(os.getcwd(), d)
                 for d in reana_spec["inputs"].get("directories") or []
             ]
+            filenames += _filter_files_by_gitignore(directories_full_path)
 
-    if workflow:
-        if filenames:
-            upload_failed = False
-            for filename in filenames:
-                try:
-                    response = upload_to_server(workflow, filename, access_token)
-                    for file_ in response:
-                        if file_.startswith("symlink:"):
-                            display_message(
-                                "Symlink resolved to {}. "
-                                "Uploaded hard copy.".format(file_[len("symlink:") :]),
-                                msg_type="success",
-                            )
-                        else:
-                            display_message(
-                                "File {} was successfully uploaded.".format(file_),
-                                msg_type="success",
-                            )
-                except FileNotFoundError as e:
-                    logging.debug(traceback.format_exc())
-                    logging.debug(str(e))
+    upload_failed = False
+    for filename in filenames:
+        try:
+            response = upload_to_server(workflow, filename, access_token)
+            for file_ in response:
+                if file_.startswith("symlink:"):
                     display_message(
-                        "File {0} could not be uploaded: "
-                        "{0} does not exist.".format(filename),
-                        msg_type="error",
+                        "Symlink resolved to {}. "
+                        "Uploaded hard copy.".format(file_[len("symlink:") :]),
+                        msg_type="success",
                     )
-                    upload_failed = True
-                except Exception as e:
-                    logging.debug(traceback.format_exc())
-                    logging.debug(str(e))
+                else:
                     display_message(
-                        "Something went wrong while uploading {}: \n"
-                        "{}".format(filename, str(e)),
-                        msg_type="error",
+                        "File {} was successfully uploaded.".format(file_),
+                        msg_type="success",
                     )
-                    upload_failed = True
-            if upload_failed:
-                sys.exit(1)
+        except FileNotFoundError as e:
+            logging.debug(traceback.format_exc())
+            logging.debug(str(e))
+            display_message(
+                "File {0} could not be uploaded: "
+                "{0} does not exist.".format(filename),
+                msg_type="error",
+            )
+            upload_failed = True
+        except Exception as e:
+            logging.debug(traceback.format_exc())
+            logging.debug(str(e))
+            display_message(
+                "Something went wrong while uploading {}: \n"
+                "{}".format(filename, str(e)),
+                msg_type="error",
+            )
+            upload_failed = True
+    if upload_failed:
+        sys.exit(1)
 
 
 @files_group.command("rm")
@@ -477,7 +505,8 @@ def move_files(ctx, source, target, workflow, access_token):  # noqa: D301
             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",
+                    "File(s) could not be moved for running workflow",
+                    msg_type="error",
                 )
                 sys.exit(1)
             files = list_files(workflow, access_token)
@@ -560,6 +589,7 @@ def workflow_disk_usage(
             logging.debug(traceback.format_exc())
             logging.debug(str(e))
             display_message(
-                "Disk usage could not be retrieved: \n{}".format(e), msg_type="error",
+                "Disk usage could not be retrieved: \n{}".format(e),
+                msg_type="error",
             )
             sys.exit(1)

test_patch: diff --git a/tests/conftest.py b/tests/conftest.py
index bc62992..369beb3 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -11,6 +11,7 @@
 from __future__ import absolute_import, print_function
 
 import pytest
+from typing import Dict
 
 
 @pytest.fixture()
@@ -53,6 +54,26 @@ def create_yaml_workflow_schema_with_workspace(create_yaml_workflow_schema: str)
     return reana_yaml_schema
 
 
+@pytest.fixture()
+def get_workflow_specification_with_directory() -> Dict:
+    """Return dummy workflow specification with "data" directory listed in inputs."""
+    reana_yaml_schema = {
+        "inputs": {
+            "directories": ["data"],
+        },
+        "version": "0.3.0",
+        "workflow": {
+            "specification": {
+                "steps": [
+                    {"commands": ["echo hello"], "environment": "python:2.7-slim"}
+                ]
+            },
+            "type": "serial",
+        },
+    }
+    return {"specification": reana_yaml_schema}
+
+
 @pytest.fixture()
 def create_cwl_yaml_workflow_schema():
     """Return dummy CWL workflow schema."""
diff --git a/tests/test_cli_files.py b/tests/test_cli_files.py
index 99093e3..1eb85ca 100644
--- a/tests/test_cli_files.py
+++ b/tests/test_cli_files.py
@@ -241,6 +241,45 @@ def test_upload_file(create_yaml_workflow_schema):
                 assert message in result.output
 
 
+def test_upload_file_respect_gitignore(get_workflow_specification_with_directory):
+    """Do not upload file from the directory in input.directories if it matches .gitignore"""
+    reana_token = "000000"
+    env = {"REANA_SERVER_URL": "http://localhost"}
+    runner = CliRunner(env=env)
+    mock_specification = Mock(return_value=get_workflow_specification_with_directory)
+    with runner.isolation():
+        with patch(
+            "reana_client.api.client.get_workflow_specification", mock_specification
+        ), patch("reana_client.api.client.requests.post") as post_request:
+            with runner.isolated_filesystem():
+                with open(".gitignore", "w") as f:
+                    f.write("do_not_upload.txt\n")
+                    f.write("test\n")
+
+                os.mkdir("data")
+                with open("data/do_not_upload.txt", "w") as f:
+                    f.write("This file should not be uploaded.")
+
+                with open("data/test", "w") as f:
+                    f.write("This file should not be uploaded.")
+
+                with open("data/will_upload.txt", "w") as f:
+                    f.write("This file will be uploaded.")
+
+                result = runner.invoke(
+                    cli, ["upload", "-t", reana_token, "--workflow", "mytest.1"]
+                )
+
+                assert (
+                    "==> Detected .gitignore file. Some files might get ignored."
+                    in result.output
+                )
+                assert "test" not in result.output
+                post_request.assert_called_once()
+                assert "will_upload.txt" in result.output
+                assert result.exit_code == 0
+
+
 def test_delete_file():
     """Test delete file."""
     status_code = 200


----

problem_statement: upload: upload full directory when files are not specified
Due to moving the workflow load logic from reana-client to the workflow-engine, we need all the workflow files in the workspace to be able to load the specification. 

**Current behavior**

When running `reana-client upload -w foo` we only upload the files specified in `inputs.files` and we have to manually upload what's in `workflow.file` and its dependencies.

**Expected behavior**

When running `reana-client upload -w foo`, upload all the files in the current `reana.yaml` directory.

**Extra considerations**
- Remove validation on `reana-client create`?
- Compare remote and local workspace before `reana-client start` and warn user if there're differences?
- Introduce the concept of `.reanaignore`. First check `.gitignore`, after `.reanaignore` and prevent uploading the present files.
date: 2022-05-24
patch: diff --git a/CHANGES.rst b/CHANGES.rst
index 75867b4..6c970db 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -4,6 +4,7 @@ Changes
 Version 0.9.0 (UNRELEASED)
 --------------------------
 
+- Adds support for ``.reanaignore`` during file upload. Files that match ``.reanaignore`` will not be uploaded.
 - Adds support for ``.gitignore`` during file upload. Files that match ``.gitignore`` will not be uploaded.
 - Changes REANA specification loading and validation functionality by porting some of the logic to ``reana-commons``.
 - Fixes ``validate --environment`` command to detect illegal white space characters in image names.
diff --git a/reana_client/cli/files.py b/reana_client/cli/files.py
index 87b14db..b81a3bb 100644
--- a/reana_client/cli/files.py
+++ b/reana_client/cli/files.py
@@ -334,27 +334,6 @@ def upload_files(  # noqa: C901
     for p in ctx.params:
         logging.debug("{param}: {value}".format(param=p, value=ctx.params[p]))
 
-    def _filter_files_by_gitignore(directories: List[str]) -> List[str]:
-        try:
-            with open(".gitignore") as f:
-                gitignore_spec = pathspec.PathSpec.from_lines("gitwildmatch", f)
-            display_message(
-                "Detected .gitignore file. Some files might get ignored.",
-                msg_type="info",
-            )
-        except FileNotFoundError:
-            return directories
-        else:
-            filtered_filenames = []
-            for directory_path in directories:
-                for root, dirs, files in os.walk(directory_path):
-                    if not gitignore_spec.match_file(root):
-                        for file_name in files:
-                            filename_full_path = os.path.join(root, file_name)
-                            if not gitignore_spec.match_file(filename_full_path):
-                                filtered_filenames.append(filename_full_path)
-            return filtered_filenames
-
     if not filenames:
         try:
             reana_spec = get_workflow_specification(workflow, access_token)[
@@ -376,12 +355,52 @@ def upload_files(  # noqa: C901
                 for f in reana_spec["inputs"].get("files") or []
             ]
 
-            directories_full_path = [
-                os.path.join(os.getcwd(), d)
-                for d in reana_spec["inputs"].get("directories") or []
-            ]
-            filenames += _filter_files_by_gitignore(directories_full_path)
+            # collect all files in input.directories
+            files_from_directories = []
+            directories = reana_spec["inputs"].get("directories") or []
+            for directory_path in directories:
+                for root, _, dir_filenames in os.walk(directory_path):
+                    filenames_full_path = [
+                        os.path.join(root, file) for file in dir_filenames
+                    ]
+                    files_from_directories.extend(filenames_full_path)
+
+            def _filter_files(
+                files: List[str], paths_spec: pathspec.PathSpec
+            ) -> List[str]:
+                return [file for file in files if not paths_spec.match_file(file)]
 
+            try:
+                with open(".gitignore") as f:
+                    gitignore_spec = pathspec.PathSpec.from_lines("gitwildmatch", f)
+                display_message(
+                    "Detected .gitignore file. Some files might get ignored.",
+                    msg_type="info",
+                )
+            except FileNotFoundError:
+                pass
+            else:
+                files_from_directories = _filter_files(
+                    files_from_directories, gitignore_spec
+                )
+
+            try:
+                with open(".reanaignore") as f:
+                    reanaignore_spec = pathspec.PathSpec.from_lines("gitwildmatch", f)
+                display_message(
+                    "Detected .reanaignore file. Some files might get ignored.",
+                    msg_type="info",
+                )
+            except FileNotFoundError:
+                pass
+            else:
+                files_from_directories = _filter_files(
+                    files_from_directories, reanaignore_spec
+                )
+
+            filenames += [
+                os.path.join(os.getcwd(), file) for file in files_from_directories
+            ]
     upload_failed = False
     for filename in filenames:
         try:

test_patch: diff --git a/tests/test_cli_files.py b/tests/test_cli_files.py
index 1eb85ca..e898ab2 100644
--- a/tests/test_cli_files.py
+++ b/tests/test_cli_files.py
@@ -241,8 +241,10 @@ def test_upload_file(create_yaml_workflow_schema):
                 assert message in result.output
 
 
-def test_upload_file_respect_gitignore(get_workflow_specification_with_directory):
-    """Do not upload file from the directory in input.directories if it matches .gitignore"""
+def test_upload_file_respect_gitignore(
+    get_workflow_specification_with_directory,
+):
+    """If .gitignore exists and is not empty, respect it's rules."""
     reana_token = "000000"
     env = {"REANA_SERVER_URL": "http://localhost"}
     runner = CliRunner(env=env)
@@ -253,17 +255,98 @@ def test_upload_file_respect_gitignore(get_workflow_specification_with_directory
         ), patch("reana_client.api.client.requests.post") as post_request:
             with runner.isolated_filesystem():
                 with open(".gitignore", "w") as f:
-                    f.write("do_not_upload.txt\n")
-                    f.write("test\n")
+                    f.write("data/should_not_upload.txt\n")
 
                 os.mkdir("data")
-                with open("data/do_not_upload.txt", "w") as f:
+                with open("data/should_not_upload.txt", "w") as f:
                     f.write("This file should not be uploaded.")
 
-                with open("data/test", "w") as f:
+                result = runner.invoke(
+                    cli, ["upload", "-t", reana_token, "--workflow", "mytest.1"]
+                )
+
+                assert (
+                    "==> Detected .gitignore file. Some files might get ignored."
+                    in result.output
+                )
+                post_request.assert_not_called()
+                assert result.exit_code == 0
+
+
+def test_upload_file_skip_empty_git_and_reana_ignore_files(
+    get_workflow_specification_with_directory,
+):
+    """If .reanaignore or .gitignore files are empty, ignore them.
+
+    This is edge case. We do not expect this to happen.
+    """
+    reana_token = "000000"
+    env = {"REANA_SERVER_URL": "http://localhost"}
+    runner = CliRunner(env=env)
+    mock_specification = Mock(return_value=get_workflow_specification_with_directory)
+    with runner.isolation():
+        with patch(
+            "reana_client.api.client.get_workflow_specification", mock_specification
+        ), patch("reana_client.api.client.requests.post") as post_request:
+            with runner.isolated_filesystem():
+                with open(".gitignore", "w") as f:
+                    f.write("\n")
+
+                with open(".reanaignore", "w") as f:
+                    f.write("\n")
+
+                os.mkdir("data")
+                with open("data/should_upload.txt", "w") as f:
+                    f.write("This file should be uploaded.")
+
+                result = runner.invoke(
+                    cli, ["upload", "-t", reana_token, "--workflow", "mytest.1"]
+                )
+
+                assert (
+                    "==> Detected .gitignore file. Some files might get ignored."
+                    in result.output
+                )
+                assert (
+                    "==> Detected .reanaignore file. Some files might get ignored."
+                    in result.output
+                )
+                post_request.assert_called_once()
+                assert "should_upload.txt" in result.output
+                assert result.exit_code == 0
+
+
+def test_upload_file_respect_reanaignore_and_gitignore(
+    get_workflow_specification_with_directory,
+):
+    """Check if file upload respect both reana and git ignore files with input.directories."""
+    reana_token = "000000"
+    env = {"REANA_SERVER_URL": "http://localhost"}
+    runner = CliRunner(env=env)
+    mock_specification = Mock(return_value=get_workflow_specification_with_directory)
+    with runner.isolation():
+        with patch(
+            "reana_client.api.client.get_workflow_specification", mock_specification
+        ), patch("reana_client.api.client.requests.post"):
+            with runner.isolated_filesystem():
+                with open(".gitignore", "w") as f:
+                    f.write("data/ignored_in_gitignore.txt\n")
+
+                with open(".reanaignore", "w") as f:
+                    f.write("ignored_in_reanaignore.txt\n")
+
+                os.mkdir("data")
+                os.mkdir("data/another_directory")
+                with open("data/ignored_in_gitignore.txt", "w") as f:
+                    f.write("This file should not be uploaded.")
+
+                with open("data/ignored_in_reanaignore.txt", "w") as f:
                     f.write("This file should not be uploaded.")
 
-                with open("data/will_upload.txt", "w") as f:
+                with open("data/should_be_uploaded.txt", "w") as f:
+                    f.write("This file will be uploaded.")
+
+                with open("data/another_directory/should_be_uploaded.txt", "w") as f:
                     f.write("This file will be uploaded.")
 
                 result = runner.invoke(
@@ -274,9 +357,15 @@ def test_upload_file_respect_gitignore(get_workflow_specification_with_directory
                     "==> Detected .gitignore file. Some files might get ignored."
                     in result.output
                 )
-                assert "test" not in result.output
-                post_request.assert_called_once()
-                assert "will_upload.txt" in result.output
+                assert (
+                    "==> Detected .reanaignore file. Some files might get ignored."
+                    in result.output
+                )
+                assert "ignored_in_gitignore.txt" not in result.output
+                assert "ignored_in_reanaignore.txt" not in result.output
+
+                assert "data/should_be_uploaded.txt" in result.output
+                assert "data/another_directory/should_be_uploaded.txt" in result.output
                 assert result.exit_code == 0