Repository: SPFlow/SPFlow (language: python)

Issue:
Add Codecov platform
Implements #168.


Discussion / hints:
## Welcome to [Codecov](https://codecov.io?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=SPFlow) :tada:

Once merged to your default branch, Codecov will compare your coverage reports and display the results in this comment.

Thanks for integrating Codecov - We've got you covered :open_umbrella:

Fix patch:
diff --git a/.github/codecov.yaml b/.github/codecov.yaml
new file mode 100644
index 000000000..982ae7ce2
--- /dev/null
+++ b/.github/codecov.yaml
@@ -0,0 +1,20 @@
+coverage:
+  status:
+    project:
+      default:
+        informational: true
+        target: auto
+        # this allows a small drop from the previous base commit coverage
+        threshold: 0.5%
+    patch:
+      default:
+        informational: true
+        target: auto
+        # this allows a small drop from the previous base commit coverage
+        threshold: 0.5%
+
+comment:
+  require_changes: true  # only post the comment if coverage changes
+
+# github_checks:
+	# annotations: false
diff --git a/README.md b/README.md
index 794f1264a..15d3f4aee 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,7 @@
 [![Python version](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
 [![License](https://img.shields.io/github/license/SPFlow/SPFlow.svg)](https://github.com/SPFlow/SPFlow/blob/master/LICENSE.md)
 [![Code style: black & isort](https://img.shields.io/badge/code%20style-black%20%26%20isort-000000.svg)](https://black.readthedocs.io/en/stable/)
+[![codecov](https://codecov.io/gh/SPFlow/SPFlow/graph/badge.svg?token=4L7geh0Pxz)](https://codecov.io/gh/SPFlow/SPFlow)
 
 #### Development branch for SPFlow 1.0.0
 
diff --git a/pyproject.toml b/pyproject.toml
index 9540c2652..f0543ef28 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -57,17 +57,17 @@ torch = [
 ]
 jax = [
   "jax>=0.4.0",
-  "jaxlib>=0.4.0"
+  "jaxlib>=0.4.0",
 ]
 
 
 [tool.pytest.ini_options]
-addopts = " --color=yes --doctest-modules"
+addopts = "--color=yes --doctest-modules"
 testpaths = [
     # for the doctests:
-    "spflow",
+    # "spflow",  # TODO: Add for doctests and for detecting broken yet unimported modules
     # for the actual tests:
-    "tests"
+    "tests",
 ]
 
 [tool.black]
@@ -77,7 +77,7 @@ target-version = [
     "py39",
     "py310",
     "py311",
-    "py312"
+    "py312",
 ]
 
 [tool.ruff]
diff --git a/spflow/modules/node/__init__.py b/spflow/modules/node/__init__.py
index 8ef34f325..c1bd85320 100644
--- a/spflow/modules/node/__init__.py
+++ b/spflow/modules/node/__init__.py
@@ -1,3 +1,4 @@
+from .node import Node
 from .sum import SumNode
 from .product import ProductNode
 from .cond_sum import CondSumNode
diff --git a/spflow/utils/model_manager.py b/spflow/utils/model_manager.py
index b774a8f5b..56e2e58a1 100644
--- a/spflow/utils/model_manager.py
+++ b/spflow/utils/model_manager.py
@@ -1,10 +1,23 @@
+import os
+from typing import Any, Union
 import pickle
+
 from spflow.modules.module import Module
 
+try:
+    from typing import TypeAlias
+except ImportError:
+    TypeAlias = None
+
+
+PathLike: TypeAlias = Union[str, bytes, os.PathLike]
+
 
-def save_model(model: Module, file: str):
-    pickle.dump(model, open(file, "wb"))
+def save_model(model: Module, path: PathLike) -> None:
+    with open(path, "wb") as file:
+        pickle.dump(model, file)
 
 
-def load_model(file: str) -> Module:
-    return pickle.load(open(file, "rb"))
+def load_model(path: PathLike) -> Module:
+    with open(path, "rb") as file:
+        return pickle.load(file)


Test patch:
diff --git a/.github/actions/run_tests/action.yaml b/.github/actions/run_tests/action.yaml
index 6aa36bb81..37ba45dc1 100644
--- a/.github/actions/run_tests/action.yaml
+++ b/.github/actions/run_tests/action.yaml
@@ -53,7 +53,7 @@ runs:
     - name: Test with pytest
       shell: bash
       run: |
-        SPFLOW_TEST_BACKEND=${{ matrix.test-backend }} pytest --cov=spflow tests
+        SPFLOW_TEST_BACKEND=${{ matrix.test-backend }} pytest --cov=spflow
 
     - name: Upload coverage reports to Codecov
       uses: codecov/codecov-action@v3
diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/utils/test_model_manager_minimal.py b/tests/utils/test_model_manager_minimal.py
new file mode 100644
index 000000000..03b1c4209
--- /dev/null
+++ b/tests/utils/test_model_manager_minimal.py
@@ -0,0 +1,39 @@
+"""TODO: A dummy test case that shall be replaced by the proper one in `tests_old`."""
+
+import unittest
+
+from pytest import fixture
+
+from spflow.meta.data import Scope
+from spflow.modules.node import Node, ProductNode
+from spflow.modules.node.leaf import Gaussian
+from spflow.utils.model_manager import load_model, save_model
+from ..fixtures import backend_auto
+
+
+@fixture
+def example_spn() -> Node:
+    return ProductNode(
+        children=[
+            Gaussian(Scope([0])),
+        ],
+    )
+
+
+@fixture
+def tmp_model_file(tmp_path):
+    return tmp_path / "test_model.pkl"
+
+
+def test_save_model(backend_auto, tmp_model_file, example_spn):
+    save_model(example_spn, tmp_model_file)
+    assert tmp_model_file.exists()
+
+
+def test_load_model(backend_auto, tmp_model_file, example_spn):
+    save_model(example_spn, tmp_model_file)
+    load_model(tmp_model_file)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests_old/tensorly/utils/test_model_manager.py b/tests_old/tensorly/utils/test_model_manager.py
index 0ae64edc8..1ee28eaab 100644
--- a/tests_old/tensorly/utils/test_model_manager.py
+++ b/tests_old/tensorly/utils/test_model_manager.py
@@ -1,23 +1,20 @@
-import os
+import unittest
 
-import tensorly as tl
+from pytest import fixture
 import numpy as np
 
-import unittest
 from spflow.modules.module import log_likelihood
 from spflow.utils.model_manager import load_model, save_model
 from spflow.meta.data import Scope
 from spflow.modules.node import Gaussian
-from spflow.structure.spn import ProductNode, SumNode
+from spflow.structure.spn import Node, ProductNode, SumNode
 from spflow.utils import Tensor
 from spflow.tensor import ops as tle
 
-tc = unittest.TestCase()
-filename = "save.p"
-
 
-def create_example_spn():
-    spn = SumNode(
+@fixture
+def example_spn() -> SumNode:
+    return Node(
         children=[
             ProductNode(
                 children=[
@@ -55,51 +52,38 @@ def create_example_spn():
         ],
         weights=tl.tensor([0.4, 0.6]),
     )
-    return spn
 
 
-def test_save_model(do_for_all_backends):
-    # save model at filename
-    save_model(create_example_spn(), filename)
-    tc.assertTrue(os.path.exists(filename))
+@fixture
+def tmp_model_file(tmp_path):
+    return tmp_path / "test_model.pkl"
 
-    # delete model
-    if os.path.exists(filename):
-        os.remove(filename)
 
+def test_save_model(do_for_all_backends, tmp_model_file, example_spn):
+    save_model(example_spn, tmp_model_file)
+    assert tmp_model_file.exists()
 
-def test_load_model(do_for_all_backends):
-    # save mdoel
-    example_spn = create_example_spn()
-    save_model(example_spn, filename)
-    # load the same model
-    loaded_model = load_model(filename)
+
+def test_load_model(do_for_all_backends, tmp_model_file, example_spn):
+    save_model(example_spn, tmp_model_file)
+    loaded_model = load_model(tmp_model_file)
 
     # check if model attributes are equal
     for m1, m2 in zip(example_spn.modules(), loaded_model.modules()):
-        tc.assertTrue(m1.__class__ == m2.__class__)
-        tc.assertTrue(m1.scope == m2.scope)
-        tc.assertTrue(m1.backend == m2.backend)
+        assert m1.__class__ == m2.__class__
+        assert m1.scope == m2.scope
+        assert m1.backend == m2.backend
         if isinstance(m1, SumNode):
-            tc.assertTrue(tl.all(m1.weights == m2.weights))
-
-    # delete model
-    if os.path.exists(filename):
-        os.remove(filename)
+            assert tl.all(m1.weights == m2.weights)
 
 
-def test_load_model_inference(do_for_all_backends):
-    example_spn = create_example_spn()
+def test_load_model_inference(do_for_all_backends, tmp_model_file, example_spn):
     dummy_data = tl.tensor([[1.0, 0.0, 1.0]])
     ll_result = log_likelihood(example_spn, dummy_data)
-    save_model(example_spn, filename)
-    loaded_model = load_model(filename)
+    save_model(example_spn, tmp_model_file)
+    loaded_model = load_model(tmp_model_file)
     loaded_ll_result = log_likelihood(loaded_model, dummy_data)
-    tc.assertTrue(np.allclose(tle.toNumpy(ll_result), tle.toNumpy(loaded_ll_result)))
-
-    # delete model
-    if os.path.exists(filename):
-        os.remove(filename)
+    assert np.allclose(tle.toNumpy(ll_result), tle.toNumpy(loaded_ll_result))
 
 
 if __name__ == "__main__":