Repository: watts-dev/watts (language: python)

Issue:
Pointing to usage examples from docs
Following up on [https://github.com/openjournals/joss-reviews/issues/4735], I see that there is quite a collection of usage examples over at https://github.com/watts-dev/watts/tree/development/examples. Unfortunately, I really do not know have enough domain knowledge figure out where in those examples to start. I was hoping that perhaps a domain-specific "Hello world" example could be added straight to docs, perhaps on the `https://watts.readthedocs.io/en/latest/user/getting_started.html` page to get a novice user started.

I naively tried to run some scripts in those example dirs and it throws missing dir errors. It might be worthwhile to foolproof these scripts, but this probably isn't necessary to proceed with the JOSS review.


Discussion / hints:

Fix patch:
diff --git a/doc/source/dev/index.rst b/doc/source/dev/index.rst
index 3581190b..de8f5396 100644
--- a/doc/source/dev/index.rst
+++ b/doc/source/dev/index.rst
@@ -7,4 +7,5 @@ Developer's Guide
    :maxdepth: 2
 
    contributing
+   plugins
    styleguide
diff --git a/doc/source/dev/plugins.rst b/doc/source/dev/plugins.rst
new file mode 100644
index 00000000..96862163
--- /dev/null
+++ b/doc/source/dev/plugins.rst
@@ -0,0 +1,100 @@
+.. devguide_plugins:
+
+Developing Plugins
+------------------
+
+As discussed in the user's guide, WATTS has a variety of plugins for different
+codes (primarily focused on application codes in nuclear science and
+engineering) that are already available. If you wish to use WATTS with a code
+that does not already have a plugin available, extending WATTS to handle another
+code can be done quite easily.
+
+There are two ways to incorporate new codes into WATTS. First, if the code uses
+a text-based input, the :class:`~watts.PluginGeneric` class can be used to
+specify the executable, how it should be invoked from a command-line, and a
+template for the input file. For example, if you have a code called ``goblin``
+that should be executed from a command line as:
+
+.. code-block:: sh
+
+    goblin input=filein.gbl
+
+a plugin can be set up as::
+
+    goblin = watts.PluginGeneric(
+        executable='goblin',
+        execute_command=['{self.executable}', 'input={self.input_name}'],
+        template_file='filein.gbl'
+    )
+
+where ``filein.gbl`` is a template input for the ``goblin`` code. Alternatively,
+the command-line arguments can be specified as a single string::
+
+    goblin = watts.PluginGeneric(
+        executable='goblin',
+        execute_command='{self.executable} input={self.input_name}',
+        template_file='filein.gbl'
+    )
+
+Executing the code is done by passing the plugin instance a set of parameters::
+
+    params = watts.Parameters()
+    ...
+
+    result = goblin(params)
+
+The ``result`` object will be a :class:`~watts.Results` instance, which holds
+lists of input/output files, the parameters used to generate the results, and a
+timestamp.
+
+Plugin Customization
+++++++++++++++++++++
+
+If the :class:`~watts.PluginGeneric` class doesn't quite meet your needs, you
+will need to write your own plugin class that is a subclass of either
+:class:`~watts.Plugin` or :class:`~watts.PluginGeneric`. Subclassing
+:class:`~watts.PluginGeneric` is appropriate if the code you're working with
+uses text-based input files. If text-based input files are not used, you will
+need to subclass the more general :class:`~watts.Plugin` class.
+
+In either case, your plugin class will need to provide one or more of the
+following:
+
+- A ``prerun(params)`` method that is responsible for rendering inputs based on
+  the parameters that were passed and any other tasks that need to be taken care
+  of prior to execution.
+- A ``run(**kwargs)`` method that is responsible for executing the code using
+  the rendered inputs. Keyword arguments are passed through when calling the
+  plugin.
+- A ``postrun(params, name)`` method that is responsible for collecting a list
+  of input and output files and returning a :class:`~watts.Results` object.
+
+If you are subclassing :class:`watts.PluginGeneric`, note that default
+implementations of ``prerun``, ``run``, and ``postrun`` are already provided.
+The executable and command-line arguments can also be customized through the
+``executable`` and ``execute_command`` properties. Finally, the unit system to
+be used for performing unit conversions is specified as an argument that is
+passed through the ``__init__`` methods.
+
+
+Results Customization
++++++++++++++++++++++
+
+The basic :class:`~watts.Results` class stores a list of input and output files
+associated with the execution of a plugin, the :class:`~watts.Parameters` that
+were used to generate input files, and a timestamp. When writing your own
+plugin, if you don't need to provide further customization of the results, you
+should simply subclass :class:`~watts.Results` with a name matching the plugin.
+For example, if you have::
+
+    class PluginGoblin(PluginGeneric):
+        ...
+
+A minimal corresponding results class would be::
+
+    class ResultsGoblin(Results):
+        """Results from a Goblin simulation"""
+
+However, you may wish to provide extra methods and properties that allow users
+of your plugin to easily interrogate results (for example, pulling out key
+numerical results from output files).
diff --git a/doc/source/reference/index.rst b/doc/source/reference/index.rst
index 09dc7b9e..0cf91cd3 100644
--- a/doc/source/reference/index.rst
+++ b/doc/source/reference/index.rst
@@ -11,6 +11,7 @@ API Reference
    watts.Database
    watts.Parameters
    watts.Plugin
+   watts.PluginGeneric
    watts.PluginABCE
    watts.PluginMCNP
    watts.PluginMOOSE
diff --git a/doc/source/user/getting_started.rst b/doc/source/user/getting_started.rst
index f1310585..29763743 100644
--- a/doc/source/user/getting_started.rst
+++ b/doc/source/user/getting_started.rst
@@ -141,3 +141,54 @@ five MCNP simulations:
      0.97663+/-0.00063,
      1.24086+/-0.00075,
      1.47152+/-0.00081]
