Repository: kyleaoman/martini (language: python)

Issue:
Tests for simulation-specific source modules
Implement unit tests? Hosting sample input data is the main hurdle. Consider using the jupyter notebooks to fill this role, I think they can be used in automated testing, but the sample data remains the hurdle.


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 650f711..0d46d29 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,4 +15,6 @@ examples/*.hdf5
 examples/*.npy
 examples/*.tar
 examples/sql_cookies.txt
+examples/eagle_db.password
+examples/tng_api.key
 .coverage
\ No newline at end of file
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 0000000..022300f
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,13 @@
+coverage:
+  status:
+    project:
+      default:
+        target: 100%
+        threshold: 10%
+ignore:
+  - "tests"
+  - "docs"
+  - "martini/sources/_illustris_tools.py"
+  - "martini/sources/tng_source.py"
+  - "martini/sources/eagle_source.py"
+  - "martini/sources/simba_source.py"
\ No newline at end of file
diff --git a/examples/martini_TNG.ipynb b/examples/martini_TNG.ipynb
index de82a8a..7dd93d0 100644
--- a/examples/martini_TNG.ipynb
+++ b/examples/martini_TNG.ipynb
@@ -49,7 +49,11 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {},
+   "metadata": {
+    "tags": [
+     "skip-execution"
+    ]
+   },
    "outputs": [],
    "source": [
     "import sys\n",
@@ -70,7 +74,11 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {},
+   "metadata": {
+    "tags": [
+     "skip-execution"
+    ]
+   },
    "outputs": [],
    "source": [
     "!{sys.executable} -m pip install matplotlib"
@@ -120,7 +128,11 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {},
+   "metadata": {
+    "tags": [
+     "skip-execution"
+    ]
+   },
    "outputs": [],
    "source": [
     "from martini import demo\n",
@@ -156,7 +168,11 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {},
+   "metadata": {
+    "tags": [
+     "skip-execution"
+    ]
+   },
    "outputs": [],
    "source": [
     "from getpass import getpass\n",
@@ -174,7 +190,18 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {},
+   "metadata": {
+    "nbmake": {
+     "post_cell_execute": [
+      "import os",
+      "try:",
+      "    api_key = os.environ['TNG_API_KEY']",
+      "except KeyError:  # running locally",
+      "    with open('tng_api.key') as f:",
+      "        api_key = f.read()"
+     ]
+    }
+   },
    "outputs": [],
    "source": [
     "simulation = \"TNG100-1\"\n",
@@ -210,7 +237,8 @@
     "from martini.noise import GaussianNoise\n",
     "from martini.spectral_models import GaussianSpectrum\n",
     "from martini.sph_kernels import CubicSplineKernel\n",
-    "import astropy.units as U"
+    "import astropy.units as U\n",
+    "import numpy as np"
    ]
   },
   {
@@ -546,6 +574,28 @@
     "M.preview()"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "The main source insertion loop, that is the most computationally demanding step, can be run in parallel if the `multiprocess` package is installed (not to be confused with `multiprocessing`, which is normally included with python!). Edit the cell below to use more than 1 CPU core if this package is installed."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "nbmake": {
+     "post_cell_execute": [
+      "ncpu = 8"
+     ]
+    }
+   },
+   "outputs": [],
+   "source": [
+    "ncpu = 1  # can be >1 if multiprocess package is installed"
+   ]
+  },
   {
    "cell_type": "markdown",
    "metadata": {},
@@ -559,7 +609,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "M.insert_source_in_cube()\n",
+    "M.insert_source_in_cube(ncpu=ncpu)\n",
     "M.add_noise()\n",
     "M.convolve_beam()"
    ]
@@ -568,8 +618,6 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "The main source insertion loop, that is the most computationally demanding step, can be run in parallel if the `multiprocess` package is installed (not to be confused with `multiprocessing`, which is normally included with python!). Simply use `M.insert_source_in_cube(ncpu=N)`, where `N` is the number of processes to run in parallel.\n",
-    "\n",
     "You may notice that the number of pixels in the progress counter differs from the number defined in the DataCube module. This is because convolution with the beam requires some padding, which is ultimately filled with nonsense and discarded.\n",
     "\n",
     "To write the results: two output formats are available, depending on preference. Both\n",
@@ -976,7 +1024,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.11.0rc1"
+   "version": "3.12.4"
   }
  },
  "nbformat": 4,
diff --git a/examples/martini_eagle.ipynb b/examples/martini_eagle.ipynb
index 53dd3a1..cc39d60 100644
--- a/examples/martini_eagle.ipynb
+++ b/examples/martini_eagle.ipynb
@@ -42,7 +42,11 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {},
+   "metadata": {
+    "tags": [
+     "skip-execution"
+    ]
+   },
    "outputs": [],
    "source": [
     "import sys\n",
@@ -63,7 +67,11 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {},
+   "metadata": {
+    "tags": [
+     "skip-execution"
+    ]
+   },
    "outputs": [],
    "source": [
     "!{sys.executable} -m pip install requests matplotlib"
@@ -113,7 +121,11 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {},
+   "metadata": {
+    "tags": [
+     "skip-execution"
+    ]
+   },
    "outputs": [],
    "source": [
     "from martini import demo\n",
@@ -147,7 +159,11 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {},
+   "metadata": {
+    "tags": [
+     "skip-execution"
+    ]
+   },
    "outputs": [],
    "source": [
     "from getpass import getpass\n",
@@ -220,7 +236,16 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {},
+   "metadata": {
+    "nbmake": {
+     "post_cell_execute": [
+      "username = \"jdw115\"",
+      "f = open(\"eagle_db.password\")",
+      "password = f.read()",
+      "f.close()"
+     ]
+    }
+   },
    "outputs": [],
    "source": [
     "snappath = \"RefL0025N0376/snapshot_028_z000p000\"\n",
@@ -651,6 +676,28 @@
     "M.preview()"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "The main source insertion loop, that is the most computationally demanding step, can be run in parallel if the `multiprocess` package is installed (not to be confused with `multiprocessing`, which is normally included with python!). Edit the cell below to use more than 1 CPU core if this package is installed."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "nbmake": {
+     "post_cell_execute": [
+      "ncpu = 8"
+     ]
+    }
+   },
+   "outputs": [],
+   "source": [
+    "ncpu = 1  # can be >1 if multiprocess package is installed"
+   ]
+  },
   {
    "cell_type": "markdown",
    "metadata": {},
@@ -664,7 +711,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "M.insert_source_in_cube()\n",
+    "M.insert_source_in_cube(ncpu=ncpu)\n",
     "M.add_noise()\n",
     "M.convolve_beam()"
    ]
@@ -673,8 +720,6 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "The main source insertion loop, that is the most computationally demanding step, can be run in parallel if the `multiprocess` package is installed (not to be confused with `multiprocessing`, which is normally included with python!). Simply use `M.insert_source_in_cube(ncpu=N)`, where `N` is the number of processes to run in parallel. \n",
-    "\n",
     "You may notice that the number of pixels in the progress counter differs from the number defined in the DataCube module. This is because convolution with the beam requires some padding, which is ultimately filled with nonsense and discarded.\n",
     "\n",
     "To write the results: two output formats are available, depending on preference. Both\n",
@@ -734,7 +779,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "FluxCube.shape"
+    "flux_cube.shape"
    ]
   },
   {
@@ -1079,7 +1124,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.11.0rc1"
+   "version": "3.12.4"
   }
  },
  "nbformat": 4,
diff --git a/examples/martini_simba.ipynb b/examples/martini_simba.ipynb
index d08c062..a1b8ff6 100644
--- a/examples/martini_simba.ipynb
+++ b/examples/martini_simba.ipynb
@@ -42,7 +42,11 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {},
+   "metadata": {
+    "tags": [
+     "skip-execution"
+    ]
+   },
    "outputs": [],
    "source": [
     "import sys\n",
@@ -63,7 +67,11 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {},
+   "metadata": {
+    "tags": [
+     "skip-execution"
+    ]
+   },
    "outputs": [],
    "source": [
     "!{sys.executable} -m pip install requests matplotlib"
@@ -113,7 +121,11 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {},
+   "metadata": {
+    "tags": [
+     "skip-execution"
+    ]
+   },
    "outputs": [],
    "source": [
     "from martini import demo\n",
@@ -591,6 +603,28 @@
     "M.preview()"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "The main source insertion loop, that is the most computationally demanding step, can be run in parallel if the `multiprocess` package is installed (not to be confused with `multiprocessing`, which is normally included with python!). Edit the cell below to use more than 1 CPU core if this package is installed."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "nbmake": {
+     "post_cell_execute": [
+      "ncpu = 8"
+     ]
+    }
+   },
+   "outputs": [],
+   "source": [
+    "ncpu = 1  # can be >1 if multiprocess package is installed"
+   ]
+  },
   {
    "cell_type": "markdown",
    "metadata": {},
@@ -604,7 +638,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "M.insert_source_in_cube()\n",
+    "M.insert_source_in_cube(ncpu=ncpu)\n",
     "M.add_noise()\n",
     "M.convolve_beam()"
    ]
@@ -613,8 +647,6 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "The main source insertion loop, that is the most computationally demanding step, can be run in parallel if the `multiprocess` package is installed (not to be confused with `multiprocessing`, which is normally included with python!). Simply use `M.insert_source_in_cube(ncpu=N)`, where `N` is the number of processes to run in parallel.\n",
-    "\n",
     "You may notice that the number of pixels in the progress counter differs from the number defined in the DataCube module. This is because convolution with the beam requires some padding, which is ultimately filled with nonsense and discarded.\n",
     "\n",
     "To write the results: two output formats are available, depending on preference. Both\n",
@@ -643,11 +675,7 @@
   },
   {
    "cell_type": "markdown",
-   "metadata": {
-    "jupyter": {
-     "source_hidden": true
-    }
-   },
+   "metadata": {},
    "source": [
     "Let's load the FITS file that MARTINI produced and take a quick look."
    ]
@@ -1025,7 +1053,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.11.0rc1"
+   "version": "3.12.4"
   }
  },
  "nbformat": 4,
diff --git a/examples/martini_source_injection.ipynb b/examples/martini_source_injection.ipynb
index 36199de..d94144c 100644
--- a/examples/martini_source_injection.ipynb
+++ b/examples/martini_source_injection.ipynb
@@ -45,7 +45,11 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {},
+   "metadata": {
+    "tags": [
+     "skip-execution"
+    ]
+   },
    "outputs": [],
    "source": [
     "import sys\n",
@@ -84,7 +88,11 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {},
+   "metadata": {
+    "tags": [
+     "skip-execution"
+    ]
+   },
    "outputs": [],
    "source": [
     "from getpass import getpass\n",
@@ -102,7 +110,18 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {},
+   "metadata": {
+    "nbmake": {
+     "post_cell_execute": [
+      "import os",
+      "try:",
+      "    api_key = os.environ['TNG_API_KEY']",
+      "except KeyError:  # running locally",
+      "    with open('tng_api.key') as f:",
+      "        api_key = f.read()"
+     ]
+    }
+   },
    "outputs": [],
    "source": [
     "simulation = \"TNG50-1\"\n",
@@ -417,8 +436,28 @@
    "source": [
     "The reason that we got a message saying that some particles were removed is clear: our galaxy extends past the edge of the cube. This isn't a problem, perhaps this \"galaxy\" that wasn't the main target of the observation happened to straddle the boundary of the field of view.\n",
     "\n",
-    "Next we run the bulk of the work of inserting the source into the datacube and the convolution of the beam. On a single CPU the source insertion step could take quite a while (hours?), so running this in parallel is recommended if you have a multi-core CPU. Just edit ``M.insert_source_in_cube(ncpu=8)`` specifying the number of cores that you wish to use. My 24-core setup runs this in a couple of minutes.\n",
-    "\n",
+    "Next we run the bulk of the work of inserting the source into the datacube and the convolution of the beam. On a single CPU the source insertion step could take quite a while (hours?), so running this in parallel is recommended if you have a multi-core CPU. Just edit the following cell specifying the number of cores that you wish to use. My 32-core setup runs the insertion in a couple of minutes."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "nbmake": {
+     "post_cell_execute": [
+      "ncpu = 24"
+     ]
+    }
+   },
+   "outputs": [],
+   "source": [
+    "ncpu = 1  # can set >1 if multiprocess is installed"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
     "By default a progress bar (provided by ``tqdm``) will be displayed. This can be a bit glitchy on notebooks, especially when running on multiple cores, you can disable it with ``M.insert_source_in_cube(progressbar=False)``.\n",
     "\n",
     "The beam convolution step is relatively inexpensive.\n",
@@ -434,7 +473,7 @@
    "source": [
     "martini_cube_file = \"sourceinjectiondemo.fits\"\n",
     "if not os.path.isfile(martini_cube_file):\n",
-    "    M.insert_source_in_cube()  # this'll take a while\n",
+    "    M.insert_source_in_cube(ncpu=ncpu)  # this could take a while\n",
     "    M.convolve_beam()\n",
     "    M.write_fits(martini_cube_file)"
    ]
@@ -464,7 +503,7 @@
     "with fits.open(\"NGC_2841_NA_CUBE_THINGS.FITS\") as fitsfile:\n",
     "    ngc2841_header = fitsfile[0].header\n",
     "    ngc2841_cube = fitsfile[0].data\n",
-    "with fits.open(\"sourceinjectiondemo.fits\") as fitsfile:\n",
+    "with fits.open(martini_cube_file) as fitsfile:\n",
     "    martini_header = fitsfile[0].header\n",
     "    martini_cube = fitsfile[0].data.transpose((0, 1, 2, 3))  # this transpose: no effect"
    ]
@@ -577,7 +616,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.11.0rc1"
+   "version": "3.12.4"
   }
  },
  "nbformat": 4,
diff --git a/optional_requirements.txt b/optional_requirements.txt
index 31c0ac2..1640edc 100644
--- a/optional_requirements.txt
+++ b/optional_requirements.txt
@@ -11,3 +11,4 @@ swiftgalaxy
 g3t
 pytest-cov
 numpydoc
+nbmake


Test patch:
diff --git a/.github/workflows/lint_and_test.yml b/.github/workflows/lint_and_test.yml
index 1bf2e77..3c1bb7f 100644
--- a/.github/workflows/lint_and_test.yml
+++ b/.github/workflows/lint_and_test.yml
@@ -72,7 +72,10 @@ jobs:
         if [ -f optional_requirements.txt ]; then pip install -r optional_requirements.txt; fi
         pip install -e .
     - name: Test with pytest
-      run: pytest
+      env:
+        TNG_API_KEY: ${{ secrets.TNG_API_KEY }}
+      run: |
+        pytest
 
   tests-and-coverage:
     needs: lint
@@ -99,7 +102,10 @@ jobs:
         if [ -f optional_requirements.txt ]; then pip install -r optional_requirements.txt; fi
         pip install -e .
     - name: Test with pytest including coverage report
-      run: pytest --cov=./ --cov-report=xml
+      env:
+        TNG_API_KEY: ${{ secrets.TNG_API_KEY }}
+      run: |
+        pytest --cov=./ --cov-report=xml
     - name: Upload coverage to Codecov
       uses: codecov/codecov-action@v4
       env:
diff --git a/tests/test_examples.py b/tests/test_examples.py
new file mode 100644
index 0000000..f419b3c
--- /dev/null
+++ b/tests/test_examples.py
@@ -0,0 +1,52 @@
+import os
+from martini.__version__ import __version__
+import pytest
+
+
+class TestExamples:
+    def test_source_injection_example_version(self):
+        """
+        Check that the source injection notebook installs current version of martini.
+        """
+        with open("examples/martini_source_injection.ipynb") as f:
+            nb_content = f.read()
+        print(nb_content)
+        assert (
+            '"!{sys.executable} -m pip install \\"astromartini[tngsource]=='
+            + __version__
+            + '\\"'
+            in nb_content
+        )
+
+    @pytest.mark.skipif(
+        not os.path.isfile("examples/NGC_2841_NA_CUBE_THINGS.FITS"),
+        reason="sample data not locally available",
+    )
+    def test_source_injection_example(self):
+        pytest.importorskip(
+            "nbmake", reason="nbmake (optional dependency) not available"
+        )
+        from nbmake.nb_run import NotebookRun
+        from nbmake.pytest_items import NotebookFailedException
+        import pathlib
+
+        files_to_cleanup = (
+            "examples/NGC_2841_AND_TNG50-1_99_737963.FITS",
+            "examples/sourceinjectiondemo.fits",
+            "examples/martini-cutout-grnr-TNG50-1-99-737963.npy",
+            "examples/martini-cutout-TNG50-1-99-2122.hdf5",
+        )
+
+        assert os.path.isfile("examples/NGC_2841_NA_CUBE_THINGS.FITS")
+        for file_to_cleanup in files_to_cleanup:
+            if os.path.isfile(file_to_cleanup):
+                os.remove(file_to_cleanup)
+        nbr = NotebookRun(pathlib.Path("examples/martini_source_injection.ipynb"), 3600)
+        try:
+            result = nbr.execute()
+            if result.error is not None:
+                raise NotebookFailedException(result)
+        finally:
+            for file_to_cleanup in files_to_cleanup:
+                if os.path.isfile(file_to_cleanup):
+                    os.remove(file_to_cleanup)
diff --git a/tests/test_sources.py b/tests/test_sources.py
index e19b722..b4400fe 100644
--- a/tests/test_sources.py
+++ b/tests/test_sources.py
@@ -8,6 +8,7 @@
 from martini.sources._cartesian_translation import translate, translate_d
 from martini.sources._L_align import L_align
 from astropy.coordinates import CartesianRepresentation, CartesianDifferential
+from martini.__version__ import __version__
 
 
 class TestSourceUtilities:
@@ -496,43 +497,167 @@ def test_preview_save(self, s, ext):
                 os.remove(testfile)
 
 
-class TestSOSource:
-    @pytest.mark.xfail
-    def test_stuff(self):
-        raise NotImplementedError
+class TestEagleSource:
+    def test_notebook_version(self):
+        """
+        Check that notebook installs current verison of martini.
+        """
+        with open("examples/martini_eagle.ipynb") as f:
+            nb_content = f.read()
+        assert (
+            '"!{sys.executable} -m pip install \\"astromartini[eaglesource]=='
+            + __version__
+            + '\\""'
+            in nb_content
+        )
 
+    @pytest.mark.skipif(
+        not os.path.isdir("examples/RefL0025N0376"),
+        reason="sample data not locally available",
+    )
+    def test_eagle_notebook(self):
+        """
+        Check that the EAGLE example notebook runs.
+        """
+        pytest.importorskip(
+            "nbmake", reason="nbmake (optional dependency) not available"
+        )
+        from nbmake.nb_run import NotebookRun
+        from nbmake.pytest_items import NotebookFailedException
+        import pathlib
 
-class TestSWIFTGalaxySource:
-    @pytest.mark.xfail
-    def test_stuff(self):
-        raise NotImplementedError
+        assert os.path.isfile("examples/eagle_db.password")
+        nbr = NotebookRun(pathlib.Path("examples/martini_eagle.ipynb"), 3600)
+        try:
+            result = nbr.execute()
+            if result.error is not None:
+                raise NotebookFailedException(result)
+        finally:
+            files_to_cleanup = (
+                "examples/eagle_martini_demo_beam.fits",
+                "examples/eagle_martini_demo.fits",
+                "examples/eagle_martini_demo.hdf5",
+            )
+            for file_to_cleanup in files_to_cleanup:
+                if os.path.isfile(file_to_cleanup):
+                    os.remove(file_to_cleanup)
 
 
-class TestColibreSource:
-    @pytest.mark.xfail
-    def test_stuff(self):
-        raise NotImplementedError
+class TestSimbaSource:
+    def test_notebook_version(self):
+        """
+        Check that notebook installs current verison of martini.
+        """
+        with open("examples/martini_simba.ipynb") as f:
+            nb_content = f.read()
+        assert (
+            '"!{sys.executable} -m pip install \\"astromartini[simbasource]=='
+            + __version__
+            + '\\""'
+            in nb_content
+        )
 
+    @pytest.mark.skipif(
+        not os.path.isfile("examples/m25n512_151.hdf5")
+        or not os.path.isfile("examples/snap_m25n512_151.hdf5"),
+        reason="sample data not locally available",
+    )
+    def test_simba_notebook(self):
+        """
+        Check that the Simba example notebook runs.
+        """
+        pytest.importorskip(
+            "nbmake", reason="nbmake (optional dependency) not available"
+        )
+        from nbmake.nb_run import NotebookRun
+        from nbmake.pytest_items import NotebookFailedException
+        import pathlib
 
-class TestEagleSource:
+        nbr = NotebookRun(pathlib.Path("examples/martini_simba.ipynb"), 3600)
+        try:
+            result = nbr.execute()
+            if result.error is not None:
+                raise NotebookFailedException(result)
+        finally:
+            files_to_cleanup = (
+                "examples/simba_martini_demo_beam.fits",
+                "examples/simba_martini_demo.fits",
+                "examples/simba_martini_demo.hdf5",
+            )
+            for file_to_cleanup in files_to_cleanup:
+                if os.path.isfile(file_to_cleanup):
+                    os.remove(file_to_cleanup)
+
+
+class TestTNGSource:
+    def test_notebook_version(self):
+        """
+        Check that notebook installs current verison of martini.
+        """
+        with open("examples/martini_TNG.ipynb") as f:
+            nb_content = f.read()
+        assert (
+            '"!{sys.executable} -m pip install \\"astromartini[tngsource]=='
+            + __version__
+            + '\\""'
+            in nb_content
+        )
+
+    def test_tng_notebook(self):
+        """
+        Check that the TNG example notebook runs.
+        """
+        pytest.importorskip(
+            "nbmake", reason="nbmake (optional dependency) not available"
+        )
+        from nbmake.nb_run import NotebookRun
+        from nbmake.pytest_items import NotebookFailedException
+        import pathlib
+
+        assert "TNG_API_KEY" in os.environ or os.path.isfile("examples/tng_api.key")
+
+        if os.path.isfile("examples/martini-cutout-grnr-TNG100-1-99-400547.npy"):
+            os.remove("examples/martini-cutout-grnr-TNG100-1-99-400547.npy")
+        if os.path.isfile("examples/martini-cutout-TNG100-1-99-517.hdf5"):
+            os.remove("examples/martini-cutout-TNG100-1-99-517.hdf5")
+
+        nbr = NotebookRun(pathlib.Path("examples/martini_TNG.ipynb"), 3600)
+        try:
+            result = nbr.execute()
+            if result.error is not None:
+                raise NotebookFailedException(result)
+        finally:
+            files_to_cleanup = (
+                "examples/tng_martini_demo_beam.fits",
+                "examples/tng_martini_demo.fits",
+                "examples/tng_martini_demo.hdf5",
+                "examples/martini-cutout-TNG100-1-99-517.hdf5",
+                "examples/martini-cutout-grnr-TNG100-1-99-400547.npy",
+            )
+            for file_to_cleanup in files_to_cleanup:
+                if os.path.isfile(file_to_cleanup):
+                    os.remove(file_to_cleanup)
+
+
+class TestMagneticumSource:
     @pytest.mark.xfail
     def test_stuff(self):
         raise NotImplementedError
 
 
-class TestMagneticumSource:
+class TestSOSource:
     @pytest.mark.xfail
     def test_stuff(self):
         raise NotImplementedError
 
 
-class TestSimbaSource:
+class TestSWIFTGalaxySource:
     @pytest.mark.xfail
     def test_stuff(self):
         raise NotImplementedError
 
 
-class TestTNGSource:
+class TestColibreSource:
     @pytest.mark.xfail
     def test_stuff(self):
         raise NotImplementedError
diff --git a/tests/test_version.py b/tests/test_version.py
new file mode 100644
index 0000000..05515b8
--- /dev/null
+++ b/tests/test_version.py
@@ -0,0 +1,19 @@
+from martini.__version__ import __version__
+
+
+class TestVersion:
+    def test_pyproject_version(self):
+        """
+        Check that pyproject version matches version file.
+        """
+        with open("pyproject.toml") as f:
+            pyproject_content = f.read()
+        assert 'version = "' + __version__ + '"' in pyproject_content
+
+    def test_codemeta_version(self):
+        """
+        Check that codemeta version matches version file.
+        """
+        with open("codemeta.json") as f:
+            codemeta_content = f.read()
+        assert '"version": "' + __version__ + '",' in codemeta_content