From 05be3e60fa59fd87af0bd4d65cbd265ebbca8dd9 Mon Sep 17 00:00:00 2001
From: Valentin Lorentz <vlorentz@softwareheritage.org>
Date: Wed, 18 Dec 2019 18:32:27 +0100
Subject: [PATCH] Clean temporary file handling for metadata generation, and
 add test.

This uses a context-manager instead of manually handling
removing the file.
---
 swh/deposit/cli/client.py            | 141 +++++++++++----------------
 swh/deposit/tests/cli/test_client.py |  47 +++++++--
 2 files changed, 96 insertions(+), 92 deletions(-)

diff --git a/swh/deposit/cli/client.py b/swh/deposit/cli/client.py
index 9eb7bfc3..f631e165 100644
--- a/swh/deposit/cli/client.py
+++ b/swh/deposit/cli/client.py
@@ -49,7 +49,7 @@ def _url(url):
     return url
 
 
-def generate_metadata_file(name, external_id, authors):
+def generate_metadata_file(name, external_id, authors, temp_dir):
     """Generate a temporary metadata file with the minimum required metadata
 
     This generates a xml file in a temporary location and returns the
@@ -67,8 +67,7 @@ def generate_metadata_file(name, external_id, authors):
         Filepath to the metadata generated file
 
     """
-    _, tmpfile = tempfile.mkstemp(prefix='swh.deposit.cli.')
-
+    path = os.path.join(temp_dir, 'metadata.xml')
     # generate a metadata file with the minimum required metadata
     codemetadata = {
         'entry': {
@@ -82,29 +81,13 @@ def generate_metadata_file(name, external_id, authors):
         },
     }
 
-    logging.debug('Temporary file: %s', tmpfile)
+    logging.debug('Temporary file: %s', path)
     logging.debug('Metadata dict to generate as xml: %s', codemetadata)
     s = xmltodict.unparse(codemetadata, pretty=True)
     logging.debug('Metadata dict as xml generated: %s', s)
-    with open(tmpfile, 'w') as fp:
+    with open(path, 'w') as fp:
         fp.write(s)
-    return tmpfile
-
-
-def _cleanup_tempfile(config):
-    """Clean up the temporary metadata file generated.
-
-    Args:
-
-        config (Dict): A configuration dict with 2 important keys for
-        that routine, 'cleanup_tempfile' (bool) and 'metadata' (path
-        to eventually clean up)
-
-    """
-    if config['cleanup_tempfile']:
-        path = config['metadata']
-        if os.path.exists(path):
-            os.unlink(path)
+    return path
 
 
 def _client(url, username, password):
@@ -144,7 +127,7 @@ def client_command_parse_input(
         username, password, archive, metadata,
         archive_deposit, metadata_deposit,
         collection, slug, partial, deposit_id, replace,
-        url, name, authors):
+        url, name, authors, temp_dir):
     """Parse the client subcommand options and make sure the combination
        is acceptable*.  If not, an InputError exception is raised
        explaining the issue.
@@ -187,66 +170,55 @@ def client_command_parse_input(
             'deposit_id': optional deposit identifier
 
     """
-    cleanup_tempfile = False
+    if archive_deposit and metadata_deposit:
+        # too many flags use, remove redundant ones (-> multipart deposit)
+        archive_deposit = False
+        metadata_deposit = False
 
-    try:
-        if archive_deposit and metadata_deposit:
-            # too many flags use, remove redundant ones (-> multipart deposit)
-            archive_deposit = False
-            metadata_deposit = False
+    if not slug:  # generate one as this is mandatory
+        slug = generate_slug()
 
-        if not slug:  # generate one as this is mandatory
-            slug = generate_slug()
+    if not metadata and name and authors:
+        metadata = generate_metadata_file(name, slug, authors, temp_dir)
 
-        if not metadata and name and authors:
-            metadata = generate_metadata_file(name, slug, authors)
-            cleanup_tempfile = True
+    if metadata_deposit:
+        archive = None
 