+
+Your First ``watts`` Program
+++++++++++++++++++++++++++++
+
+To show how the various classes fit together, the example below creates a plugin
+for a "code" (in this case, just the Linux ``cat`` command) and executes the
+code on a templated input file that is rendered using parameters that are
+defined in a :class:`~watts.Parameters` instance. This example assumes we have a
+file called triangle.txt containing the following text:
+
+.. code-block:: jinja
+
+    width={{ width }}
+    height={{ height }}
+    area={{ 0.5 * height * width }}
+
+The ``watts`` script is as follows::
+
+    import watts
+
+    # Create a plugin for the 'cat' code
+    plugin = watts.PluginGeneric(
+        executable='cat',
+        execute_command=['{self.executable}', '{self.input_name}'],
+        template_file='triangle.txt',
+        unit_system='cgs'
+    )
+
+    # Define some parameters that will be used to render the input file
+    params = watts.Parameters()
+    params['width'] = watts.Quantity(1.0, 'm')
+    params['height'] = watts.Quantity(1.0, 'inch')
+
+    # Execute the plugin
+    result = plugin(params)
+
+    # Show the resulting input file
+    print(result.stdout)
+
+Running this example will produce the following output:
+
+.. code-block:: text
+
+    width=100.0
+    height=2.54
+    area=127.0
+
+When the plugin is executed, the ``cat`` command is called on the rendered input
+file, which is just the triangle.txt file where the parameters have been filled
+in. Note that the physical quantities were :ref:`converted <units>` to
+centimeters since we indicated that this plugin uses CGS units.
diff --git a/doc/source/user/usage.rst b/doc/source/user/usage.rst
index 98b949ca..8a1b3bc0 100755
--- a/doc/source/user/usage.rst
+++ b/doc/source/user/usage.rst
@@ -167,7 +167,7 @@ Specifying an Executable
 Each plugin has a default executable name for the underlying code. For example,
 the :class:`~watts.PluginMCNP` class uses the executable ``mcnp6`` by default.
 You can both view and/or change the executable using the
-:class:`~watts.TemplatePlugin.executable` attribute:
+:class:`~watts.PluginGeneric.executable` attribute:
 
 .. code-block:: pycon
 
diff --git a/examples/1App_BISON_MetalFuel/watts_exec.py b/examples/1App_BISON_MetalFuel/watts_exec.py
index 69f1204d..b10a9807 100644
--- a/examples/1App_BISON_MetalFuel/watts_exec.py
+++ b/examples/1App_BISON_MetalFuel/watts_exec.py
@@ -1,8 +1,9 @@
 # SPDX-FileCopyrightText: 2022 UChicago Argonne, LLC
 # SPDX-License-Identifier: MIT
 
-from math import cos, pi
+from pathlib import Path
 import os
+
 import watts
 
 params = watts.Parameters()
@@ -38,12 +39,14 @@
 params.show_summary(show_metadata=False, sort_by='key')
 
 # MOOSE Workflow
-# set your BISON directorate as BISON_DIR
-
-moose_app_type = "bison"
-app_dir = os.environ[moose_app_type.upper() + "_DIR"]
-moose_plugin = watts.PluginMOOSE('bison_template', show_stdout=True) # show all the output
-moose_plugin.executable = app_dir + "/" + moose_app_type.lower() + "-opt"
+# set your BISON directory as BISON_DIR
+
+app_dir = Path(os.environ["BISON_DIR"])
+moose_plugin = watts.PluginMOOSE(
+    'bison_template',
+    executable=app_dir / 'bison-opt',
+    show_stdout=True
+)
 moose_result = moose_plugin(params, mpi_args=['mpiexec', '-n', '2'])
 for key in moose_result.csv_data:
     print(key, moose_result.csv_data[key])
diff --git a/examples/1App_MOOSE-MultiApp_Simple/watts_exec.py b/examples/1App_MOOSE-MultiApp_Simple/watts_exec.py
index 713f388c..f91b9860 100644
--- a/examples/1App_MOOSE-MultiApp_Simple/watts_exec.py
+++ b/examples/1App_MOOSE-MultiApp_Simple/watts_exec.py
@@ -2,11 +2,13 @@
 # SPDX-License-Identifier: MIT
 
 """
-This example provides a demonstration on how to use WATTS to perform a simple simulation leveraging MOOSE's MultiApps system.
+This example provides a demonstration on how to use WATTS to perform a simple
+simulation leveraging MOOSE's MultiApps system.
 """
 
-from math import cos, pi
+from pathlib import Path
 import os
+
 import watts
 
 params = watts.Parameters()
@@ -18,12 +20,14 @@
 params.show_summary(show_metadata=False, sort_by='key')
 
 # MOOSE Workflow
-# set your BISON directorate as BISON_DIR
+# set your BISON directory as BISON_DIR
 
-moose_app_type = "bison"
-app_dir = os.environ[moose_app_type.upper() + "_DIR"]
-moose_plugin = watts.PluginMOOSE('main.tmpl', extra_inputs=['main_in.e', 'sub.i'])
-moose_plugin.executable = app_dir + "/" + moose_app_type.lower() + "-opt"
+app_dir = Path(os.environ["BISON_DIR"])
+moose_plugin = watts.PluginMOOSE(
+    'main.tmpl',
+    executable=app_dir / 'bison-opt',
+    extra_inputs=['main_in.e', 'sub.i']
+)
 moose_result = moose_plugin(params)
 for key in moose_result.csv_data:
     print(key, moose_result.csv_data[key])
