Repository: AgnostiqHQ/covalent
Pull request #1567 (2023-04-05 17:15:31)

Fix functional tests
<!--
⚠️ Your pull request title should be short detailed and understandable for all.
⚠️ Increment the version number in the /VERSION file. If this is a bugfix, increment the patch version (the rightmost number), for example 0.18.2 becomes 0.18.3. If it's a new feature, increment the minor version (the middle number), for example 0.18.2 becomes 0.19.0.
⚠️ Add a note to /CHANGELOG.md with your version number and the date, summarizing the changes.
⚠️ If your pull request fixes an open issue, please link to the issue.
⚠️ Rebase the latest changes from the develop branch. Assuming origin points to https://github.com/AgnostiqHQ/covalent, you should run git rebase origin/develop.
-->

- [ ] I have added the tests to cover my changes.
- [ ] I have updated the documentation and CHANGELOG accordingly.
- [ ] I have read the CONTRIBUTING document.


codecov[bot]: ## [Codecov](https://codecov.io/gh/AgnostiqHQ/covalent/pull/1567?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AgnostiqHQ) Report
> Merging [#1567](https://codecov.io/gh/AgnostiqHQ/covalent/pull/1567?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AgnostiqHQ) (83872d8) into [develop](https://codecov.io/gh/AgnostiqHQ/covalent/commit/501f82e5c81a62949cc6a588efa9d6db6fbb61e9?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AgnostiqHQ) (501f82e) will **decrease** coverage by `0.04%`.
> The diff coverage is `91.86%`.

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://codecov.io/gh/AgnostiqHQ/covalent/pull/1567/graphs/tree.svg?width=650&height=150&src=pr&token=YGHCB3DE4P&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AgnostiqHQ)](https://codecov.io/gh/AgnostiqHQ/covalent/pull/1567?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=AgnostiqHQ)

```diff
@@             Coverage Diff             @@
##           develop    #1567      +/-   ##
===========================================
- Coverage    85.87%   85.83%   -0.04%     
===========================================
  Files          157      159       +2     
  Lines         6476     6522      +46     
  Branches       145      145              
===========================================
+ Hits          5561     5598      +37     
- Misses         813      822       +9     
  Partials       102      102              
```

| Flag | Coverage Δ | |
|---|---|---|
| Dispatcher | `88.51% <85.71%> (-0.43%)` | :arrow_down: |
| Functional_Tests | `59.31% <59.34%> (+0.95%)` | :arrow_up: |
| SDK | `84.53% <91.17%> (+0.77%)` | :arrow_up: |
| UI_Frontend | `72.13% <ø> (ø)` | |

</details>

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d0790e45..ed34f5ee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - Doubling of nodes that are added to the transport graph.
 - Ensure postprocessing node end time is added as the workflow end time.
+- Functional tests
 
 ### Added
 
diff --git a/covalent/_dispatcher_plugins/local.py b/covalent/_dispatcher_plugins/local.py
index fc9a9d09..ac6177d5 100644
--- a/covalent/_dispatcher_plugins/local.py
+++ b/covalent/_dispatcher_plugins/local.py
@@ -91,7 +91,7 @@ class LocalDispatcher(BaseDispatcher):
         Args:
             orig_lattice: The lattice/workflow to send to the dispatcher server.
             dispatcher_addr: The address of the dispatcher server.  If None then defaults to the address set in Covalent's config.
-            disable_run: Whether to disable running the worklow and rather just save it on Covalent's server for later execution
+            disable_run: Whether to disable running the workflow and rather just save it on Covalent's server for later execution
 
         Returns:
             Wrapper function which takes the inputs of the workflow as arguments
@@ -248,12 +248,11 @@ class LocalDispatcher(BaseDispatcher):
 
             Returns:
                 The result of the executed workflow.
-            """
 
+            """
             body = get_redispatch_request_body(
                 dispatch_id, new_args, new_kwargs, replace_electrons, reuse_previous_results
             )
-
             redispatch_url = f"{dispatcher_addr}/api/redispatch"
             r = requests.post(redispatch_url, json=body, params={"is_pending": is_pending})
             r.raise_for_status()
diff --git a/covalent/_workflow/lattice.py b/covalent/_workflow/lattice.py
index d738d2d9..1a766575 100644
--- a/covalent/_workflow/lattice.py
+++ b/covalent/_workflow/lattice.py
@@ -120,13 +120,11 @@ class Lattice:
             attributes["electron_outputs"][node_name] = output.to_dict()
 
         attributes["cova_imports"] = list(self.cova_imports)
-
         return json.dumps(attributes)
 
     @staticmethod
     def deserialize_from_json(json_data: str) -> None:
         attributes = json.loads(json_data)
-
         attributes["cova_imports"] = set(attributes["cova_imports"])
 
         for node_name, object_dict in attributes["electron_outputs"].items():
diff --git a/covalent/executor/executor_plugins/timing_plugin.py b/covalent/executor/executor_plugins/timing_plugin.py
new file mode 100644
index 00000000..427fdb2f
--- /dev/null
+++ b/covalent/executor/executor_plugins/timing_plugin.py
@@ -0,0 +1,74 @@
+# Copyright 2023 Agnostiq Inc.
+#
+# This file is part of Covalent.
+#
+# Licensed under the GNU Affero General Public License 3.0 (the "License").
+# A copy of the License may be obtained with this software package or at
+#
+#      https://www.gnu.org/licenses/agpl-3.0.en.html
+#
+# Use of this file is prohibited except in compliance with the License. Any
+# modifications or derivative works of this file must retain this copyright
+# notice, and modified files must contain a notice indicating that they have
+# been altered from the originals.
+#
+# Covalent is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details.
+#
+# Relief from the License may be granted by purchasing a commercial license.
+
+
+"""Timing executor plugin for Covalent."""
+
+import time
+from pathlib import Path
+from typing import Any, Callable, Dict, List
+
+from covalent.executor.base import BaseExecutor
+
+EXECUTOR_PLUGIN_NAME = "TimingExecutor"  # Required by covalent.executors
+
+_EXECUTOR_PLUGIN_DEFAULTS = {
+    "timing_filepath": ""
+}  # Set default values for executor plugin parameters here
+
+
+class TimingExecutor(BaseExecutor):
+    """Executor that times the execution time."""
+
+    def __init__(self, timing_filepath: str = "", **kwargs):
+        """Init function.
+
+        Args:
+            timing_filepath: Filepath where the timing information will be written.
+
+        """
+        self.timing_filepath = str(Path(timing_filepath).resolve())
+        super().__init__(**kwargs)
+
+    def run(self, function: Callable, args: List, kwargs: Dict, task_metadata: Dict) -> Any:
+        """Measures the time taken to execute a function.
+
+        Args:
+            function: Function to be executed.
+            args: Arguments to be passed to the function.
+            kwargs: Keyword arguments to be passed to the function.
+            task_metadata: Metadata about the task. Expects node_id and dispatch_id.
+
+        Returns:
+            The result of the function.
+
+        """
+        start = time.process_time()
+
+        result = function(*args, **kwargs)
+
+        time_taken = time.process_time() - start
+
+        with open(f"{self.timing_filepath}", "w") as f:
+            f.write(
+                f"Node {task_metadata['node_id']} in dispatch {task_metadata['dispatch_id']} took {time_taken}s of CPU time."
+            )
+
+        return result
diff --git a/covalent_dispatcher/_core/data_manager.py b/covalent_dispatcher/_core/data_manager.py
index c2b89a5b..8dbcfa9e 100644
--- a/covalent_dispatcher/_core/data_manager.py
+++ b/covalent_dispatcher/_core/data_manager.py
@@ -156,8 +156,8 @@ def get_unique_id() -> str:
 
     Returns:
         str: Unique ID
-    """
 
+    """
     return str(uuid.uuid4())
 
 
@@ -191,7 +191,16 @@ def _get_result_object_from_new_lattice(
 def _get_result_object_from_old_result(
     old_result_object: Result, reuse_previous_results: bool
 ) -> Result:
-    """Get new result object for re-dispatching from old result object."""
+    """Get new result object for re-dispatching from old result object.
+
+    Args:
+        old_result_object: Parent dispatch result object.
+        reuse_previous_results: Whether to reuse previous results from the parent dispatch.
+
+    Returns:
+        Result object.
+
+    """
     result_object = Result(old_result_object.lattice, get_unique_id())
     result_object._num_nodes = old_result_object._num_nodes
 
@@ -207,7 +216,19 @@ def make_derived_dispatch(
     electron_updates: Optional[Dict[str, Callable]] = None,
     reuse_previous_results: bool = False,
 ) -> str:
-    """Make a re-dispatch from a previous dispatch."""
+    """Make a re-dispatch from a previous dispatch.
+
+    Args:
+        parent_dispatch_id: dispatch ID of the parent dispatch.
+        json_lattice: JSON-serialized lattice to use for the new dispatch.
+        electron_updates: dictionary of electron updates to apply to the new dispatch.
+        reuse_previous_results: whether to reuse previous results from the parent dispatch.
+
+    Returns:
+        str: dispatch ID of the new dispatch.
+
+    """
+    app_log.debug("Making derived dispatch ...")
     if electron_updates is None:
         electron_updates = {}
 
@@ -228,6 +249,7 @@ def make_derived_dispatch(
     )
     update.persist(result_object)
     _register_result_object(result_object)
+    app_log.debug(f"Redispatch result object: {result_object}")
 
     return result_object.dispatch_id
 
diff --git a/covalent_dispatcher/_core/dispatcher.py b/covalent_dispatcher/_core/dispatcher.py
index d4f9b4d8..5b86cbc1 100644
--- a/covalent_dispatcher/_core/dispatcher.py
+++ b/covalent_dispatcher/_core/dispatcher.py
@@ -158,32 +158,31 @@ async def _submit_task(result_object, node_id):
 
     # Handle parameter nodes
     if node_name.startswith(parameter_prefix):
-        app_log.debug("7C: Parameter if block (run_planned_workflow).")
         output = result_object.lattice.transport_graph.get_node_value(node_id, "value")
-        app_log.debug(f"7C: Node output: {output} (run_planned_workflow).")
-        app_log.debug("8: Starting update node (run_planned_workflow).")
-
         timestamp = datetime.now(timezone.utc)
-        node_result = {
-            "node_id": node_id,
-            "start_time": timestamp,
-            "end_time": timestamp,
-            "status": Result.COMPLETED,
-            "output": output,
-        }
+        node_result = datasvc.generate_node_result(
+            node_id=node_id,
+            start_time=timestamp,
+            end_time=timestamp,
+            status=Result.COMPLETED,
+            output=output,
+        )
+
         await datasvc.update_node_result(result_object, node_result)
         app_log.debug("8A: Update node success (run_planned_workflow).")
 
     elif node_status == Result.COMPLETED:
+        output = result_object.lattice.transport_graph.get_node_value(node_id, "output")
         timestamp = datetime.now(timezone.utc)
-        node_result = {
-            "node_id": node_id,
-            "start_time": timestamp,
-            "end_time": timestamp,
-            "status": Result.COMPLETED,
-        }
+        node_result = datasvc.generate_node_result(
+            node_id=node_id,
+            start_time=timestamp,
+            end_time=timestamp,
+            status=Result.COMPLETED,
+            output=output,
+        )
         await datasvc.update_node_result(result_object, node_result)
-        app_log.debug(f"Skipped completed node {node_id}.")
+        app_log.debug(f"Skipped completed node {node_name}.")
 
     else:
         # Executor for post_processing and dispatching sublattices
@@ -398,6 +397,8 @@ def run_dispatch(dispatch_id: str) -> asyncio.Future:
 
     Return(s)
         asyncio.Future
+
     """
+    app_log.debug(f"Running dispatch with dispatch_id: {dispatch_id}.")
     result_object = datasvc.get_result_object(dispatch_id)
     return asyncio.create_task(run_workflow(result_object))
diff --git a/covalent_dispatcher/_db/load.py b/covalent_dispatcher/_db/load.py
index 41fb8d91..39892cd4 100644
--- a/covalent_dispatcher/_db/load.py
+++ b/covalent_dispatcher/_db/load.py
@@ -115,6 +115,7 @@ def _result_from(lattice_record: Lattice) -> Result:
         "lattice_imports": lattice_imports,
         "post_processing": False,
         "electron_outputs": {},
+        "_bound_electrons": {},
     }
 
     def dummy_function(x):
diff --git a/covalent_dispatcher/_db/update.py b/covalent_dispatcher/_db/update.py
index 58bb60fa..f2b3a15d 100644
--- a/covalent_dispatcher/_db/update.py
+++ b/covalent_dispatcher/_db/update.py
@@ -110,6 +110,7 @@ def _node(
     upsert.electron_data(result)
 
     if node_name.startswith(postprocess_prefix):
+        app_log.warning(f"Persisting postprocess result {output}, node_name: {node_name}")
         result._result = output
         result._status = status
         result._end_time = end_time
diff --git a/covalent_dispatcher/_service/app.py b/covalent_dispatcher/_service/app.py
index 6c791275..b96409cf 100644
--- a/covalent_dispatcher/_service/app.py
+++ b/covalent_dispatcher/_service/app.py
@@ -76,6 +76,9 @@ async def redispatch(request: Request, is_pending: bool = False) -> str:
         json_lattice = data["json_lattice"]
         electron_updates = data["electron_updates"]
         reuse_previous_results = data["reuse_previous_results"]
+        app_log.debug(
+            f"Unpacked redispatch request for {dispatch_id}. reuse_previous_results: {reuse_previous_results}, electron_updates: {electron_updates}"
+        )
         return await dispatcher.run_redispatch(
             dispatch_id, json_lattice, electron_updates, reuse_previous_results, is_pending
         )
diff --git a/covalent_dispatcher/entry_point.py b/covalent_dispatcher/entry_point.py
index cbb02616..42ec31a8 100644
--- a/covalent_dispatcher/entry_point.py
+++ b/covalent_dispatcher/entry_point.py
@@ -66,6 +66,7 @@ async def run_redispatch(
 ):
     from ._core import make_derived_dispatch, run_dispatch
 
+    app_log.debug("Running redispatch ...")
     if is_pending:
         run_dispatch(dispatch_id)
         app_log.debug(f"Submitted pending dispatch_id {dispatch_id} to run_dispatch.")
@@ -74,6 +75,7 @@ async def run_redispatch(
     redispatch_id = make_derived_dispatch(
         dispatch_id, json_lattice, electron_updates, reuse_previous_results
     )
+    app_log.debug(f"Redispatch_id {redispatch_id} created.")
     run_dispatch(redispatch_id)
 
     app_log.debug(f"Re-dispatching {dispatch_id} as {redispatch_id}")
diff --git a/doc/source/how_to/coding/add_pip_dependencies_to_electron.ipynb b/doc/source/how_to/coding/add_pip_dependencies_to_electron.ipynb
index aa73f700..5d35f72b 100644
--- a/doc/source/how_to/coding/add_pip_dependencies_to_electron.ipynb
+++ b/doc/source/how_to/coding/add_pip_dependencies_to_electron.ipynb
@@ -136,7 +136,7 @@
    "source": [
     "@ct.electron(\n",
     "    call_before = [ct.DepsPip(packages=[\"numpy==1.22.4\"])],\n",
-    "    call_after = [ct.DepsPip(packages=[\"networkx==2.5\"])]\n",
+    "    call_after = [ct.DepsPip(packages=[\"networkx==2.8.6\"])]\n",
     ")\n",
     "def get_result():\n",
     "    matrix = numpy.identity(3)\n",
diff --git a/doc/source/how_to/execution/creating_custom_executors.ipynb b/doc/source/how_to/execution/creating_custom_executors.ipynb
index de46faf0..c8684103 100644
--- a/doc/source/how_to/execution/creating_custom_executors.ipynb
+++ b/doc/source/how_to/execution/creating_custom_executors.ipynb
@@ -25,6 +25,7 @@
    ]
   },
   {
+   "attachments": {},
    "cell_type": "markdown",
    "metadata": {},
    "source": [
@@ -42,8 +43,8 @@
     "- [SlurmPlugin](https://github.com/AgnostiqHQ/covalent-slurm-plugin).\n",
     "\n",
     "To make your executor async-capable, do the following:\n",
-    "    1. Subclass `AsyncBaseExecutor` instead of `BaseExecutor`\n",
-    "    2. Define your `run()` function with:\n",
+    "    i. Subclass `AsyncBaseExecutor` instead of `BaseExecutor`\n",
+    "    ii. Define your `run()` function with:\n",
     "\n",
     "        `async def run(...)`\n",
     "    \n",
@@ -55,26 +56,14 @@
    ]
   },
   {
-   "cell_type": "code",
-   "execution_count": 1,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "# timing_plugin.py\n",
-    "\n",
-    "from covalent.executor import BaseExecutor\n",
-    "from typing import Callable, Dict, List\n",
-    "import time \n",
-    "from pathlib import Path"
-   ]
-  },
-  {
+   "attachments": {},
    "cell_type": "markdown",
    "metadata": {},
    "source": [
     "3. Write the plugin class. The class must contain:\n",
     "\n",
-    "- The class name of the executor, shared in `executor_plugin_name` to make it importable by `covalent.executors`.\n",
+    "- The class name of the executor, shared in `EXECUTOR_PLUGIN_NAME` to make it importable by `covalent.executors`.\n",
+    "- The default values for the plugin parameters defined in `_EXECUTOR_PLUGIN_DEFAULTS`.\n",
     "- A `run()` function that handles the task to be executed. The `run()` function must take these parameters:\n",
     "    - A `Callable` object to contain the task;\n",
     "    - A list of arguments (`args`) and a dictionary of keyword arguments (`kwargs`) to pass to the `Callable`.\n",
@@ -86,15 +75,27 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 2,
+   "execution_count": 1,
    "metadata": {},
    "outputs": [],
    "source": [
-    "executor_plugin_name = \"TimingExecutor\" # Required by covalent.executors \n",
+    "# timing_plugin.py\n",
     "\n",
-    "class TimingExecutor(BaseExecutor):\n",
+    "from typing import Callable, Dict, List\n",
+    "import time\n",
+    "from pathlib import Path\n",
+    "from covalent.executor.base import BaseExecutor\n",
+    "\n",
+    "\n",
+    "EXECUTOR_PLUGIN_NAME = \"TimingExecutor\"  # Required by covalent.executors\n",
     "\n",
-    "    def __init__(self, timing_filepath, **kwargs):\n",
+    "_EXECUTOR_PLUGIN_DEFAULTS = {\n",
+    "    \"timing_filepath\": \"\"\n",
+    "}  # Set default values for executor plugin parameters here\n",
+    "\n",
+    "\n",
+    "class TimingExecutor(BaseExecutor):\n",
+    "    def __init__(self, timing_filepath: str = \"\", **kwargs):\n",
     "        self.timing_filepath = str(Path(timing_filepath).resolve())\n",
     "        super().__init__(**kwargs)\n",
     "\n",
@@ -106,10 +107,11 @@
     "\n",
     "        time_taken = time.process_time() - start\n",
     "\n",
-    "        with open(f\"{self.timing_filepath}\", \"a\") as f:\n",
-    "            f.write(f\"Node {task_metadata['node_id']} in dispatch {task_metadata['dispatch_id']} took {time_taken}s of CPU time.\")\n",
+    "        with open(f\"{self.timing_filepath}\", \"w\") as f:\n",
+    "            f.write(\n",
+    "                f\"Node {task_metadata['node_id']} in dispatch {task_metadata['dispatch_id']} took {time_taken}s of CPU time.\"\n",
+    "            )\n",
     "\n",
-    "        close(f)\n",
     "        return result\n"
    ]
   },
@@ -124,28 +126,29 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 3,
+   "execution_count": 2,
    "metadata": {},
    "outputs": [],
    "source": [
     "import covalent as ct\n",
     "\n",
-    "timing_log = \"./cpu_timing.log\"\n",
-    "timing_executor = TimingExecutor(timing_log)\n",
+    "timing_log = \"/tmp/cpu_timing.log\"\n",
+    "timing_executor = ct.executor.TimingExecutor(timing_log)\n",
     "\n",
     "# Calculate e based on a series\n",
-    "@ct.electron    \n",
+    "@ct.electron(executor=timing_executor)\n",
     "def e_ser(x):\n",
     "    e_est = 1\n",
     "    fact = 1\n",
     "    for i in range(1, x):\n",
     "        fact *= i\n",
-    "        e_est += 1/fact\n",
+    "        e_est += 1 / fact\n",
     "    return e_est\n",
     "\n",
-    "@ct.lattice\n",
+    "\n",
+    "@ct.lattice(executor=timing_executor)\n",
     "def workflow(x):\n",
-    "    return e_ser(x)"
+    "    return e_ser(x)\n"
    ]
   },
   {
@@ -157,7 +160,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 4,
+   "execution_count": 3,
    "metadata": {},
    "outputs": [
     {
@@ -173,28 +176,19 @@
       "input kwargs: {}\n",
       "error: None\n",
       "\n",
-      "start_time: 2023-01-31 23:07:16.920729\n",
-      "end_time: 2023-01-31 23:07:17.030380\n",
+      "start_time: 2023-04-11 16:42:37.321829\n",
+      "end_time: 2023-04-11 16:42:37.829460\n",
       "\n",
-      "results_dir: /Users/mini-me/agnostiq/covalent/doc/source/how_to/execution/results\n",
-      "dispatch_id: a2119573-5465-4390-869b-5709991da0e1\n",
+      "results_dir: /Users/faiyaz/.local/share/covalent/data\n",
+      "dispatch_id: f25e85fd-6654-4032-84a1-c8474134aa5f\n",
       "\n",
       "Node Outputs\n",
       "------------\n",
       "e_ser(0): 2.7182815255731922\n",
       ":parameter:10(1): 10\n",
-      "\n"
-     ]
-    },
-    {
-     "ename": "FileNotFoundError",
-     "evalue": "[Errno 2] No such file or directory: './cpu_timing.log'",
-     "output_type": "error",
-     "traceback": [
-      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
-      "\u001b[0;31mFileNotFoundError\u001b[0m                         Traceback (most recent call last)",
-      "Input \u001b[0;32mIn [4]\u001b[0m, in \u001b[0;36m<cell line: 5>\u001b[0;34m()\u001b[0m\n\u001b[1;32m      2\u001b[0m result \u001b[38;5;241m=\u001b[39m ct\u001b[38;5;241m.\u001b[39mget_result(dispatch_id, wait\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[1;32m      3\u001b[0m \u001b[38;5;28mprint\u001b[39m(result)\n\u001b[0;32m----> 5\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m line \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28;43mopen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mtiming_log\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mr\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m:\n\u001b[1;32m      6\u001b[0m     \u001b[38;5;28mprint\u001b[39m(line)\n",
-      "\u001b[0;31mFileNotFoundError\u001b[0m: [Errno 2] No such file or directory: './cpu_timing.log'"
+      ":postprocess:(2): 2.7182815255731922\n",
+      "\n",
+      "Node 0 in dispatch f25e85fd-6654-4032-84a1-c8474134aa5f took 0.00023200000000000998s of CPU time.\n"
      ]
     }
    ],
@@ -203,8 +197,8 @@
     "result = ct.get_result(dispatch_id, wait=True)\n",
     "print(result)\n",
     "\n",
-    "for line in open(timing_log, 'r'):\n",
-    "    print(line)"
+    "for line in open(timing_log, \"r\"):\n",
+    "    print(line)\n"
    ]
   },
   {


Test patch:
diff --git a/tests/covalent_dispatcher_tests/_db/load_test.py b/tests/covalent_dispatcher_tests/_db/load_test.py
index 5c36f330..ea35cb13 100644
--- a/tests/covalent_dispatcher_tests/_db/load_test.py
+++ b/tests/covalent_dispatcher_tests/_db/load_test.py
@@ -179,9 +179,11 @@ def test_result_from(mocker):
         "lattice_imports",
         "post_processing",
         "electron_outputs",
+        "_bound_electrons",
     }
     assert lattice_mock_attrs["post_processing"] is False
     assert lattice_mock_attrs["electron_outputs"] == {}
+    assert lattice_mock_attrs["_bound_electrons"] == {}
 
     _, args, _ = lattice_mock.mock_calls[0]
     assert args[0].__name__ == "dummy_function"
diff --git a/tests/covalent_tests/executor/executor_plugins/timing_plugin_test.py b/tests/covalent_tests/executor/executor_plugins/timing_plugin_test.py
new file mode 100644
index 00000000..3a7a8517
--- /dev/null
+++ b/tests/covalent_tests/executor/executor_plugins/timing_plugin_test.py
@@ -0,0 +1,59 @@
+# Copyright 2023 Agnostiq Inc.
+#
+# This file is part of Covalent.
+#
+# Licensed under the GNU Affero General Public License 3.0 (the "License").
+# A copy of the License may be obtained with this software package or at
+#
+#      https://www.gnu.org/licenses/agpl-3.0.en.html
+#
+# Use of this file is prohibited except in compliance with the License. Any
+# modifications or derivative works of this file must retain this copyright
+# notice, and modified files must contain a notice indicating that they have
+# been altered from the originals.
+#
+# Covalent is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the License for more details.
+#
+# Relief from the License may be granted by purchasing a commercial license.
+
+
+"""Unit tests for the TimingPlugin class."""
+
+
+from covalent.executor.executor_plugins.timing_plugin import (
+    _EXECUTOR_PLUGIN_DEFAULTS,
+    EXECUTOR_PLUGIN_NAME,
+    TimingExecutor,
+)
+
+
+def test_init():
+    """Test initialization of TimingPlugin."""
+
+    timing_plugin = TimingExecutor("mock-path")
+    assert EXECUTOR_PLUGIN_NAME == "TimingExecutor"
+    assert _EXECUTOR_PLUGIN_DEFAULTS == {"timing_filepath": ""}
+    assert timing_plugin.timing_filepath.endswith("mock-path")
+
+
+def test_run(mocker):
+    """Test the run method of TimingPlugin."""
+
+    mocker.patch(
+        "covalent.executor.executor_plugins.timing_plugin.time.process_time", side_effect=[1, 2]
+    )
+
+    timing_plugin = TimingExecutor("/tmp/mock_timing_file.log")
+
+    def mock_func(x):
+        return x + 2
+
+    res = timing_plugin.run(mock_func, [1], {}, {"node_id": 1, "dispatch_id": 1})
+    assert res == 3
+
+    with open(timing_plugin.timing_filepath, "r") as f:
+        timing_file = f.read()
+
+    assert timing_file == "Node 1 in dispatch 1 took 1s of CPU time."
diff --git a/tests/functional_tests/docs_how_to_test.py b/tests/functional_tests/docs_how_to_test.py
index 2fca3708..b38088b2 100644
--- a/tests/functional_tests/docs_how_to_test.py
+++ b/tests/functional_tests/docs_how_to_test.py
@@ -40,6 +40,7 @@ ignore_files = [
     "visualize_lattice.ipynb",
     "cancel_dispatch.ipynb",
     "construct_bash_task.ipynb",
+    "file_transfers_to_from_remote.ipynb",
 ]
 
 
diff --git a/tests/functional_tests/workflow_stack_test.py b/tests/functional_tests/workflow_stack_test.py
index a97eff76..c160feab 100644
--- a/tests/functional_tests/workflow_stack_test.py
+++ b/tests/functional_tests/workflow_stack_test.py
@@ -64,7 +64,7 @@ def test_check_nodes():
         return identity(a=result1)
 
     check.build_graph(a=1, b=2)
-    assert [0, 1, 2, 3] == list(check.transport_graph._graph.nodes)
+    assert list(check.transport_graph._graph.nodes) == list(range(5))
 
 
 def test_electron_takes_nested_iterables():
@@ -621,29 +621,6 @@ def test_all_parameter_types_in_lattice():
     assert result.result == (10, (3, 4), {"d": 6, "e": 7})
 
 
-def test_client_workflow_executor():
-    """
-    Test setting `workflow_executor="client"`
-    """
-
-    @ct.electron
-    def new_func(a, b, c, d, e):
-        return a + b + c + d + e
-
-    @ct.lattice(workflow_executor="client")
-    def work_func(a, b, c):
-        return new_func(a, b, c, d=4, e=5)
-
-    dispatch_id = ct.dispatch(work_func)(1, 2, c=3)
-
-    workflow_result = rm.get_result(dispatch_id, wait=True)
-    assert workflow_result.status == Result.PENDING_POSTPROCESSING
-    assert workflow_result.result is None
-    update.persist(workflow_result)
-    assert workflow_result.post_process() == 15
-    rm._delete_result(dispatch_id)
-
-
 def test_two_iterations():
     """Confirm we can build the graph with more than one iteration"""
 
@@ -657,7 +634,7 @@ def test_two_iterations():
         return first + b + last
 
     midword.build_graph("hello world", "beautiful", 6)
-    assert [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] == list(midword.transport_graph._graph.nodes)
+    assert list(midword.transport_graph._graph.nodes) == list(range(11))
 
 
 def test_two_iterations_float():
@@ -673,7 +650,7 @@ def test_two_iterations_float():
         return half + quarter
 
     add_half_quarter.build_graph(0.1)
-    assert [0, 1, 2, 3, 4, 5, 6] == list(add_half_quarter.transport_graph._graph.nodes)
+    assert list(add_half_quarter.transport_graph._graph.nodes) == list(range(8))
 
 
 def test_wait_for():
@@ -890,10 +867,6 @@ def test_redispatch_reusing_previous_results():
     def sum_task(x, y):
         return x + y
 
-    @ct.electron
-    def mult_task(x, y):
-        return x * y
-
     @ct.electron
     def square_task(x):
         return x * x
diff --git a/tests/requirements.txt b/tests/requirements.txt
index 5cfaacf0..c93e853e 100644
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -1,3 +1,4 @@
+boto3==1.26.110
 detect-secrets==1.3.0
 flake8==5.0.4
 isort==5.10.1
@@ -11,3 +12,4 @@ pytest-asyncio==0.19.0
 pytest-cov==3.0.0
 pytest-mock==3.8.2
 pytest-rerunfailures==10.2
+scikit-image==0.19.1