-        if metadata_deposit:
-            archive = None
+    if archive_deposit:
+        metadata = None
 
-        if archive_deposit:
-            metadata = None
+    if metadata_deposit and not metadata:
+        raise InputError(
+            "Metadata deposit must be provided for metadata "
+            "deposit (either a filepath or --name and --author)")
 
-        if metadata_deposit and not metadata:
-            raise InputError(
-                "Metadata deposit must be provided for metadata "
-                "deposit (either a filepath or --name and --author)")
+    if not archive and not metadata and partial:
+        raise InputError(
+            'Please provide an actionable command. See --help for more '
+            'information')
 
-        if not archive and not metadata:
-            raise InputError(
-                'Please provide an actionable command. See --help for more '
-                'information')
+    if replace and not deposit_id:
+        raise InputError(
+            'To update an existing deposit, you must provide its id')
 
-        if replace and not deposit_id:
-            raise InputError(
-                'To update an existing deposit, you must provide its id')
-
-        client = _client(url, username, password)
+    client = _client(url, username, password)
 
-        if not collection:
-            collection = _collection(client)
+    if not collection:
+        collection = _collection(client)
 
-        return {
-            'archive': archive,
-            'username': username,
-            'password': password,
-            'metadata': metadata,
-            'cleanup_tempfile': cleanup_tempfile,
-            'collection': collection,
-            'slug': slug,
-            'in_progress': partial,
-            'client': client,
-            'url': url,
-            'deposit_id': deposit_id,
-            'replace': replace,
-        }
-    except Exception:  # to be clean, cleanup prior to raise
-        _cleanup_tempfile({
-            'cleanup_tempfile': cleanup_tempfile,
-            'metadata': metadata
-        })
-        raise
+    return {
+        'archive': archive,
+        'username': username,
+        'password': password,
+        'metadata': metadata,
+        'collection': collection,
+        'slug': slug,
+        'in_progress': partial,
+        'client': client,
+        'url': url,
+        'deposit_id': deposit_id,
+        'replace': replace,
+    }
 
 
 def _subdict(d, keys):
@@ -329,17 +301,17 @@ https://docs.softwareheritage.org/devel/swh-deposit/getting-started.html.
     url = _url(url)
     config = {}
 
-    try:
-        logger.debug('Parsing cli options')
-        config = client_command_parse_input(
-            username, password, archive, metadata, archive_deposit,
-            metadata_deposit, collection, slug, partial, deposit_id,
-            replace, url, name, author)
-    except InputError as e:
-        logger.error('Problem during parsing options: %s', e)
-        sys.exit(1)
+    with tempfile.TemporaryDirectory() as temp_dir:
+        try:
+            logger.debug('Parsing cli options')
+            config = client_command_parse_input(
+                username, password, archive, metadata, archive_deposit,
+                metadata_deposit, collection, slug, partial, deposit_id,
+                replace, url, name, author, temp_dir)
+        except InputError as e:
+            logger.error('Problem during parsing options: %s', e)
+            sys.exit(1)
 
-    try:
         if verbose:
             logger.info("Parsed configuration: %s" % (
                 config, ))
@@ -353,9 +325,6 @@ https://docs.softwareheritage.org/devel/swh-deposit/getting-started.html.
 
         logger.info(r)
 
-    finally:
-        _cleanup_tempfile(config)
-
 
 @deposit.command()
 @click.option('--url', default='https://deposit.softwareheritage.org',
diff --git a/swh/deposit/tests/cli/test_client.py b/swh/deposit/tests/cli/test_client.py
index b2dafab5..c265e11c 100644
--- a/swh/deposit/tests/cli/test_client.py
+++ b/swh/deposit/tests/cli/test_client.py
@@ -3,6 +3,7 @@
 # License: GNU General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
+import contextlib
 import logging
 import os
 from unittest.mock import MagicMock
@@ -76,11 +77,15 @@ def test_collection_ok():
 
 
 def test_single_minimal_deposit(
-        sample_archive, mocker, caplog, client_mock, slug):
+        sample_archive, mocker, caplog, client_mock, slug, tmp_path):
     """ from:
     https://docs.softwareheritage.org/devel/swh-deposit/getting-started.html#single-deposit
     """  # noqa
 