diff --git a/examples/1App_PyARC_UnitCell/pyarc_template b/examples/1App_PyARC_UnitCell/pyarc_template
index 20434d69..5d42180f 100644
--- a/examples/1App_PyARC_UnitCell/pyarc_template
+++ b/examples/1App_PyARC_UnitCell/pyarc_template
@@ -47,12 +47,12 @@ geometry{
     }
 }
 calculations{
-
+	lumped_element_text_file( lu35 ) = "lumped.son"
     mcc3{
        xslib            = "endf7.0"
        egroupname       = ANL33
        scattering_order = 1
-       lumped_element_text_file( lu35 ) = "lumped.son"
+       
         cell( a ){
             associated_sub_assembly     = sub_assembly_name
         }
diff --git a/examples/1App_SAM_VHTR/watts_exec.py b/examples/1App_SAM_VHTR/watts_exec.py
index dd85d392..6f090991 100644
--- a/examples/1App_SAM_VHTR/watts_exec.py
+++ b/examples/1App_SAM_VHTR/watts_exec.py
@@ -12,6 +12,8 @@
 
 from math import cos, pi
 import os
+from pathlib import Path
+
 import watts
 from astropy.units import Quantity
 
@@ -52,12 +54,10 @@
 params.show_summary(show_metadata=False, sort_by='key')
 
 # MOOSE Workflow
-# set your SAM directorate as SAM_DIR
+# set your SAM directory as SAM_DIR
 
-moose_app_type = "SAM"
-app_dir = os.environ[moose_app_type.upper() + "_DIR"]
-moose_plugin = watts.PluginMOOSE(moose_app_type.lower() + '_template') # show all the output
-moose_plugin.executable = app_dir + "/" + moose_app_type.lower() + "-opt"
+app_dir = Path(os.environ["SAM_DIR"])
+moose_plugin = watts.PluginMOOSE('sam_template', executable=app_dir / 'sam-opt')
 moose_result = moose_plugin(params)
 for key in moose_result.csv_data:
     print(key, moose_result.csv_data[key])
diff --git a/examples/Main_SAM-OpenMC_VHTR/watts_main.py b/examples/Main_SAM-OpenMC_VHTR/watts_main.py
index eb290588..f7a81137 100644
--- a/examples/Main_SAM-OpenMC_VHTR/watts_main.py
+++ b/examples/Main_SAM-OpenMC_VHTR/watts_main.py
@@ -7,10 +7,11 @@
 sensitivity analyses.
 """
 
+from pathlib import Path
 from math import cos, pi
 import os
 import watts
-from statistics import mean, stdev
+from statistics import mean
 from openmc_template import build_openmc_model
 
 params = watts.Parameters()
@@ -64,11 +65,13 @@ def calc_workflow(X):
     print("FuelPin_rad / cool_hole_rad", X[0], X[1])
 
     # MOOSE Workflow
-    # set your SAM directorate as SAM_DIR
-    moose_app_type = "SAM"
-    app_dir = os.environ[moose_app_type.upper() + "_DIR"]
-    sam_plugin = watts.PluginMOOSE('../1App_SAM_VHTR/sam_template', show_stderr=False) # does not show anything
-    sam_plugin.executable = app_dir + "/" + moose_app_type.lower() + "-opt"
+    # set your SAM directory as SAM_DIR
+    app_dir = Path(os.environ["SAM_DIR"])
+    sam_plugin = watts.PluginMOOSE(
+        '../1App_SAM_VHTR/sam_template',
+        executable=app_dir / 'sam-opt',
+        show_stderr=False
+    )
     sam_result = sam_plugin(params)
     max_Tf = max(sam_result.csv_data[f'max_Tf_{i}'][-1] for i in range(1, 6))
     avg_Tf = mean(sam_result.csv_data[f'avg_Tf_{i}'][-1] for i in range(1, 6))
diff --git a/examples/MultiApp_SAM-OpenMC_VHTR/watts_exec.py b/examples/MultiApp_SAM-OpenMC_VHTR/watts_exec.py
index 7612e306..f391275e 100644
--- a/examples/MultiApp_SAM-OpenMC_VHTR/watts_exec.py
+++ b/examples/MultiApp_SAM-OpenMC_VHTR/watts_exec.py
@@ -15,6 +15,7 @@
 in the params database.
 """
 
+from pathlib import Path
 from math import cos, pi
 import os
 import watts
@@ -70,12 +71,14 @@
 
 
 # MOOSE Workflow
-# set your SAM directorate as SAM_DIR
-
-moose_app_type = "SAM"
-app_dir = os.environ[moose_app_type.upper() + "_DIR"]
-moose_plugin = watts.PluginMOOSE('../1App_SAM_VHTR/sam_template', show_stderr=True) # show only error
-moose_plugin.executable = app_dir + "/" + moose_app_type.lower() + "-opt"
+# set your SAM directory as SAM_DIR
+
+app_dir = Path(os.environ["SAM_DIR"])
+moose_plugin = watts.PluginMOOSE(
+    '../1App_SAM_VHTR/sam_template',
+    executable=app_dir / 'sam-opt',
+    show_stderr=True
+)
 moose_result = moose_plugin(params)
 for key in moose_result.csv_data:
     print(key, moose_result.csv_data[key])
diff --git a/examples/MultiStep_Griffin-BISON-Sockeye_MR/watts_exec.py b/examples/MultiStep_Griffin-BISON-Sockeye_MR/watts_exec.py
index 04475c79..c7e1d8e9 100644
--- a/examples/MultiStep_Griffin-BISON-Sockeye_MR/watts_exec.py
+++ b/examples/MultiStep_Griffin-BISON-Sockeye_MR/watts_exec.py
@@ -1,6 +1,7 @@
 # SPDX-FileCopyrightText: 2022 UChicago Argonne, LLC
 # SPDX-License-Identifier: MIT
 
+from pathlib import Path
 import os
 import watts
 
@@ -11,15 +12,15 @@
 Third step is a real transient simulation induced by loss of cooling capacity
 """
 
-# set your SuperMoose directory as SUPER_MOOSE
+# set your SuperMoose directory as SUPER_MOOSE_DIR
 # As Sockeye is run through dynamic linking
 # set your Sockeye directory as SOCKEYE_DIR
 
 if not os.getenv("SOCKEYE_DIR"):
     raise RuntimeError("SOCKEYE_DIR must be set to enable this example.")
 # MOOSE app type to run
-moose_app_type = "super_moose"
-app_dir = os.environ[moose_app_type.upper() + "_DIR"]
+app_dir = Path(os.enrivon["SUPER_MOOSE_DIR"])
+moose_exec = app_dir / "super_moose-opt"
 
 # Steady State Parameters
 params_ss = watts.Parameters()
@@ -42,8 +43,11 @@
 # MOOSE Workflow for steady state
 print("Steady-state calculation")
 mpi_args = ['mpiexec', '-n', '40']
-moose_plugin_ss = watts.PluginMOOSE('MP_ss_griffin.tmpl', extra_inputs=['MP_ss_moose.i', 'MP_ss_sockeye.i', '3D_unit_cell_FY21_level-1_bison.e', '3D_unit_cell_FY21_supersimple.e', 'unitcell_nogap_hom_xml_G11_df_MP.xml'])
-moose_plugin_ss.executable = app_dir + "/" + moose_app_type.lower() + "-opt"
+moose_plugin_ss = watts.PluginMOOSE(
+    'MP_ss_griffin.tmpl',
+    executable=moose_exec,
+    extra_inputs=['MP_ss_moose.i', 'MP_ss_sockeye.i', '3D_unit_cell_FY21_level-1_bison.e', '3D_unit_cell_FY21_supersimple.e', 'unitcell_nogap_hom_xml_G11_df_MP.xml']
+)
 moose_result_ss = moose_plugin_ss(params_ss, mpi_args=mpi_args)
 for key in moose_result_ss.csv_data:
     print(key, moose_result_ss.csv_data[key])
@@ -58,8 +62,12 @@
 
 # MOOSE Workflow for Null transient
 print("Null transient calculation")
-moose_plugin_trN = watts.PluginMOOSE('MP_trN_griffin.tmpl', show_stdout=False, extra_inputs=['MP_trN_moose.i', 'MP_trN_sockeye.i', 'unitcell_nogap_hom_xml_G11_df_MP.xml'])
-moose_plugin_trN.executable = app_dir + "/" + moose_app_type.lower() + "-opt"
+moose_plugin_trN = watts.PluginMOOSE(
+    'MP_trN_griffin.tmpl',
+    executable=moose_exec,
+    show_stdout=False,
+    extra_inputs=['MP_trN_moose.i', 'MP_trN_sockeye.i', 'unitcell_nogap_hom_xml_G11_df_MP.xml']
+)
 moose_result_trN = moose_plugin_trN(params_trN, mpi_args=mpi_args)
 for key in moose_result_trN.csv_data:
     print(key, moose_result_trN.csv_data[key])
@@ -78,8 +86,12 @@
 
 # MOOSE Workflow for transient
 print("Transient calculation")
-moose_plugin_tr = watts.PluginMOOSE('MP_tr_griffin.tmpl', show_stdout=True, extra_inputs=['MP_tr_moose.i', 'MP_tr_sockeye.i', 'unitcell_nogap_hom_xml_G11_df_MP.xml'])
-moose_plugin_tr.executable = app_dir + "/" + moose_app_type.lower() + "-opt"
+moose_plugin_tr = watts.PluginMOOSE(
+    'MP_tr_griffin.tmpl',
+    executable=moose_exec,
+    show_stdout=True,
+    extra_inputs=['MP_tr_moose.i', 'MP_tr_sockeye.i', 'unitcell_nogap_hom_xml_G11_df_MP.xml']
+)
 moose_result_tr = moose_plugin_tr(params_tr, mpi_args=mpi_args)
 for key in moose_result_tr.csv_data:
     print(key, moose_result_tr.csv_data[key])
diff --git a/examples/Optimization_PyARC_DAKOTA/pyarc_template b/examples/Optimization_PyARC_DAKOTA/pyarc_template
index 90a74716..c3a64c0d 100644
--- a/examples/Optimization_PyARC_DAKOTA/pyarc_template
+++ b/examples/Optimization_PyARC_DAKOTA/pyarc_template
@@ -47,12 +47,12 @@ geometry{
     }
 }
 calculations{
-
+	lumped_element_text_file( lu35 ) = "lumped.son"
     mcc3{
        xslib            = "endf7.0"
        egroupname       = ANL33
        scattering_order = 1
-       lumped_element_text_file( lu35 ) = "lumped.son"
+       
         cell( a ){
             associated_sub_assembly     = sub_assembly_name
         }
diff --git a/examples/ParamStudy_SAM_VHTR/watts_exec.py b/examples/ParamStudy_SAM_VHTR/watts_exec.py
index da8accf7..0feb9879 100644
--- a/examples/ParamStudy_SAM_VHTR/watts_exec.py
+++ b/examples/ParamStudy_SAM_VHTR/watts_exec.py
@@ -15,8 +15,9 @@
 output CSV file can accept columns of different lengths.
 """
 
-from math import cos, pi
+from pathlib import Path
 import os
+
 import watts
 import pandas as pd
 from astropy.units import Quantity
@@ -45,8 +46,7 @@
 params.show_summary(show_metadata=False, sort_by='key')
 
 # MOOSE Workflow
-moose_app_type = "SAM"
-app_dir = os.environ[moose_app_type.upper() + "_DIR"]
+app_dir = Path(os.environ["SAM_DIR"])
 
 power = [100_000, 250_000, 300_000, 400_000, 500_000] # Watts
 endtime = [50, 100, 100, 50, 50] # End time is varied to artificially create results of different lengths.
@@ -56,8 +56,7 @@
     params['endtime'] = endtime[i]
 
     # Execute WATTS
-    moose_plugin = watts.PluginMOOSE(moose_app_type.lower() + '_template') # show all the output
-    moose_plugin.executable = app_dir + "/" + moose_app_type.lower() + "-opt"
+    moose_plugin = watts.PluginMOOSE('sam_template', executable=app_dir / 'sam-opt')
     moose_result = moose_plugin(params)
 
     # Add items to dictionary.
diff --git a/examples/PicardIterations_SAM-OpenMC_VHTR/watts_exec.py b/examples/PicardIterations_SAM-OpenMC_VHTR/watts_exec.py
index 2c78d1f7..c419d624 100644
--- a/examples/PicardIterations_SAM-OpenMC_VHTR/watts_exec.py
+++ b/examples/PicardIterations_SAM-OpenMC_VHTR/watts_exec.py
@@ -9,6 +9,7 @@
 model with 1 coolant channel surrounded by graphite and fuel.
 """
 
+from pathlib import Path
 from math import cos, pi
 import os
 import watts
@@ -64,13 +65,15 @@
 nmax_it = 5
 conv_criteria = 1e-4
 
+app_dir = Path(os.environ["SAM_DIR"])
 list_keff = []
 while conv_it:
     # MOOSE Workflow
-    moose_app_type = "SAM"
-    app_dir = os.environ[moose_app_type.upper() + "_DIR"]
-    sam_plugin = watts.PluginMOOSE('../1App_SAM_VHTR/sam_template', show_stderr=True) # show only error
-    sam_plugin.executable = app_dir + "/" + moose_app_type.lower() + "-opt"
+    sam_plugin = watts.PluginMOOSE(
+        '../1App_SAM_VHTR/sam_template',
+        executable=app_dir / "sam-opt",
+        show_stderr=True
+    )
     sam_result = sam_plugin(params)
 
     # get temperature from SAM results
diff --git a/src/watts/__init__.py b/src/watts/__init__.py
index 770b9bc6..499606aa 100644
--- a/src/watts/__init__.py
+++ b/src/watts/__init__.py
@@ -11,6 +11,7 @@
 from .plugin_serpent import *
 from .plugin_abce import *
 from .plugin_dakota import *
+from .results import *
 from .template import *
 from .parameters import *
 from .database import *
diff --git a/src/watts/plugin.py b/src/watts/plugin.py
index f3881755..fb96dbb8 100644
--- a/src/watts/plugin.py
+++ b/src/watts/plugin.py
@@ -8,7 +8,7 @@
 from pathlib import Path
 import shutil
 import time
-from typing import Optional, List
+from typing import Optional, List, Union
 
 from .database import Database
 from .fileutils import cd_tmpdir, PathLike, tee_stdout, tee_stderr, run as run_proc
@@ -29,6 +29,8 @@ class Plugin(ABC):
         Whether to display output from stdout when :math:`run` is called
     show_stderr
         Whether to display output from stderr when :meth:`run` is called
+    unit_system : {'si', 'cgs'}
+        Unit system to convert to when rendering input files
 
     Attributes
     ----------
@@ -39,14 +41,19 @@ class Plugin(ABC):
 
     """
 
-    def __init__(self, extra_inputs: Optional[List[PathLike]] = None,
-                 show_stdout: bool = False, show_stderr: bool = False):
+    def __init__(
+        self,
+        extra_inputs: Optional[List[PathLike]] = None,
+        show_stdout: bool = False,
+        show_stderr: bool = False,
+        unit_system: str = 'si'
+    ):
         self.extra_inputs = []
         if extra_inputs is not None:
             self.extra_inputs = [Path(f).resolve() for f in extra_inputs]
         self.show_stdout = show_stdout
         self.show_stderr = show_stderr
-        self.unit_system = 'si'
+        self.unit_system = unit_system
 
     @abstractmethod
     def prerun(self, params):
@@ -57,7 +64,7 @@ def run(self):
         ...
 
     @abstractmethod
-    def postrun(self, params) -> Results:
+    def postrun(self, params, name) -> Results:
         ...
 
     @property
@@ -127,11 +134,22 @@ def __call__(self, params: Parameters = None, name: str = 'Workflow', **kwargs)
         return result
 
 
-class TemplatePlugin(Plugin):
+class PluginGeneric(Plugin):
     """Plugin that relies on generating a template file
 
+    This class can be used to control the execution of an arbitrary executable,
+    first rendering one or more templated input files.
+
     Parameters
     ----------
+    executable
+        Path to executable
+    execute_command
+        List of command-line arguments, where each is formatted using the
+        instance of the class as ``self``. The first string normally indicates
+        the executable, i.e. "{self.executable}". The rendered input file can be
+        accessed as "{self.input_name}". A single string of command-line
+        arguments is also accepted.
     template_file
         Path to template file
     extra_inputs
@@ -142,24 +160,40 @@ class TemplatePlugin(Plugin):
         Whether to display output from stdout when :math:`run` is called
     show_stderr
         Whether to display output from stderr when :meth:`run` is called
+    unit_system : {'si', 'cgs'}
+        Unit system to convert to when rendering input files
 
     Attributes
     ----------
     executable
         Path to plugin executable
+    execute_command
+        List of command-line arguments used to call the executable
 
     """
-    def __init__(self, template_file: PathLike,
-                 extra_inputs: Optional[List[PathLike]] = None,
-                 extra_template_inputs: Optional[List[PathLike]] = None,
-                 show_stdout: bool = False, show_stderr: bool = False):
-        super().__init__(extra_inputs, show_stdout, show_stderr)
+    def __init__(self,
+        executable: PathLike,
+        execute_command: Union[List[str], str],
+        template_file: PathLike,
+        extra_inputs: Optional[List[PathLike]] = None,
+        extra_template_inputs: Optional[List[PathLike]] = None,
+        show_stdout: bool = False,
+        show_stderr: bool = False,
+        unit_system: str = 'si'
+    ):
+        super().__init__(extra_inputs, show_stdout, show_stderr, unit_system)
         self.render_template = TemplateRenderer(template_file)
         self.extra_render_templates = []
-        self.input_name = None
+        self.input_name = 'input_rendered'
         if extra_template_inputs is not None:
             self.extra_render_templates = [TemplateRenderer(f, '') for f in extra_template_inputs]
 
+        self.executable = executable
+        if isinstance(execute_command, str):
+            self._execute_command = execute_command.split()
+        else:
+            self._execute_command = execute_command
+
     @property
     def executable(self) -> Path:
         return self._executable
@@ -170,6 +204,10 @@ def executable(self, exe: PathLike):
             raise RuntimeError(f"{self.plugin_name} executable '{exe}' is missing.")
         self._executable = Path(exe)
 
+    @property
+    def execute_command(self) -> List[str]:
+        return [item.format(self=self) for item in self._execute_command]
+
     def prerun(self, params: Parameters, filename: Optional[str] = None):
         """Render the template based on model parameters
 
@@ -219,7 +257,7 @@ def postrun(self, params: Parameters, name: str, **kwargs) -> Results:
         outputs = [p for p in Path.cwd().iterdir() if p.name not in inputs]
 
         # Get correct Results subclass and return instance
-        results_cls = getattr(watts, f'Results{self.plugin_name}')
+        results_cls = getattr(watts, f'Results{self.plugin_name}', Results)
         return results_cls(params, name, time, inputs, outputs, **kwargs)
 
     def run(self, mpi_args: Optional[List[str]] = None,
diff --git a/src/watts/plugin_abce.py b/src/watts/plugin_abce.py
index d4dac936..b58c9f11 100644
--- a/src/watts/plugin_abce.py
+++ b/src/watts/plugin_abce.py
@@ -9,11 +9,11 @@
 
 from .fileutils import PathLike
 from .parameters import Parameters
-from .plugin import TemplatePlugin
+from .plugin import PluginGeneric
 from .results import Results
 
 
-class PluginABCE(TemplatePlugin):
+class PluginABCE(PluginGeneric):
     """Plugin for running ABCE
 
     Parameters
@@ -33,6 +33,8 @@ class PluginABCE(TemplatePlugin):
     ----------
     executable
         Path to ABCE executable
+    execute_command
+        List of command-line arguments used to call the executable
 
     """
 
@@ -41,17 +43,16 @@ def __init__(self, template_file: str,
                  extra_inputs: Optional[List[str]] = None,
                  extra_template_inputs: Optional[List[PathLike]] = None,
                  show_stdout: bool = False, show_stderr: bool = False):
-        super().__init__(template_file, extra_inputs, extra_template_inputs,
-                         show_stdout, show_stderr)
-        # requires users specify an environment variable, ABCE_DIR
-        self._executable = Path(os.environ['ABCE_DIR']) / 'run.py'
+        try:
+            executable = Path(os.environ['ABCE_DIR']) / 'run.py'
+        except KeyError:
+            raise OSError("Use of the ABCE plugin requires setting the ABCE_DIR environment variable.")
+        execute_command = [sys.executable, '{self.executable}', '--settings_file', '{self.input_name}']
+        super().__init__(
+            executable, execute_command, template_file, extra_inputs,
+            extra_template_inputs, show_stdout, show_stderr)
         self.input_name = 'settings.yml'
 
-    @property
-    def execute_command(self):
-        return [sys.executable, str(self._executable), '--settings_file',
-                f'{self.input_name}']
-
 
 class ResultsABCE(Results):
     """ABCE simulation results
@@ -76,4 +77,4 @@ class ResultsABCE(Results):
     """
     def __init__(self, params: Parameters, name: str, time: datetime,
                  inputs: List[PathLike], outputs: List[PathLike]):
-        super().__init__(params, name, time, inputs, outputs)
\ No newline at end of file
+        super().__init__(params, name, time, inputs, outputs)
diff --git a/src/watts/plugin_dakota.py b/src/watts/plugin_dakota.py
index b7706f90..90c3980f 100644
--- a/src/watts/plugin_dakota.py
+++ b/src/watts/plugin_dakota.py
@@ -15,7 +15,7 @@
 
 from .fileutils import PathLike
 from .parameters import Parameters
-from .plugin import TemplatePlugin
+from .plugin import PluginGeneric
 from .results import Results
 
 
@@ -60,7 +60,7 @@ def _get_Dakota_output(self, params: Parameters) -> dict:
 
         # Save Dakota's main output '.dat' files
         output_data = {}
-        
+
         if Path(dakota_out_file_name).exists():
             with open(dakota_out_file_name) as f:
                 col_names = f.readline().split()
@@ -81,7 +81,7 @@ def _get_Dakota_output(self, params: Parameters) -> dict:
         return output_data
 
 
-class PluginDakota(TemplatePlugin):
+class PluginDakota(PluginGeneric):
     """Plugin for running Dakota
 
     Parameters
@@ -97,22 +97,30 @@ class PluginDakota(TemplatePlugin):
     show_stderr
         Whether to display output from stderr when Dakota is run
 
-    """
+    Attributes
+    ----------
+    executable
+        Path to DAKOTA script
+    execute_command
+        List of command-line arguments used to call the executable
 
-    def __init__(self, template_file: str,  
+    """
+    def __init__(self, template_file: str,
                  extra_inputs: Optional[List[str]] = None,
                  extra_template_inputs: Optional[List[PathLike]] = None,
                  auto_link_files: Optional[str] = None,
                  show_stdout: bool = False, show_stderr: bool = False):
-        super().__init__(template_file, extra_inputs, extra_template_inputs,
-                         show_stdout, show_stderr)
-
         dakota_dir = Path(os.environ.get("DAKOTA_DIR", ""))
-        self._executable = dakota_dir / f"dakota.sh"
+        executable = dakota_dir / f"dakota.sh"
+        execute_command = ['{self.executable}', '-i', '{self.input_name}']
+        super().__init__(
+            executable, execute_command, template_file, extra_inputs,
+            extra_template_inputs, show_stdout, show_stderr)
+
         self.input_name = template_file
         self._auto_link_files = auto_link_files
 
-        # Setup to automatically include all 'extra_inputs' and 'extra_template_inputs' 
+        # Setup to automatically include all 'extra_inputs' and 'extra_template_inputs'
         # to Dakota's "link files" option. Create a string of the file names in
         # 'extra_inputs' and 'extra_template_inputs'.
         if self._auto_link_files is not None:
@@ -174,8 +182,8 @@ def _parse_dakota_input() -> Results:
     results
         Results from Dakota's output file
     """
-    
-    from interfacing import interfacing as di # Dakota's interface module        
+
+    from interfacing import interfacing as di # Dakota's interface module
 
     params, results = di.read_parameters_file()
 
@@ -206,10 +214,10 @@ def _run_coupled_code(coupled_code_exec: str) -> dict:
 
     subprocess.check_output(["python", coupled_code_exec])
 
-    # Read the 'opt_res.out' pickle file and 
+    # Read the 'opt_res.out' pickle file and
     # store the results to 'res_output' for data
     # transfer with Dakota.
-    if os.path.exists('opt_res.out'): 
+    if os.path.exists('opt_res.out'):
         db = pickle.load(open('opt_res.out', 'rb'))
         if 'dakota_descriptors' in db.keys():
             res_output = []
@@ -238,8 +246,8 @@ def _return_dakota_input(results: Results, retval: dict):
     try:
         for i, n, r in results:
             if r.asv.function:
-                # Returns the response function value from the coupled 
-                # code to Dakota (for pre Dakota v6.9). 
+                # Returns the response function value from the coupled
+                # code to Dakota (for pre Dakota v6.9).
                 try:
                     r.function = retval['fns'][i]
                 except:
@@ -253,4 +261,4 @@ def _return_dakota_input(results: Results, retval: dict):
 
     # Dump to external results.json file
     with open('results.json', 'w') as outfile:
-        rst = json.dump(results, outfile, default=lambda o: o.__dict__)
\ No newline at end of file
+        rst = json.dump(results, outfile, default=lambda o: o.__dict__)
diff --git a/src/watts/plugin_mcnp.py b/src/watts/plugin_mcnp.py
index cf485e05..8b3bb400 100644
--- a/src/watts/plugin_mcnp.py
+++ b/src/watts/plugin_mcnp.py
@@ -7,7 +7,7 @@
 from uncertainties import ufloat
 
 from .fileutils import PathLike
-from .plugin import TemplatePlugin
+from .plugin import PluginGeneric
 from .results import Results
 
 
@@ -49,7 +49,7 @@ def keff(self):
                     "Could not determine final k-effective value from MCNP output")
 
 
-class PluginMCNP(TemplatePlugin):
+class PluginMCNP(PluginGeneric):
     """Plugin for running MCNP
 
     Parameters
@@ -69,6 +69,8 @@ class PluginMCNP(TemplatePlugin):
     ----------
     executable
         Path to MCNP executable
+    execute_command
+        List of command-line arguments used to call the executable
 
     """
 
@@ -76,12 +78,8 @@ def __init__(self, template_file: str,
                  extra_inputs: Optional[List[str]] = None,
                  extra_template_inputs: Optional[List[PathLike]] = None,
                  show_stdout: bool = False, show_stderr: bool = False):
-        super().__init__(template_file, extra_inputs, extra_template_inputs,
-                         show_stdout, show_stderr)
-        self._executable = Path('mcnp6')
+        super().__init__(
+            'mcnp6', ['{self.executable}', 'i={self.input_name}'],
+            template_file, extra_inputs, extra_template_inputs,
+            show_stdout, show_stderr, unit_system='cgs')
         self.input_name = "mcnp_input"
-        self.unit_system = 'cgs'
-
-    @property
-    def execute_command(self):
-        return [str(self.executable), f"i={self.input_name}"]
diff --git a/src/watts/plugin_moose.py b/src/watts/plugin_moose.py
index 8be1e73d..ec76e469 100644
--- a/src/watts/plugin_moose.py
+++ b/src/watts/plugin_moose.py
@@ -2,7 +2,6 @@
 # SPDX-License-Identifier: MIT
 
 from datetime import datetime
-from pathlib import Path
 from typing import List, Optional
 
 import numpy as np
@@ -10,7 +9,7 @@
 
 from .fileutils import PathLike
 from .parameters import Parameters
-from .plugin import TemplatePlugin
+from .plugin import PluginGeneric
 from .results import Results
 
 
@@ -77,13 +76,15 @@ def _save_MOOSE_csv(self) -> dict:
         return csv_data
 
 
-class PluginMOOSE(TemplatePlugin):
+class PluginMOOSE(PluginGeneric):
     """Plugin for running MOOSE
 
     Parameters
     ----------
     template_file
         Templated MOOSE input
+    executable
+        Path to MOOSE executable
     extra_inputs
         List of extra (non-templated) input files that are needed
     extra_template_inputs
@@ -97,18 +98,18 @@ class PluginMOOSE(TemplatePlugin):
     ----------
     executable
         Path to MOOSE executable
+    execute_command
+        List of command-line arguments used to call the executable
 
     """
 
     def __init__(self, template_file: str,
+                 executable: PathLike = 'moose-opt',
                  extra_inputs: Optional[List[str]] = None,
                  extra_template_inputs: Optional[List[PathLike]] = None,
                  show_stdout: bool = False, show_stderr: bool = False):
-        super().__init__(template_file, extra_inputs, extra_template_inputs,
-                         show_stdout, show_stderr)
-        self._executable = Path('moose-opt')
+        execute_command = ['{self.executable}', '-i', '{self.input_name}']
+        super().__init__(
+            executable, execute_command, template_file, extra_inputs,
+            extra_template_inputs, show_stdout, show_stderr)
         self.input_name = "MOOSE.i"
-
-    @property
-    def execute_command(self):
-        return [str(self.executable), "-i", self.input_name]
diff --git a/src/watts/plugin_pyarc.py b/src/watts/plugin_pyarc.py
index fea786e5..e56089bb 100644
--- a/src/watts/plugin_pyarc.py
+++ b/src/watts/plugin_pyarc.py
@@ -10,7 +10,7 @@
 
 from .fileutils import PathLike
 from .parameters import Parameters
-from .plugin import TemplatePlugin
+from .plugin import PluginGeneric
 from .results import Results
 
 
@@ -40,7 +40,7 @@ def __init__(self, params: Parameters, name: str, time: datetime,
         self.results_data = results_data
 
 
-class PluginPyARC(TemplatePlugin):
+class PluginPyARC(PluginGeneric):
     """Plugin for running PyARC
 
     Parameters
@@ -67,15 +67,20 @@ def __init__(self, template_file: str,
                  extra_inputs: Optional[List[str]] = None,
                  extra_template_inputs: Optional[List[PathLike]] = None,
                  show_stdout: bool = False, show_stderr: bool = False):
-        super().__init__(template_file, extra_inputs, extra_template_inputs,
-                         show_stdout, show_stderr)
-        self._executable = Path(os.environ.get('PyARC_DIR', 'PyARC.py'))
+        executable = Path(os.environ.get('PyARC_DIR', ''))
+        super().__init__(executable, None, template_file, extra_inputs,
+                         extra_template_inputs, show_stdout, show_stderr)
         self.input_name = "pyarc_input.son"
 
-    @TemplatePlugin.executable.setter
+    @PluginGeneric.executable.setter
     def executable(self, exe: PathLike):
-        if Path(exe).exists():
-            raise RuntimeError(f"{self.plugin_name} executable '{exe}' is missing.")
+        pyarc_module = Path(exe) / 'PyARC.py'
+        if not pyarc_module.is_file():
+            raise RuntimeError(
+                f"{self.plugin_name} module '{pyarc_module}' does not exist. The "
+                "PyARC_DIR environment variable needs to be set to a directory "
+                "containing the PyARC.py module."
+            )
         self._executable = Path(exe)
 
     def run(self, **kwargs: Mapping):
diff --git a/src/watts/plugin_relap5.py b/src/watts/plugin_relap5.py
index 906ba2b0..026a69d0 100644
--- a/src/watts/plugin_relap5.py
+++ b/src/watts/plugin_relap5.py
@@ -14,7 +14,7 @@
 
 from .fileutils import PathLike
 from .parameters import Parameters
-from .plugin import TemplatePlugin
+from .plugin import PluginGeneric
 from .results import Results
 
 
@@ -61,7 +61,7 @@ def _get_relap5_csv_data(self) -> dict:
                 csv_data[column_name] =  np.array(csv_file_df[column_name])
         return csv_data
 
-class PluginRELAP5(TemplatePlugin):
+class PluginRELAP5(PluginGeneric):
     """Plugin for running RELAP5
 
     Parameters
@@ -90,17 +90,17 @@ def  __init__(self, template_file: str, plotfl_to_csv: bool = True,
                   extra_inputs: Optional[List[str]] = None,
                   extra_template_inputs: Optional[List[PathLike]] = None,
                   show_stdout: bool = False, show_stderr: bool = False):
-        super().__init__(template_file, extra_inputs, extra_template_inputs,
-                         show_stdout, show_stderr)
-
         # Check OS to make sure the extension of the executable is correct.
         # Linux and macOS have different executables but both are ".x".
         # The Windows executable is ".exe".
-
         self._relap5_dir = Path(os.environ.get("RELAP5_DIR", ""))
         self.ext = "exe" if platform.system() == "Windows" else "x"
-        self._executable = f"relap5.{self.ext}"
+        executable = f"relap5.{self.ext}"
+        execute_command = ['{self.executable}', '-i', '{self.input_name}']
 
+        super().__init__(
+            executable, execute_command, template_file, extra_inputs,
+            extra_template_inputs, show_stdout, show_stderr)
         self.input_name = "RELAP5.i"
         self.plotfl_to_csv = plotfl_to_csv
 
@@ -114,10 +114,6 @@ def relap5_dir(self, relap5_directory: PathLike):
             raise RuntimeError("RELAP5 executable is missing.")
         self._relap5_dir = Path(relap5_directory)
 
-    @property
-    def execute_command(self):
-        return [str(self.executable), '-i', self.input_name]
-
     def run(self, extra_args: Optional[List[str]] = None):
         """Run RELAP5
 
@@ -203,8 +199,7 @@ def _plotfl_to_csv(self):
         id_list = self._extract_value(ids)
 
         # Create a dictionary to store data
-        data_dict = {'channel': channel_list,
-                'id': id_list}
+        data_dict = {'channel': channel_list, 'id': id_list}
 
         for i in range(len(n_plotrec)):
             i_start = n_plotrec[i] - n_plotrec[0] # Subtract n_plotrec[0] to offset the the 'content' above 'values'
diff --git a/src/watts/plugin_sas.py b/src/watts/plugin_sas.py
index a99a9613..033f1ec2 100644
--- a/src/watts/plugin_sas.py
+++ b/src/watts/plugin_sas.py
@@ -15,7 +15,7 @@
 
 from .fileutils import PathLike, run as run_proc
 from .parameters import Parameters
-from .plugin import TemplatePlugin
+from .plugin import PluginGeneric
 from .results import Results
 
 
@@ -66,7 +66,7 @@ def _get_sas_csv_data(self) -> dict:
         return csv_data
 
 
-class PluginSAS(TemplatePlugin):
+class PluginSAS(PluginGeneric):
     """Plugin for running SAS
 
     Parameters
@@ -86,6 +86,8 @@ class PluginSAS(TemplatePlugin):
     ----------
     executable
         Path to SAS executable
+    execute_command
+        List of command-line arguments used to call the executable
     conv_channel
         Path to CHANNELtoCSV utility executable
     conv_primar4
@@ -97,17 +99,19 @@ def  __init__(self, template_file: str,
                   extra_inputs: Optional[List[str]] = None,
                   extra_template_inputs: Optional[List[PathLike]] = None,
                   show_stdout: bool = False, show_stderr: bool = False):
-        super().__init__(template_file, extra_inputs, extra_template_inputs,
-                         show_stdout, show_stderr)
-
         # Check OS to make sure the extension of the executable is correct.
         # Linux and macOS have different executables but both are ".x".
         # The Windows executable is ".exe".
-        sas_dir = Path(os.environ.get("SAS_DIR", ""))
         ext = "exe" if platform.system() == "Windows" else "x"
-        self._executable = sas_dir / f"sas.{ext}"
+        sas_dir = Path(os.environ.get("SAS_DIR", ""))
         self._conv_channel = sas_dir / f"CHANNELtoCSV.{ext}"
         self._conv_primar4 = sas_dir / f"PRIMAR4toCSV.{ext}"
+        executable = sas_dir / f"sas.{ext}"
+        execute_command = ['{self.executable}', '-i', '{self.input_name}',
+                           '-o', 'out.txt']
+
+        super().__init__(executable, execute_command, template_file, extra_inputs,
+                         extra_template_inputs, show_stdout, show_stderr)
         self.input_name = "SAS.inp"
 
     @property
diff --git a/src/watts/plugin_serpent.py b/src/watts/plugin_serpent.py
index 26919bc0..eb1d8b5e 100644
--- a/src/watts/plugin_serpent.py
+++ b/src/watts/plugin_serpent.py
@@ -7,7 +7,7 @@
 from uncertainties import ufloat
 
 from .fileutils import PathLike
-from .plugin import TemplatePlugin
+from .plugin import PluginGeneric
 from .results import Results
 
 
@@ -34,7 +34,7 @@ class ResultsSerpent(Results):
     """
 
 
-class PluginSerpent(TemplatePlugin):
+class PluginSerpent(PluginGeneric):
     """Plugin for running Serpent
 
     Parameters
@@ -54,6 +54,8 @@ class PluginSerpent(TemplatePlugin):
     ----------
     executable
         Path to Serpent executable
+    execute_command
+        List of command-line arguments used to call the executable
 
     """
 
@@ -61,12 +63,9 @@ def __init__(self, template_file: str,
                  extra_inputs: Optional[List[str]] = None,
                  extra_template_inputs: Optional[List[PathLike]] = None,
                  show_stdout: bool = False, show_stderr: bool = False):
-        super().__init__(template_file, extra_inputs, extra_template_inputs,
-                         show_stdout, show_stderr)
-        self._executable = Path('sss2')
+        super().__init__(
+            'sss2', ['{self.executable}', '{self.input_name}'],
+            template_file, extra_inputs, extra_template_inputs,
+            show_stdout, show_stderr, unit_system='cgs'
+        )
         self.input_name = "serpent_input"
-        self.unit_system = 'cgs'
-
-    @property
-    def execute_command(self):
-        return [str(self.executable), str(self.input_name)]
diff --git a/src/watts/results.py b/src/watts/results.py
index 8b08bece..0c129aef 100644
--- a/src/watts/results.py
+++ b/src/watts/results.py
@@ -38,7 +38,10 @@ def __init__(self, params: Parameters, name: str, time: datetime,
 
     @property
     def plugin(self):
-        return type(self).__name__[7:]
+        if type(self) is Results:
+            return "Generic"
+        else:
+            return type(self).__name__[7:]
 
     @property
     def stdout(self) -> str:


Test patch:
diff --git a/tests/test_template.py b/tests/test_template.py
index eb63faa5..c7fdfe4d 100644
--- a/tests/test_template.py
+++ b/tests/test_template.py
@@ -6,24 +6,18 @@
 import pytest
 
 
-# Add run and postrun methods to TemplatePlugin so that it can be instantiated
-class TemplatePlugin(watts.TemplatePlugin):
-    def run(self): ...
-    def postrun(self, model): ...
-
-
 def test_undefined_template(run_in_tmpdir):
     # Create template expecting variable
     with open('example_template', 'w') as fh:
         fh.write("{{ variable }}")
 
     # Create plugin to build template
-    plugin = TemplatePlugin('example_template')
+    plugin = watts.PluginGeneric('cat', ['{self.executable}', '{self.input_name}'], 'example_template')
 
     # Passing parameters with 'variable' set should work
     params = watts.Parameters(variable=1)
     plugin.prerun(params)
-    with open('example_template.rendered', 'r') as fh:
+    with open('input_rendered', 'r') as fh:
         rendered = fh.read()
     assert rendered == '1'
 
@@ -40,12 +34,14 @@ def test_extra_template_inputs(run_in_tmpdir):
         fh.write("{{ y }}")
 
     # Create plugin to build template
-    plugin = TemplatePlugin('main_template', extra_template_inputs=['extra_template'])
+    plugin = watts.PluginGeneric(
+        'cat', ['{self.executable}', '{self.input_name}'],
+        'main_template', extra_template_inputs=['extra_template'])
 
     # Pass parameters with 'x' and 'y' set
     params = watts.Parameters(x=1, y=2)
     plugin.prerun(params)
-    with open('main_template.rendered', 'r') as fh:
+    with open('input_rendered', 'r') as fh:
         rendered = fh.read()
     assert rendered == '1'
     with open('extra_template', 'r') as fh: