Repository: cta-observatory/ctapipe
Pull request #2038 (2022-07-25 12:51:26)

Add 'raises' to run_tool, which raises exceptions instead of logging them.
With this, we can test that tools fails with the expected Exceptions. 


Patch:
diff --git a/ctapipe/core/tool.py b/ctapipe/core/tool.py
index ab0fa1de..97e4e80b 100644
--- a/ctapipe/core/tool.py
+++ b/ctapipe/core/tool.py
@@ -6,10 +6,11 @@ import pathlib
 import re
 import textwrap
 from abc import abstractmethod
+from inspect import cleandoc
 from typing import Union
+
 import yaml
 from docutils.core import publish_parts
-from inspect import cleandoc
 
 try:
     import tomli as toml
@@ -18,7 +19,7 @@ try:
 except ImportError:
     HAS_TOML = False
 
-from traitlets import default, List
+from traitlets import List, default
 from traitlets.config import Application, Config, Configurable
 
 from .. import __version__ as version
@@ -312,7 +313,7 @@ class Tool(Application):
         after `Tool.start` when `Tool.run` is called."""
         self.log.info("Goodbye")
 
-    def run(self, argv=None):
+    def run(self, argv=None, raises=False):
         """Run the tool. This automatically calls `initialize()`,
         `start()` and `finish()`
 
@@ -360,6 +361,8 @@ class Tool(Application):
             self.log.exception(f"Caught unexpected exception: {err}")
             Provenance().finish_activity(activity_name=self.name, status="error")
             exit_status = 1  # any other error
+            if raises:
+                raise err
         finally:
             if not {"-h", "--help", "--help-all"}.intersection(self.argv):
                 self.write_provenance()
@@ -525,7 +528,7 @@ def export_tool_config_to_commented_yaml(tool_instance: Tool, classes=None):
     return "\n".join(lines)
 
 
-def run_tool(tool: Tool, argv=None, cwd=None):
+def run_tool(tool: Tool, argv=None, cwd=None, raises=False):
     """
     Utility run a certain tool in a python session without exitinig
 
@@ -539,7 +542,7 @@ def run_tool(tool: Tool, argv=None, cwd=None):
     try:
         # switch to cwd for running and back after
         os.chdir(cwd)
-        tool.run(argv or [])
+        tool.run(argv or [], raises=raises)
     except SystemExit as e:
         return e.code
     finally:


Test patch:
diff --git a/ctapipe/core/tests/test_tool.py b/ctapipe/core/tests/test_tool.py
index 8e402ecc..26df23c0 100644
--- a/ctapipe/core/tests/test_tool.py
+++ b/ctapipe/core/tests/test_tool.py
@@ -1,13 +1,14 @@
-import os
+import json
 import logging
+import os
 import tempfile
+from pathlib import Path
+
 import pytest
-import json
-from traitlets import Float, TraitError, Dict, Int
+from traitlets import Dict, Float, Int, TraitError
 from traitlets.config import Config
-from pathlib import Path
 
-from .. import Tool, Component
+from .. import Component, Tool
 from ..tool import export_tool_config_to_commented_yaml, run_tool
 
 
@@ -33,7 +34,7 @@ def test_tool_simple():
 
 
 def test_tool_version():
-    """ check that the tool gets an automatic version string"""
+    """check that the tool gets an automatic version string"""
 
     class MyTool(Tool):
         description = "test"
@@ -44,7 +45,7 @@ def test_tool_version():
 
 
 def test_provenance_dir():
-    """ check that the tool gets the provenance dir"""
+    """check that the tool gets the provenance dir"""
 
     class MyTool(Tool):
         description = "test"
@@ -57,7 +58,7 @@ def test_provenance_dir():
 
 
 def test_provenance_log_help(tmpdir):
-    """ check that the tool does not write a provenance log if only the help was run"""
+    """check that the tool does not write a provenance log if only the help was run"""
     from ctapipe.core.tool import run_tool
 
     class MyTool(Tool):
@@ -72,8 +73,9 @@ def test_provenance_log_help(tmpdir):
 
 
 def test_export_config_to_yaml():
-    """ test that we can export a Tool's config to YAML"""
+    """test that we can export a Tool's config to YAML"""
     import yaml
+
     from ctapipe.tools.process import ProcessorTool
 
     tool = ProcessorTool()
@@ -87,7 +89,7 @@ def test_export_config_to_yaml():
 
 
 def test_tool_html_rep(tmp_path):
-    """ check that the HTML rep for Jupyter notebooks works"""
+    """check that the HTML rep for Jupyter notebooks works"""
 
     class MyTool(Tool):
         description = "test"
@@ -100,7 +102,7 @@ def test_tool_html_rep(tmp_path):
         val = Float(1.0, help="val").tag(config=True)
 
     class MyTool2(Tool):
-        """ A docstring description"""
+        """A docstring description"""
 
         userparam = Float(5.0, help="parameter").tag(config=True)
 
@@ -121,7 +123,7 @@ def test_tool_html_rep(tmp_path):
 
 
 def test_tool_current_config():
-    """ Check that we can get the full instance configuration """
+    """Check that we can get the full instance configuration"""
 
     class MyTool(Tool):
         description = "test"
@@ -137,7 +139,7 @@ def test_tool_current_config():
 
 
 def test_tool_current_config_subcomponents():
-    """ Check that we can get the full instance configuration """
+    """Check that we can get the full instance configuration"""
     from ctapipe.core.component import Component
 
     class SubComponent(Component):
@@ -172,7 +174,7 @@ def test_tool_current_config_subcomponents():
 
 
 def test_tool_exit_code():
-    """ Check that we can get the full instance configuration """
+    """Check that we can get the full instance configuration"""
 
     class MyTool(Tool):
 
@@ -340,3 +342,26 @@ def test_invalid_traits(tmp_path, caplog):
         json.dump({"MyTool": {"foo": 5}}, f)
 
     assert run_tool(MyTool(), [f"--config={config}"]) == 2
+
+
+def test_tool_raises():
+    class ToolGood(Tool):
+        name = "Good"
+        description = "This runs without an exception."
+
+        def start(self):
+            print("All good.")
+
+    class ToolBad(Tool):
+        name = "Bad"
+        description = "This tool raises an exception."
+
+        def start(self):
+            raise ValueError("1 does not equal 0.")
+
+    assert run_tool(ToolGood(), raises=True) == 0
+
+    assert run_tool(ToolBad(), raises=False) == 1
+
+    with pytest.raises(ValueError):
+        run_tool(ToolBad(), raises=True)