+    metadata_path = os.path.join(tmp_path, 'metadata.xml')
+    mocker.patch('swh.deposit.cli.client.tempfile.TemporaryDirectory',
+                 return_value=contextlib.nullcontext(str(tmp_path)))
+
     runner = CliRunner()
     result = runner.invoke(cli, [
         'upload',
@@ -89,6 +94,7 @@ def test_single_minimal_deposit(
         '--password', TEST_USER['password'],
         '--name', 'test-project',
         '--archive', sample_archive['path'],
+        '--author', 'Jane Doe',
     ])
 
     assert result.exit_code == 0, result.output
@@ -99,18 +105,34 @@ def test_single_minimal_deposit(
 
     client_mock.deposit_create.assert_called_once_with(
         archive=sample_archive['path'],
-        collection='softcol', in_progress=False, metadata=None,
+        collection='softcol', in_progress=False, metadata=metadata_path,
         slug=slug)
 
-
-def test_single_deposit_slug_collection(
-        sample_archive, mocker, caplog, client_mock):
+    with open(metadata_path) as fd:
+        assert fd.read() == f'''\
+<?xml version="1.0" encoding="utf-8"?>
+<entry xmlns="http://www.w3.org/2005/Atom" \
+xmlns:codemeta="https://doi.org/10.5063/SCHEMA/CODEMETA-2.0">
+\t<codemeta:name>test-project</codemeta:name>
+\t<codemeta:identifier>{slug}</codemeta:identifier>
+\t<codemeta:author>
+\t\t<codemeta:name>Jane Doe</codemeta:name>
+\t</codemeta:author>
+</entry>'''
+
+
+def test_single_deposit_slug_generation(
+        sample_archive, mocker, caplog, tmp_path, client_mock):
     """ from:
     https://docs.softwareheritage.org/devel/swh-deposit/getting-started.html#single-deposit
     """  # noqa
     slug = 'my-slug'
     collection = 'my-collection'
 
+    metadata_path = os.path.join(tmp_path, 'metadata.xml')
+    mocker.patch('swh.deposit.cli.client.tempfile.TemporaryDirectory',
+                 return_value=contextlib.nullcontext(str(tmp_path)))
+
     runner = CliRunner()
     result = runner.invoke(cli, [
         'upload',
@@ -121,6 +143,7 @@ def test_single_deposit_slug_collection(
         '--archive', sample_archive['path'],
         '--slug', slug,
         '--collection', collection,
+        '--author', 'Jane Doe',
     ])
 
     assert result.exit_code == 0, result.output
@@ -131,9 +154,21 @@ def test_single_deposit_slug_collection(
 
     client_mock.deposit_create.assert_called_once_with(
         archive=sample_archive['path'],
-        collection=collection, in_progress=False, metadata=None,
+        collection=collection, in_progress=False, metadata=metadata_path,
         slug=slug)
 
+    with open(metadata_path) as fd:
+        assert fd.read() == '''\
+<?xml version="1.0" encoding="utf-8"?>
+<entry xmlns="http://www.w3.org/2005/Atom" \
+xmlns:codemeta="https://doi.org/10.5063/SCHEMA/CODEMETA-2.0">
+\t<codemeta:name>test-project</codemeta:name>
+\t<codemeta:identifier>my-slug</codemeta:identifier>
+\t<codemeta:author>
+\t\t<codemeta:name>Jane Doe</codemeta:name>
+\t</codemeta:author>
+</entry>'''
+
 
 def test_multisteps_deposit(
         sample_archive, atom_dataset, mocker, caplog, datadir,
-- 
GitLab