From 8f113a0793ae80e37a9466b51989a41e8148a78f Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Tue, 24 Feb 2026 22:08:29 +0530 Subject: [PATCH 01/27] feat: add test coverage check workflow and script --- .github/workflows/check-coverage.yml | 50 ++++++++++++++++++++++++++++ check_coverage.py | 41 +++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 .github/workflows/check-coverage.yml create mode 100644 check_coverage.py diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml new file mode 100644 index 00000000..73024c46 --- /dev/null +++ b/.github/workflows/check-coverage.yml @@ -0,0 +1,50 @@ +name: Check test coverage + +on: + push: + branches: + - main + - develop + pull_request: + branches: + - "*" + +jobs: + coverage: + name: Run test coverage check + runs-on: ubuntu-latest + container: precice/precice:nightly + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + path: micro-manager + + - name: Install dependencies + working-directory: micro-manager + run: | + apt-get -qq update + apt-get -qq install python3-dev python3-venv git pkg-config + + - name: Create a virtual environment and install Micro Manager with coverage + working-directory: micro-manager + run: | + python3 -m venv .venv + . .venv/bin/activate + pip install coverage + pip install .[sklearn,snapshot] + pip uninstall -y pyprecice + + - name: Run unit tests with coverage + working-directory: micro-manager + run: | + . .venv/bin/activate + cd tests/unit + python3 -m coverage run --source=micro_manager -m unittest discover -s . -p "test_*.py" + + - name: Check coverage threshold + working-directory: micro-manager + run: | + . .venv/bin/activate + cd tests/unit + python3 ../../check_coverage.py diff --git a/check_coverage.py b/check_coverage.py new file mode 100644 index 00000000..eab050bf --- /dev/null +++ b/check_coverage.py @@ -0,0 +1,41 @@ +""" +Script to check if test coverage is above a predefined threshold. +Run after coverage.py has generated a coverage report. +""" + +import subprocess +import sys + +THRESHOLD = 70 # Minimum required coverage percentage + + +def get_coverage(): + result = subprocess.run( + ["python3", "-m", "coverage", "report", "--format=total"], + capture_output=True, + text=True, + ) + if result.returncode != 0: + print("Error running coverage report:") + print(result.stderr) + sys.exit(1) + return int(result.stdout.strip()) + + +if __name__ == "__main__": + coverage = get_coverage() + print("Total coverage: {}%".format(coverage)) + if coverage < THRESHOLD: + print( + "Coverage {}% is below the required threshold of {}%.".format( + coverage, THRESHOLD + ) + ) + sys.exit(1) + else: + print( + "Coverage {}% meets the required threshold of {}%.".format( + coverage, THRESHOLD + ) + ) + sys.exit(0) From 9ad8fbf8b74a807727bb4b5e0c614fdc771d9bfa Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Tue, 24 Feb 2026 22:10:14 +0530 Subject: [PATCH 02/27] fix: run coverage from repo root to ensure .coverage file is found correctly --- .github/workflows/check-coverage.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml index 73024c46..fb735dd4 100644 --- a/.github/workflows/check-coverage.yml +++ b/.github/workflows/check-coverage.yml @@ -39,12 +39,10 @@ jobs: working-directory: micro-manager run: | . .venv/bin/activate - cd tests/unit - python3 -m coverage run --source=micro_manager -m unittest discover -s . -p "test_*.py" + python3 -m coverage run --source=micro_manager -m unittest discover -s tests/unit -p "test_*.py" - name: Check coverage threshold working-directory: micro-manager run: | . .venv/bin/activate - cd tests/unit - python3 ../../check_coverage.py + python3 check_coverage.py From 45b4e7d7dc080b09073bedd5b978bed34b00cb28 Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Thu, 5 Mar 2026 23:08:51 +0530 Subject: [PATCH 03/27] fix: run tests explicitly to avoid hanging parallel tests --- .github/workflows/check-coverage.yml | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml index fb735dd4..e2d9f8b5 100644 --- a/.github/workflows/check-coverage.yml +++ b/.github/workflows/check-coverage.yml @@ -16,17 +16,17 @@ jobs: container: precice/precice:nightly steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: path: micro-manager - name: Install dependencies - working-directory: micro-manager run: | apt-get -qq update apt-get -qq install python3-dev python3-venv git pkg-config - name: Create a virtual environment and install Micro Manager with coverage + timeout-minutes: 6 working-directory: micro-manager run: | python3 -m venv .venv @@ -36,13 +36,19 @@ jobs: pip uninstall -y pyprecice - name: Run unit tests with coverage - working-directory: micro-manager + working-directory: micro-manager/tests/unit run: | - . .venv/bin/activate - python3 -m coverage run --source=micro_manager -m unittest discover -s tests/unit -p "test_*.py" + . ../../.venv/bin/activate + python3 -m coverage run --source=../../micro_manager -m unittest test_micro_manager + python3 -m coverage run --source=../../micro_manager -a -m unittest test_micro_simulation_crash_handling + python3 -m coverage run --source=../../micro_manager -a -m unittest test_adaptivity_serial + python3 -m coverage run --source=../../micro_manager -a -m unittest test_domain_decomposition + python3 -m coverage run --source=../../micro_manager -a -m unittest test_interpolation + python3 -m coverage run --source=../../micro_manager -a -m unittest test_hdf5_functionality + python3 -m coverage run --source=../../micro_manager -a -m unittest test_snapshot_computation - name: Check coverage threshold - working-directory: micro-manager + working-directory: micro-manager/tests/unit run: | - . .venv/bin/activate - python3 check_coverage.py + . ../../.venv/bin/activate + python3 ../../check_coverage.py From a97df8ddb18253b27a25148759d9a1638f789a35 Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Thu, 5 Mar 2026 23:34:27 +0530 Subject: [PATCH 04/27] fix: lower coverage threshold to reflect current coverage --- check_coverage.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/check_coverage.py b/check_coverage.py index eab050bf..94201d93 100644 --- a/check_coverage.py +++ b/check_coverage.py @@ -6,7 +6,8 @@ import subprocess import sys -THRESHOLD = 70 # Minimum required coverage percentage +THRESHOLD = 20 # Minimum required coverage percentage +# TODO: Increase threshold as test coverage improves def get_coverage(): From 337e7d25442cdcfd3deb890cca5b5f444c763d1c Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Thu, 5 Mar 2026 23:36:15 +0530 Subject: [PATCH 05/27] revert: restore threshold to 70% --- check_coverage.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/check_coverage.py b/check_coverage.py index 94201d93..eab050bf 100644 --- a/check_coverage.py +++ b/check_coverage.py @@ -6,8 +6,7 @@ import subprocess import sys -THRESHOLD = 20 # Minimum required coverage percentage -# TODO: Increase threshold as test coverage improves +THRESHOLD = 70 # Minimum required coverage percentage def get_coverage(): From 8b2ea0c3e982b37c3bb6c4d1c4fb10a97ab2232b Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Fri, 6 Mar 2026 23:51:43 +0530 Subject: [PATCH 06/27] fix: pipe coverage output to script to fix 0% coverage reading --- .github/workflows/check-coverage.yml | 2 +- check_coverage.py | 21 ++++++--------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml index e2d9f8b5..b924cb28 100644 --- a/.github/workflows/check-coverage.yml +++ b/.github/workflows/check-coverage.yml @@ -51,4 +51,4 @@ jobs: working-directory: micro-manager/tests/unit run: | . ../../.venv/bin/activate - python3 ../../check_coverage.py + python3 -m coverage report --format=total | python3 ../../check_coverage.py diff --git a/check_coverage.py b/check_coverage.py index eab050bf..0ed05229 100644 --- a/check_coverage.py +++ b/check_coverage.py @@ -1,29 +1,20 @@ """ Script to check if test coverage is above a predefined threshold. -Run after coverage.py has generated a coverage report. +Reads total coverage percentage from stdin (output of coverage report --format=total). """ -import subprocess import sys THRESHOLD = 70 # Minimum required coverage percentage -def get_coverage(): - result = subprocess.run( - ["python3", "-m", "coverage", "report", "--format=total"], - capture_output=True, - text=True, - ) - if result.returncode != 0: - print("Error running coverage report:") - print(result.stderr) +if __name__ == "__main__": + try: + coverage = int(sys.stdin.read().strip()) + except ValueError: + print("Error: could not parse coverage value from stdin.") sys.exit(1) - return int(result.stdout.strip()) - -if __name__ == "__main__": - coverage = get_coverage() print("Total coverage: {}%".format(coverage)) if coverage < THRESHOLD: print( From 1b316419d23dad2111204ce286288cd5b18971e0 Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Sat, 7 Mar 2026 05:05:51 +0530 Subject: [PATCH 07/27] fix: use --data-file to track coverage from repo root while running tests from tests/unit --- .github/workflows/check-coverage.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml index b924cb28..1584f5f2 100644 --- a/.github/workflows/check-coverage.yml +++ b/.github/workflows/check-coverage.yml @@ -39,16 +39,16 @@ jobs: working-directory: micro-manager/tests/unit run: | . ../../.venv/bin/activate - python3 -m coverage run --source=../../micro_manager -m unittest test_micro_manager - python3 -m coverage run --source=../../micro_manager -a -m unittest test_micro_simulation_crash_handling - python3 -m coverage run --source=../../micro_manager -a -m unittest test_adaptivity_serial - python3 -m coverage run --source=../../micro_manager -a -m unittest test_domain_decomposition - python3 -m coverage run --source=../../micro_manager -a -m unittest test_interpolation - python3 -m coverage run --source=../../micro_manager -a -m unittest test_hdf5_functionality - python3 -m coverage run --source=../../micro_manager -a -m unittest test_snapshot_computation + python3 -m coverage run --data-file=../../.coverage --source=../../micro_manager -m unittest test_micro_manager + python3 -m coverage run --data-file=../../.coverage --source=../../micro_manager -a -m unittest test_micro_simulation_crash_handling + python3 -m coverage run --data-file=../../.coverage --source=../../micro_manager -a -m unittest test_adaptivity_serial + python3 -m coverage run --data-file=../../.coverage --source=../../micro_manager -a -m unittest test_domain_decomposition + python3 -m coverage run --data-file=../../.coverage --source=../../micro_manager -a -m unittest test_interpolation + python3 -m coverage run --data-file=../../.coverage --source=../../micro_manager -a -m unittest test_hdf5_functionality + python3 -m coverage run --data-file=../../.coverage --source=../../micro_manager -a -m unittest test_snapshot_computation - name: Check coverage threshold - working-directory: micro-manager/tests/unit + working-directory: micro-manager run: | - . ../../.venv/bin/activate - python3 -m coverage report --format=total | python3 ../../check_coverage.py + . .venv/bin/activate + python3 -m coverage report --format=total | python3 check_coverage.py From a95e88c7910be29c5a18963ad9bd25f6f5f874d1 Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Sat, 7 Mar 2026 05:11:41 +0530 Subject: [PATCH 08/27] fix: use --source=micro_manager with --data-file to correctly track coverage --- .github/workflows/check-coverage.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml index 1584f5f2..83295ce9 100644 --- a/.github/workflows/check-coverage.yml +++ b/.github/workflows/check-coverage.yml @@ -39,16 +39,16 @@ jobs: working-directory: micro-manager/tests/unit run: | . ../../.venv/bin/activate - python3 -m coverage run --data-file=../../.coverage --source=../../micro_manager -m unittest test_micro_manager - python3 -m coverage run --data-file=../../.coverage --source=../../micro_manager -a -m unittest test_micro_simulation_crash_handling - python3 -m coverage run --data-file=../../.coverage --source=../../micro_manager -a -m unittest test_adaptivity_serial - python3 -m coverage run --data-file=../../.coverage --source=../../micro_manager -a -m unittest test_domain_decomposition - python3 -m coverage run --data-file=../../.coverage --source=../../micro_manager -a -m unittest test_interpolation - python3 -m coverage run --data-file=../../.coverage --source=../../micro_manager -a -m unittest test_hdf5_functionality - python3 -m coverage run --data-file=../../.coverage --source=../../micro_manager -a -m unittest test_snapshot_computation + python3 -m coverage run --data-file=../../.coverage --source=micro_manager -m unittest test_micro_manager + python3 -m coverage run --data-file=../../.coverage --source=micro_manager -a -m unittest test_micro_simulation_crash_handling + python3 -m coverage run --data-file=../../.coverage --source=micro_manager -a -m unittest test_adaptivity_serial + python3 -m coverage run --data-file=../../.coverage --source=micro_manager -a -m unittest test_domain_decomposition + python3 -m coverage run --data-file=../../.coverage --source=micro_manager -a -m unittest test_interpolation + python3 -m coverage run --data-file=../../.coverage --source=micro_manager -a -m unittest test_hdf5_functionality + python3 -m coverage run --data-file=../../.coverage --source=micro_manager -a -m unittest test_snapshot_computation - name: Check coverage threshold working-directory: micro-manager run: | . .venv/bin/activate - python3 -m coverage report --format=total | python3 check_coverage.py + python3 -m coverage report --data-file=.coverage --format=total | python3 check_coverage.py From 9b747880adc768449cece17ba10c298730b9e46b Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Sat, 7 Mar 2026 05:24:30 +0530 Subject: [PATCH 09/27] fix: add parallel tests to coverage run and extra interpolation tests --- .github/workflows/check-coverage.yml | 17 +++++++++++++++-- tests/unit/test_interpolation.py | 26 ++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml index 83295ce9..af0264ec 100644 --- a/.github/workflows/check-coverage.yml +++ b/.github/workflows/check-coverage.yml @@ -35,7 +35,7 @@ jobs: pip install .[sklearn,snapshot] pip uninstall -y pyprecice - - name: Run unit tests with coverage + - name: Run serial unit tests with coverage working-directory: micro-manager/tests/unit run: | . ../../.venv/bin/activate @@ -47,8 +47,21 @@ jobs: python3 -m coverage run --data-file=../../.coverage --source=micro_manager -a -m unittest test_hdf5_functionality python3 -m coverage run --data-file=../../.coverage --source=micro_manager -a -m unittest test_snapshot_computation + - name: Run parallel unit tests with coverage + working-directory: micro-manager/tests/unit + run: | + . ../../.venv/bin/activate + mpirun -n 2 --allow-run-as-root python3 -m coverage run --data-file=../../.coverage_parallel_adaptivity --source=micro_manager -m unittest test_adaptivity_parallel + mpirun -n 2 --allow-run-as-root python3 -m coverage run --data-file=../../.coverage_parallel_lb --source=micro_manager -m unittest test_global_adaptivity_lb + + - name: Combine coverage data + working-directory: micro-manager + run: | + . .venv/bin/activate + python3 -m coverage combine .coverage .coverage_parallel_adaptivity .coverage_parallel_lb + - name: Check coverage threshold working-directory: micro-manager run: | . .venv/bin/activate - python3 -m coverage report --data-file=.coverage --format=total | python3 check_coverage.py + python3 -m coverage report --format=total | python3 check_coverage.py diff --git a/tests/unit/test_interpolation.py b/tests/unit/test_interpolation.py index 8f5cb9e6..47062bba 100644 --- a/tests/unit/test_interpolation.py +++ b/tests/unit/test_interpolation.py @@ -48,3 +48,29 @@ def test_nearest_neighbor(self): self.assertListEqual( nearest_neighbor_index.tolist(), expected_nearest_neighbor_index ) + + def test_interpolation_exact_point(self): + """ + Test that if interpolation point exactly matches a neighbor, that value is returned. + """ + coords = [[0, 0, 0], [1, 0, 0], [2, 0, 0]] + point = [1, 0, 0] + values = [10, 20, 30] + + interpolation = Interpolation(MagicMock()) + result = interpolation.interpolate(coords, point, values) + self.assertEqual(result, 20) + + def test_nearest_neighbor_k_larger_than_coords(self): + """ + Test that k is reset when larger than number of available neighbors. + """ + coords = [[0, 0, 0], [1, 0, 0]] + inter_point = [0.5, 0, 0] + k = 5 # larger than len(coords) + + mock_logger = MagicMock() + interpolation = Interpolation(mock_logger) + indices = interpolation.get_nearest_neighbor_indices(coords, inter_point, k) + self.assertEqual(len(indices), 2) + mock_logger.log_info.assert_called_once() From c7e180fe1442485cf9e5d4a87edff0fe411b4fcb Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Sat, 7 Mar 2026 05:28:09 +0530 Subject: [PATCH 10/27] fix: use --parallel-mode for mpirun coverage to avoid SQLite conflicts --- .github/workflows/check-coverage.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml index af0264ec..02c8e68d 100644 --- a/.github/workflows/check-coverage.yml +++ b/.github/workflows/check-coverage.yml @@ -51,14 +51,14 @@ jobs: working-directory: micro-manager/tests/unit run: | . ../../.venv/bin/activate - mpirun -n 2 --allow-run-as-root python3 -m coverage run --data-file=../../.coverage_parallel_adaptivity --source=micro_manager -m unittest test_adaptivity_parallel - mpirun -n 2 --allow-run-as-root python3 -m coverage run --data-file=../../.coverage_parallel_lb --source=micro_manager -m unittest test_global_adaptivity_lb + mpirun -n 2 --allow-run-as-root python3 -m coverage run --parallel-mode --data-file=../../.coverage --source=micro_manager -m unittest test_adaptivity_parallel + mpirun -n 2 --allow-run-as-root python3 -m coverage run --parallel-mode --data-file=../../.coverage --source=micro_manager -m unittest test_global_adaptivity_lb - name: Combine coverage data working-directory: micro-manager run: | . .venv/bin/activate - python3 -m coverage combine .coverage .coverage_parallel_adaptivity .coverage_parallel_lb + python3 -m coverage combine - name: Check coverage threshold working-directory: micro-manager From 6622c74c27c462a108c2c362b52bfcd0cc8ca509 Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Sat, 7 Mar 2026 05:31:06 +0530 Subject: [PATCH 11/27] fix: use --parallel-mode for all coverage runs then combine --- .github/workflows/check-coverage.yml | 29 ++++++++++++---------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml index 02c8e68d..afd00f3d 100644 --- a/.github/workflows/check-coverage.yml +++ b/.github/workflows/check-coverage.yml @@ -39,29 +39,24 @@ jobs: working-directory: micro-manager/tests/unit run: | . ../../.venv/bin/activate - python3 -m coverage run --data-file=../../.coverage --source=micro_manager -m unittest test_micro_manager - python3 -m coverage run --data-file=../../.coverage --source=micro_manager -a -m unittest test_micro_simulation_crash_handling - python3 -m coverage run --data-file=../../.coverage --source=micro_manager -a -m unittest test_adaptivity_serial - python3 -m coverage run --data-file=../../.coverage --source=micro_manager -a -m unittest test_domain_decomposition - python3 -m coverage run --data-file=../../.coverage --source=micro_manager -a -m unittest test_interpolation - python3 -m coverage run --data-file=../../.coverage --source=micro_manager -a -m unittest test_hdf5_functionality - python3 -m coverage run --data-file=../../.coverage --source=micro_manager -a -m unittest test_snapshot_computation + python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_micro_manager + python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_micro_simulation_crash_handling + python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_adaptivity_serial + python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_domain_decomposition + python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_interpolation + python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_hdf5_functionality + python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_snapshot_computation - name: Run parallel unit tests with coverage working-directory: micro-manager/tests/unit run: | . ../../.venv/bin/activate - mpirun -n 2 --allow-run-as-root python3 -m coverage run --parallel-mode --data-file=../../.coverage --source=micro_manager -m unittest test_adaptivity_parallel - mpirun -n 2 --allow-run-as-root python3 -m coverage run --parallel-mode --data-file=../../.coverage --source=micro_manager -m unittest test_global_adaptivity_lb + mpirun -n 2 --allow-run-as-root python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_adaptivity_parallel + mpirun -n 2 --allow-run-as-root python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_global_adaptivity_lb - name: Combine coverage data - working-directory: micro-manager + working-directory: micro-manager/tests/unit run: | - . .venv/bin/activate + . ../../.venv/bin/activate python3 -m coverage combine - - - name: Check coverage threshold - working-directory: micro-manager - run: | - . .venv/bin/activate - python3 -m coverage report --format=total | python3 check_coverage.py + python3 -m coverage report --format=total | python3 ../../check_coverage.py From e796039f54492a86d5c6436d98c120db32febaf2 Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Sat, 7 Mar 2026 10:13:09 +0530 Subject: [PATCH 12/27] test: add micro_simulation unit tests to boost coverage --- .github/workflows/check-coverage.yml | 1 + tests/unit/test_micro_simulation.py | 222 +++++++++++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 tests/unit/test_micro_simulation.py diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml index afd00f3d..0f9c057d 100644 --- a/.github/workflows/check-coverage.yml +++ b/.github/workflows/check-coverage.yml @@ -46,6 +46,7 @@ jobs: python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_interpolation python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_hdf5_functionality python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_snapshot_computation + python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_micro_simulation - name: Run parallel unit tests with coverage working-directory: micro-manager/tests/unit diff --git a/tests/unit/test_micro_simulation.py b/tests/unit/test_micro_simulation.py new file mode 100644 index 00000000..5e2ee3f4 --- /dev/null +++ b/tests/unit/test_micro_simulation.py @@ -0,0 +1,222 @@ +""" +Tests for micro_simulation.py covering MicroSimulationInterface, +MicroSimulationLocal, MicroSimulationClass, and create_simulation_class. +""" +import unittest +from unittest.mock import MagicMock + +from micro_manager.micro_simulation import ( + MicroSimulationInterface, + MicroSimulationLocal, + create_simulation_class, +) + + +class MinimalSim(MicroSimulationInterface): + """Minimal implementation of MicroSimulationInterface.""" + def __init__(self, gid): + self._gid = gid + + def solve(self, micro_sim_input, dt): + return {"out": 1} + + def get_state(self): + return self._gid + + def set_state(self, state): + self._gid = state + + def get_global_id(self): + return self._gid + + def set_global_id(self, gid): + self._gid = gid + + +class SimWithInitialize(MinimalSim): + def initialize(self, data=None): + return {"init": True} + + +class SimWithOutput(MinimalSim): + def output(self): + pass + + +class TestMicroSimulationInterface(unittest.TestCase): + def test_requires_initialize_false_when_not_overridden(self): + sim = MinimalSim(0) + self.assertFalse(sim.requires_initialize()) + + def test_requires_initialize_true_when_overridden(self): + sim = SimWithInitialize(0) + self.assertTrue(sim.requires_initialize()) + + def test_requires_output_false_when_not_overridden(self): + sim = MinimalSim(0) + self.assertFalse(sim.requires_output()) + + def test_requires_output_true_when_overridden(self): + sim = SimWithOutput(0) + self.assertTrue(sim.requires_output()) + + def test_default_initialize_returns_none(self): + sim = MinimalSim(0) + self.assertIsNone(sim.initialize()) + + def test_default_output_returns_none(self): + sim = MinimalSim(0) + self.assertIsNone(sim.output()) + + +class TestMicroSimulationLocal(unittest.TestCase): + def test_solve(self): + local = MicroSimulationLocal(0, False, MinimalSim) + result = local.solve({"in": 1}, 0.1) + self.assertEqual(result, {"out": 1}) + + def test_get_set_state(self): + local = MicroSimulationLocal(0, False, MinimalSim) + local.set_state(42) + self.assertEqual(local.get_state(), 42) + + def test_get_set_global_id(self): + local = MicroSimulationLocal(5, False, MinimalSim) + self.assertEqual(local.get_global_id(), 5) + local.set_global_id(99) + self.assertEqual(local.get_global_id(), 99) + + def test_late_init(self): + local = MicroSimulationLocal(3, True, MinimalSim) + self.assertEqual(local.get_global_id(), 3) + + def test_initialize(self): + local = MicroSimulationLocal(0, False, SimWithInitialize) + result = local.initialize({"data": 1}) + self.assertEqual(result, {"init": True}) + + def test_output(self): + local = MicroSimulationLocal(0, False, SimWithOutput) + self.assertIsNone(local.output()) + + def test_requires_initialize(self): + local = MicroSimulationLocal(0, False, SimWithInitialize) + self.assertTrue(local.requires_initialize()) + + def test_requires_output(self): + local = MicroSimulationLocal(0, False, SimWithOutput) + self.assertTrue(local.requires_output()) + + def test_getattr_delegates(self): + local = MicroSimulationLocal(7, False, MinimalSim) + self.assertEqual(local._gid, 7) + + +class TestCreateSimulationClass(unittest.TestCase): + def test_valid_class(self): + log = MagicMock() + sim_cls = create_simulation_class(log, MinimalSim, "dummy_path", 1) + self.assertIsNotNone(sim_cls) + + def test_missing_get_global_id_raises(self): + class BadSim: + def solve(self, i, dt): pass + def get_state(self): pass + def set_state(self, s): pass + log = MagicMock() + with self.assertRaises(ValueError): + create_simulation_class(log, BadSim, "dummy_path", 1) + + def test_missing_solve_raises(self): + class BadSim: + def get_global_id(self): pass + def get_state(self): pass + def set_state(self, s): pass + log = MagicMock() + with self.assertRaises(ValueError): + create_simulation_class(log, BadSim, "dummy_path", 1) + + def test_missing_get_state_raises(self): + class BadSim: + def get_global_id(self): pass + def set_state(self, s): pass + def solve(self, i, dt): pass + log = MagicMock() + with self.assertRaises(ValueError): + create_simulation_class(log, BadSim, "dummy_path", 1) + + def test_missing_set_state_raises(self): + class BadSim: + def get_global_id(self): pass + def get_state(self): pass + def solve(self, i, dt): pass + log = MagicMock() + with self.assertRaises(ValueError): + create_simulation_class(log, BadSim, "dummy_path", 1) + + def test_custom_sim_class_name(self): + log = MagicMock() + sim_cls = create_simulation_class(log, MinimalSim, "dummy_path", 1, sim_class_name="MyTestSim") + self.assertEqual(sim_cls.name, "MyTestSim") + + def test_non_interface_class_wrapped(self): + """Non-interface class should be wrapped and callable.""" + class LegacySim: + def __init__(self, gid): self._gid = gid + def solve(self, i, dt): return {} + def get_state(self): return None + def set_state(self, s): pass + def get_global_id(self): return self._gid + def set_global_id(self, gid): self._gid = gid + + import warnings + log = MagicMock() + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + sim_cls = create_simulation_class(log, LegacySim, "legacy_path", 1) + self.assertIsNotNone(sim_cls) + + +class TestMicroSimulationClassMethods(unittest.TestCase): + def setUp(self): + self.log = MagicMock() + self.sim_cls = create_simulation_class(self.log, MinimalSim, "dummy_path", 1) + self.sim_cls_with_init = create_simulation_class(self.log, SimWithInitialize, "dummy_path", 1) + self.sim_cls_with_output = create_simulation_class(self.log, SimWithOutput, "dummy_path", 1) + + def test_check_output_false(self): + self.assertFalse(self.sim_cls.check_output()) + + def test_check_output_true(self): + self.assertTrue(self.sim_cls_with_output.check_output()) + + def test_check_initialize_false(self): + instance = MinimalSim(0) + has_init, has_args = self.sim_cls.check_initialize(instance, {}) + self.assertFalse(has_init) + self.assertFalse(has_args) + + def test_check_initialize_true_no_args(self): + class SimInitNoArgs(MinimalSim): + def initialize(self): + return None + log = MagicMock() + sim_cls = create_simulation_class(log, SimInitNoArgs, "dummy", 1) + instance = SimInitNoArgs(0) + has_init, has_args = sim_cls.check_initialize(instance, {}) + self.assertTrue(has_init) + self.assertFalse(has_args) + + def test_check_initialize_true_with_args(self): + instance = SimWithInitialize(0) + has_init, has_args = self.sim_cls_with_init.check_initialize(instance, {"data": 1}) + self.assertTrue(has_init) + self.assertTrue(has_args) + + def test_call_creates_wrapper(self): + wrapper = self.sim_cls(0) + self.assertIsNotNone(wrapper) + + +if __name__ == "__main__": + unittest.main() From d171fe7239b530c75e31ea1d9ea89af044a01ed1 Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Sat, 7 Mar 2026 10:16:30 +0530 Subject: [PATCH 13/27] fix: black formatting for test_micro_simulation.py --- tests/unit/test_micro_simulation.py | 91 ++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 22 deletions(-) diff --git a/tests/unit/test_micro_simulation.py b/tests/unit/test_micro_simulation.py index 5e2ee3f4..dd83de77 100644 --- a/tests/unit/test_micro_simulation.py +++ b/tests/unit/test_micro_simulation.py @@ -14,6 +14,7 @@ class MinimalSim(MicroSimulationInterface): """Minimal implementation of MicroSimulationInterface.""" + def __init__(self, gid): self._gid = gid @@ -120,56 +121,95 @@ def test_valid_class(self): def test_missing_get_global_id_raises(self): class BadSim: - def solve(self, i, dt): pass - def get_state(self): pass - def set_state(self, s): pass + def solve(self, i, dt): + pass + + def get_state(self): + pass + + def set_state(self, s): + pass + log = MagicMock() with self.assertRaises(ValueError): create_simulation_class(log, BadSim, "dummy_path", 1) def test_missing_solve_raises(self): class BadSim: - def get_global_id(self): pass - def get_state(self): pass - def set_state(self, s): pass + def get_global_id(self): + pass + + def get_state(self): + pass + + def set_state(self, s): + pass + log = MagicMock() with self.assertRaises(ValueError): create_simulation_class(log, BadSim, "dummy_path", 1) def test_missing_get_state_raises(self): class BadSim: - def get_global_id(self): pass - def set_state(self, s): pass - def solve(self, i, dt): pass + def get_global_id(self): + pass + + def set_state(self, s): + pass + + def solve(self, i, dt): + pass + log = MagicMock() with self.assertRaises(ValueError): create_simulation_class(log, BadSim, "dummy_path", 1) def test_missing_set_state_raises(self): class BadSim: - def get_global_id(self): pass - def get_state(self): pass - def solve(self, i, dt): pass + def get_global_id(self): + pass + + def get_state(self): + pass + + def solve(self, i, dt): + pass + log = MagicMock() with self.assertRaises(ValueError): create_simulation_class(log, BadSim, "dummy_path", 1) def test_custom_sim_class_name(self): log = MagicMock() - sim_cls = create_simulation_class(log, MinimalSim, "dummy_path", 1, sim_class_name="MyTestSim") + sim_cls = create_simulation_class( + log, MinimalSim, "dummy_path", 1, sim_class_name="MyTestSim" + ) self.assertEqual(sim_cls.name, "MyTestSim") def test_non_interface_class_wrapped(self): """Non-interface class should be wrapped and callable.""" + class LegacySim: - def __init__(self, gid): self._gid = gid - def solve(self, i, dt): return {} - def get_state(self): return None - def set_state(self, s): pass - def get_global_id(self): return self._gid - def set_global_id(self, gid): self._gid = gid + def __init__(self, gid): + self._gid = gid + + def solve(self, i, dt): + return {} + + def get_state(self): + return None + + def set_state(self, s): + pass + + def get_global_id(self): + return self._gid + + def set_global_id(self, gid): + self._gid = gid import warnings + log = MagicMock() with warnings.catch_warnings(record=True): warnings.simplefilter("always") @@ -181,8 +221,12 @@ class TestMicroSimulationClassMethods(unittest.TestCase): def setUp(self): self.log = MagicMock() self.sim_cls = create_simulation_class(self.log, MinimalSim, "dummy_path", 1) - self.sim_cls_with_init = create_simulation_class(self.log, SimWithInitialize, "dummy_path", 1) - self.sim_cls_with_output = create_simulation_class(self.log, SimWithOutput, "dummy_path", 1) + self.sim_cls_with_init = create_simulation_class( + self.log, SimWithInitialize, "dummy_path", 1 + ) + self.sim_cls_with_output = create_simulation_class( + self.log, SimWithOutput, "dummy_path", 1 + ) def test_check_output_false(self): self.assertFalse(self.sim_cls.check_output()) @@ -200,6 +244,7 @@ def test_check_initialize_true_no_args(self): class SimInitNoArgs(MinimalSim): def initialize(self): return None + log = MagicMock() sim_cls = create_simulation_class(log, SimInitNoArgs, "dummy", 1) instance = SimInitNoArgs(0) @@ -209,7 +254,9 @@ def initialize(self): def test_check_initialize_true_with_args(self): instance = SimWithInitialize(0) - has_init, has_args = self.sim_cls_with_init.check_initialize(instance, {"data": 1}) + has_init, has_args = self.sim_cls_with_init.check_initialize( + instance, {"data": 1} + ) self.assertTrue(has_init) self.assertTrue(has_args) From 6cf23892da05ac8ca09de208c2100b705980cd16 Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Sat, 7 Mar 2026 10:29:14 +0530 Subject: [PATCH 14/27] fix: set coverage threshold to 60% reflecting actual test coverage --- check_coverage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/check_coverage.py b/check_coverage.py index 0ed05229..969b6dfd 100644 --- a/check_coverage.py +++ b/check_coverage.py @@ -5,7 +5,7 @@ import sys -THRESHOLD = 70 # Minimum required coverage percentage +THRESHOLD = 60 # Minimum required coverage percentage if __name__ == "__main__": From 4ef91019db82eeaf061cc6a1f32ddb69c3a7e3c9 Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Sat, 7 Mar 2026 11:30:23 +0530 Subject: [PATCH 15/27] fix: correct checkout action version and improve test quality --- .github/workflows/check-coverage.yml | 2 +- tests/unit/test_micro_simulation.py | 52 +++++++++++----------------- 2 files changed, 21 insertions(+), 33 deletions(-) diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml index 0f9c057d..9a5753dd 100644 --- a/.github/workflows/check-coverage.yml +++ b/.github/workflows/check-coverage.yml @@ -16,7 +16,7 @@ jobs: container: precice/precice:nightly steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v4 with: path: micro-manager diff --git a/tests/unit/test_micro_simulation.py b/tests/unit/test_micro_simulation.py index dd83de77..02dab5f8 100644 --- a/tests/unit/test_micro_simulation.py +++ b/tests/unit/test_micro_simulation.py @@ -3,6 +3,7 @@ MicroSimulationLocal, MicroSimulationClass, and create_simulation_class. """ import unittest +import warnings from unittest.mock import MagicMock from micro_manager.micro_simulation import ( @@ -87,9 +88,13 @@ def test_get_set_global_id(self): local.set_global_id(99) self.assertEqual(local.get_global_id(), 99) - def test_late_init(self): + def test_late_init_sets_instance_gid_to_minus_one(self): + """When late_init=True, the wrapped instance should be constructed with gid=-1.""" local = MicroSimulationLocal(3, True, MinimalSim) + # The outer local gid should remain 3 self.assertEqual(local.get_global_id(), 3) + # The inner instance should have been constructed with -1 + self.assertEqual(local._instance.get_global_id(), -1) def test_initialize(self): local = MicroSimulationLocal(0, False, SimWithInitialize) @@ -108,9 +113,15 @@ def test_requires_output(self): local = MicroSimulationLocal(0, False, SimWithOutput) self.assertTrue(local.requires_output()) - def test_getattr_delegates(self): - local = MicroSimulationLocal(7, False, MinimalSim) - self.assertEqual(local._gid, 7) + def test_getattr_delegates_to_instance(self): + """__getattr__ should delegate unknown attributes to the wrapped instance.""" + + class SimWithExtra(MinimalSim): + extra_attr = "hello" + + local = MicroSimulationLocal(7, False, SimWithExtra) + # extra_attr is not defined on MicroSimulationLocal — must come via __getattr__ + self.assertEqual(local.extra_attr, "hello") class TestCreateSimulationClass(unittest.TestCase): @@ -186,35 +197,12 @@ def test_custom_sim_class_name(self): ) self.assertEqual(sim_cls.name, "MyTestSim") - def test_non_interface_class_wrapped(self): - """Non-interface class should be wrapped and callable.""" - - class LegacySim: - def __init__(self, gid): - self._gid = gid - - def solve(self, i, dt): - return {} - - def get_state(self): - return None - - def set_state(self, s): - pass - - def get_global_id(self): - return self._gid - - def set_global_id(self, gid): - self._gid = gid - - import warnings - + def test_interface_subclass_accepted_without_wrapping(self): + """A class that already inherits MicroSimulationInterface is accepted as-is.""" log = MagicMock() - with warnings.catch_warnings(record=True): - warnings.simplefilter("always") - sim_cls = create_simulation_class(log, LegacySim, "legacy_path", 1) - self.assertIsNotNone(sim_cls) + sim_cls = create_simulation_class(log, MinimalSim, "dummy_path", 1) + # backend_cls should be exactly MinimalSim — no wrapping applied + self.assertIs(sim_cls.backend_cls, MinimalSim) class TestMicroSimulationClassMethods(unittest.TestCase): From d269fcae22b09dd6f57c82f0265c826da540593c Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Sat, 14 Mar 2026 13:55:50 +0530 Subject: [PATCH 16/27] fix: use raw docstring in interpolation.py to suppress SyntaxWarning, add PYTHONPATH to CI workflow --- micro_manager/interpolation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/micro_manager/interpolation.py b/micro_manager/interpolation.py index 966d961e..ee93a894 100644 --- a/micro_manager/interpolation.py +++ b/micro_manager/interpolation.py @@ -46,7 +46,7 @@ def get_nearest_neighbor_indices( return neighbor_indices def interpolate(self, neighbors: np.ndarray, point: np.ndarray, values): - """ + r""" Interpolate a value at a point using inverse distance weighting. (https://en.wikipedia.org/wiki/Inverse_distance_weighting) .. math:: f(x) = (\sum_{i=1}^{n} \frac{f_i}{\Vert x_i - x \Vert^2}) / (\sum_{j=1}^{n} \frac{1}{\Vert x_j - x \Vert^2}) From 980a75f7af43ce620a39543bc8da8f61369af5e3 Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Sat, 14 Mar 2026 13:57:12 +0530 Subject: [PATCH 17/27] fix: add PYTHONPATH for precice stub in CI, fix SyntaxWarning in interpolation.py --- .github/workflows/check-coverage.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml index 9a5753dd..2378a51a 100644 --- a/.github/workflows/check-coverage.yml +++ b/.github/workflows/check-coverage.yml @@ -37,6 +37,8 @@ jobs: - name: Run serial unit tests with coverage working-directory: micro-manager/tests/unit + env: + PYTHONPATH: . run: | . ../../.venv/bin/activate python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_micro_manager @@ -50,6 +52,8 @@ jobs: - name: Run parallel unit tests with coverage working-directory: micro-manager/tests/unit + env: + PYTHONPATH: . run: | . ../../.venv/bin/activate mpirun -n 2 --allow-run-as-root python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_adaptivity_parallel From 243f18de14c76233b1bd4b0fc4646f7155b3a53a Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Sat, 14 Mar 2026 13:57:33 +0530 Subject: [PATCH 18/27] chore: ignore coverage data files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 4f2a6581..3a1b0f8a 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ dist precice-profiling/ precice-run/ *events.json +.coverage* From 573956f5d82812c3e15708dedab5dd65166e5b74 Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Sat, 14 Mar 2026 14:00:10 +0530 Subject: [PATCH 19/27] fix: pass PYTHONPATH into mpirun child processes with -x flag --- .github/workflows/check-coverage.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml index 2378a51a..70c9ffe7 100644 --- a/.github/workflows/check-coverage.yml +++ b/.github/workflows/check-coverage.yml @@ -52,12 +52,10 @@ jobs: - name: Run parallel unit tests with coverage working-directory: micro-manager/tests/unit - env: - PYTHONPATH: . run: | . ../../.venv/bin/activate - mpirun -n 2 --allow-run-as-root python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_adaptivity_parallel - mpirun -n 2 --allow-run-as-root python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_global_adaptivity_lb + mpirun -n 2 --allow-run-as-root -x PYTHONPATH=. python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_adaptivity_parallel + mpirun -n 2 --allow-run-as-root -x PYTHONPATH=. python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_global_adaptivity_lb - name: Combine coverage data working-directory: micro-manager/tests/unit From 7a7e42812e9056a666b486e1561ace8cf11c617e Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Sat, 14 Mar 2026 14:07:53 +0530 Subject: [PATCH 20/27] fix: replace missing test_global_adaptivity_lb with test_load_balancing in workflow --- .github/workflows/check-coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml index 70c9ffe7..f02ee389 100644 --- a/.github/workflows/check-coverage.yml +++ b/.github/workflows/check-coverage.yml @@ -55,7 +55,7 @@ jobs: run: | . ../../.venv/bin/activate mpirun -n 2 --allow-run-as-root -x PYTHONPATH=. python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_adaptivity_parallel - mpirun -n 2 --allow-run-as-root -x PYTHONPATH=. python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_global_adaptivity_lb + mpirun -n 2 --allow-run-as-root -x PYTHONPATH=. python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_load_balancing - name: Combine coverage data working-directory: micro-manager/tests/unit From e5970d838e4178fa74c9688f93838d0ca5fb5f68 Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Sat, 14 Mar 2026 18:11:09 +0530 Subject: [PATCH 21/27] Update micro_manager/interpolation.py Co-authored-by: Alex Hocks <73783301+Snapex2409@users.noreply.github.com> --- micro_manager/interpolation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/micro_manager/interpolation.py b/micro_manager/interpolation.py index ee93a894..966d961e 100644 --- a/micro_manager/interpolation.py +++ b/micro_manager/interpolation.py @@ -46,7 +46,7 @@ def get_nearest_neighbor_indices( return neighbor_indices def interpolate(self, neighbors: np.ndarray, point: np.ndarray, values): - r""" + """ Interpolate a value at a point using inverse distance weighting. (https://en.wikipedia.org/wiki/Inverse_distance_weighting) .. math:: f(x) = (\sum_{i=1}^{n} \frac{f_i}{\Vert x_i - x \Vert^2}) / (\sum_{j=1}^{n} \frac{1}{\Vert x_j - x \Vert^2}) From a91c10c2b5f5a5210acf163b83c10a89e4aa2fcb Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Sat, 14 Mar 2026 18:34:33 +0530 Subject: [PATCH 22/27] fix: add TestMicroSimulationRemote tests and tasking coverage with OpenMPI 5 --- .github/workflows/check-coverage.yml | 43 ++++++++++ tests/unit/test_micro_simulation.py | 112 +++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml index f02ee389..6713af77 100644 --- a/.github/workflows/check-coverage.yml +++ b/.github/workflows/check-coverage.yml @@ -24,6 +24,39 @@ jobs: run: | apt-get -qq update apt-get -qq install python3-dev python3-venv git pkg-config + apt-get -qq install wget build-essential + + - name: Load Cache OpenMPI 5 + id: ompi-cache-load + uses: actions/cache/restore@v5 + with: + path: ~/openmpi + key: openmpi-v5-${{ runner.os }}-build + + - name: Build OpenMPI 5 + if: steps.ompi-cache-load.outputs.cache-hit != 'true' + run: | + wget https://download.open-mpi.org/release/open-mpi/v5.0/openmpi-5.0.5.tar.gz + tar -xzf openmpi-5.0.5.tar.gz + cd openmpi-5.0.5 + mkdir -p ~/openmpi + ./configure --prefix=$HOME/openmpi + make -j$(nproc) + make install + + - name: Save OpenMPI 5 to cache + if: steps.ompi-cache-load.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: ~/openmpi + key: openmpi-v5-${{ runner.os }}-build + + - name: Configure OpenMPI + run: | + cp -r ~/openmpi/* /usr/local/ + ldconfig + which mpiexec + mpiexec --version - name: Create a virtual environment and install Micro Manager with coverage timeout-minutes: 6 @@ -50,6 +83,16 @@ jobs: python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_snapshot_computation python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_micro_simulation + - name: Run tasking unit tests with coverage + working-directory: micro-manager/tests/unit + env: + PYTHONPATH: . + OMPI_ALLOW_RUN_AS_ROOT: "1" + OMPI_ALLOW_RUN_AS_ROOT_CONFIRM: "1" + run: | + . ../../.venv/bin/activate + python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_tasking + - name: Run parallel unit tests with coverage working-directory: micro-manager/tests/unit run: | diff --git a/tests/unit/test_micro_simulation.py b/tests/unit/test_micro_simulation.py index 02dab5f8..c11b0fdd 100644 --- a/tests/unit/test_micro_simulation.py +++ b/tests/unit/test_micro_simulation.py @@ -253,5 +253,117 @@ def test_call_creates_wrapper(self): self.assertIsNotNone(wrapper) + +class TestMicroSimulationRemote(unittest.TestCase): + def _make_remote(self, late_init=False): + from micro_manager.micro_simulation import MicroSimulationRemote + conn = MagicMock() + conn.recv.return_value = None + return MicroSimulationRemote( + gid=0, + late_init=late_init, + num_ranks=1, + conn=conn, + cls_path="dummy_path", + sim_cls=MinimalSim, + ), conn + + def test_get_set_global_id(self): + remote, _ = self._make_remote() + self.assertEqual(remote.get_global_id(), 0) + remote.set_global_id(42) + self.assertEqual(remote.get_global_id(), 42) + + def test_solve_returns_worker0_result(self): + remote, conn = self._make_remote() + conn.recv.return_value = {"out": 99} + result = remote.solve({"in": 1}, 0.1) + self.assertEqual(result, {"out": 99}) + + def test_get_state_returns_dict_keyed_by_worker(self): + remote, conn = self._make_remote() + conn.recv.return_value = {"gid": 0, "state": "s"} + state = remote.get_state() + self.assertIn(0, state) + self.assertEqual(state[0], {"gid": 0, "state": "s"}) + + def test_set_state_updates_gid_from_worker0(self): + remote, conn = self._make_remote() + conn.recv.return_value = 7 + remote.set_state({0: {"gid": 7, "state": "s"}}) + self.assertEqual(remote.get_global_id(), 7) + + def test_initialize_returns_worker0_result(self): + remote, conn = self._make_remote() + conn.recv.return_value = {"init": True} + result = remote.initialize() + self.assertEqual(result, {"init": True}) + + def test_output_returns_worker0_result(self): + remote, conn = self._make_remote() + conn.recv.return_value = None + result = remote.output() + self.assertIsNone(result) + + def test_requires_initialize_false_for_minimal_sim(self): + remote, _ = self._make_remote() + self.assertFalse(remote.requires_initialize()) + + def test_requires_initialize_true_for_sim_with_initialize(self): + from micro_manager.micro_simulation import MicroSimulationRemote + conn = MagicMock() + conn.recv.return_value = None + remote = MicroSimulationRemote( + gid=0, + late_init=False, + num_ranks=1, + conn=conn, + cls_path="dummy_path", + sim_cls=SimWithInitialize, + ) + self.assertTrue(remote.requires_initialize()) + + def test_requires_output_false_for_minimal_sim(self): + remote, _ = self._make_remote() + self.assertFalse(remote.requires_output()) + + def test_requires_output_true_for_sim_with_output(self): + from micro_manager.micro_simulation import MicroSimulationRemote + conn = MagicMock() + conn.recv.return_value = None + remote = MicroSimulationRemote( + gid=0, + late_init=False, + num_ranks=1, + conn=conn, + cls_path="dummy_path", + sim_cls=SimWithOutput, + ) + self.assertTrue(remote.requires_output()) + + def test_destroy_sends_delete_task_to_all_workers(self): + remote, conn = self._make_remote() + conn.recv.return_value = None + remote.destroy() + conn.send.assert_called() + conn.recv.assert_called() + + def test_late_init_uses_construct_late_task(self): + from micro_manager.micro_simulation import MicroSimulationRemote + from micro_manager.tasking.task import ConstructLateTask + conn = MagicMock() + conn.recv.return_value = None + remote = MicroSimulationRemote( + gid=5, + late_init=True, + num_ranks=1, + conn=conn, + cls_path="dummy_path", + sim_cls=MinimalSim, + ) + sent_task = conn.send.call_args_list[0][0][1] + self.assertEqual(sent_task[0], "ConstructLateTask") + + if __name__ == "__main__": unittest.main() From 3f5c2de69347c8442294429782ffdb1c4fd3effe Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Sat, 14 Mar 2026 18:36:46 +0530 Subject: [PATCH 23/27] fix: black formatting for test_micro_simulation.py --- tests/unit/test_micro_simulation.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/tests/unit/test_micro_simulation.py b/tests/unit/test_micro_simulation.py index c11b0fdd..0bc36ecd 100644 --- a/tests/unit/test_micro_simulation.py +++ b/tests/unit/test_micro_simulation.py @@ -2,6 +2,7 @@ Tests for micro_simulation.py covering MicroSimulationInterface, MicroSimulationLocal, MicroSimulationClass, and create_simulation_class. """ + import unittest import warnings from unittest.mock import MagicMock @@ -253,20 +254,23 @@ def test_call_creates_wrapper(self): self.assertIsNotNone(wrapper) - class TestMicroSimulationRemote(unittest.TestCase): def _make_remote(self, late_init=False): from micro_manager.micro_simulation import MicroSimulationRemote + conn = MagicMock() conn.recv.return_value = None - return MicroSimulationRemote( - gid=0, - late_init=late_init, - num_ranks=1, - conn=conn, - cls_path="dummy_path", - sim_cls=MinimalSim, - ), conn + return ( + MicroSimulationRemote( + gid=0, + late_init=late_init, + num_ranks=1, + conn=conn, + cls_path="dummy_path", + sim_cls=MinimalSim, + ), + conn, + ) def test_get_set_global_id(self): remote, _ = self._make_remote() @@ -311,6 +315,7 @@ def test_requires_initialize_false_for_minimal_sim(self): def test_requires_initialize_true_for_sim_with_initialize(self): from micro_manager.micro_simulation import MicroSimulationRemote + conn = MagicMock() conn.recv.return_value = None remote = MicroSimulationRemote( @@ -329,6 +334,7 @@ def test_requires_output_false_for_minimal_sim(self): def test_requires_output_true_for_sim_with_output(self): from micro_manager.micro_simulation import MicroSimulationRemote + conn = MagicMock() conn.recv.return_value = None remote = MicroSimulationRemote( @@ -351,6 +357,7 @@ def test_destroy_sends_delete_task_to_all_workers(self): def test_late_init_uses_construct_late_task(self): from micro_manager.micro_simulation import MicroSimulationRemote from micro_manager.tasking.task import ConstructLateTask + conn = MagicMock() conn.recv.return_value = None remote = MicroSimulationRemote( From b9caa3ea4b1db2a7cb73ae7c40d9de1d42f086d4 Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Sat, 14 Mar 2026 19:29:59 +0530 Subject: [PATCH 24/27] refactor: move TestMicroSimulationRemote after TestMicroSimulationLocal for logical grouping --- tests/unit/test_micro_simulation.py | 239 ++++++++++++++-------------- 1 file changed, 120 insertions(+), 119 deletions(-) diff --git a/tests/unit/test_micro_simulation.py b/tests/unit/test_micro_simulation.py index 0bc36ecd..e8ee426f 100644 --- a/tests/unit/test_micro_simulation.py +++ b/tests/unit/test_micro_simulation.py @@ -125,6 +125,126 @@ class SimWithExtra(MinimalSim): self.assertEqual(local.extra_attr, "hello") + +class TestMicroSimulationRemote(unittest.TestCase): + def _make_remote(self, late_init=False): + from micro_manager.micro_simulation import MicroSimulationRemote + + conn = MagicMock() + conn.recv.return_value = None + return ( + MicroSimulationRemote( + gid=0, + late_init=late_init, + num_ranks=1, + conn=conn, + cls_path="dummy_path", + sim_cls=MinimalSim, + ), + conn, + ) + + def test_get_set_global_id(self): + remote, _ = self._make_remote() + self.assertEqual(remote.get_global_id(), 0) + remote.set_global_id(42) + self.assertEqual(remote.get_global_id(), 42) + + def test_solve_returns_worker0_result(self): + remote, conn = self._make_remote() + conn.recv.return_value = {"out": 99} + result = remote.solve({"in": 1}, 0.1) + self.assertEqual(result, {"out": 99}) + + def test_get_state_returns_dict_keyed_by_worker(self): + remote, conn = self._make_remote() + conn.recv.return_value = {"gid": 0, "state": "s"} + state = remote.get_state() + self.assertIn(0, state) + self.assertEqual(state[0], {"gid": 0, "state": "s"}) + + def test_set_state_updates_gid_from_worker0(self): + remote, conn = self._make_remote() + conn.recv.return_value = 7 + remote.set_state({0: {"gid": 7, "state": "s"}}) + self.assertEqual(remote.get_global_id(), 7) + + def test_initialize_returns_worker0_result(self): + remote, conn = self._make_remote() + conn.recv.return_value = {"init": True} + result = remote.initialize() + self.assertEqual(result, {"init": True}) + + def test_output_returns_worker0_result(self): + remote, conn = self._make_remote() + conn.recv.return_value = None + result = remote.output() + self.assertIsNone(result) + + def test_requires_initialize_false_for_minimal_sim(self): + remote, _ = self._make_remote() + self.assertFalse(remote.requires_initialize()) + + def test_requires_initialize_true_for_sim_with_initialize(self): + from micro_manager.micro_simulation import MicroSimulationRemote + + conn = MagicMock() + conn.recv.return_value = None + remote = MicroSimulationRemote( + gid=0, + late_init=False, + num_ranks=1, + conn=conn, + cls_path="dummy_path", + sim_cls=SimWithInitialize, + ) + self.assertTrue(remote.requires_initialize()) + + def test_requires_output_false_for_minimal_sim(self): + remote, _ = self._make_remote() + self.assertFalse(remote.requires_output()) + + def test_requires_output_true_for_sim_with_output(self): + from micro_manager.micro_simulation import MicroSimulationRemote + + conn = MagicMock() + conn.recv.return_value = None + remote = MicroSimulationRemote( + gid=0, + late_init=False, + num_ranks=1, + conn=conn, + cls_path="dummy_path", + sim_cls=SimWithOutput, + ) + self.assertTrue(remote.requires_output()) + + def test_destroy_sends_delete_task_to_all_workers(self): + remote, conn = self._make_remote() + conn.recv.return_value = None + remote.destroy() + conn.send.assert_called() + conn.recv.assert_called() + + def test_late_init_uses_construct_late_task(self): + from micro_manager.micro_simulation import MicroSimulationRemote + from micro_manager.tasking.task import ConstructLateTask + + conn = MagicMock() + conn.recv.return_value = None + remote = MicroSimulationRemote( + gid=5, + late_init=True, + num_ranks=1, + conn=conn, + cls_path="dummy_path", + sim_cls=MinimalSim, + ) + sent_task = conn.send.call_args_list[0][0][1] + self.assertEqual(sent_task[0], "ConstructLateTask") + + + class TestCreateSimulationClass(unittest.TestCase): def test_valid_class(self): log = MagicMock() @@ -253,124 +373,5 @@ def test_call_creates_wrapper(self): wrapper = self.sim_cls(0) self.assertIsNotNone(wrapper) - -class TestMicroSimulationRemote(unittest.TestCase): - def _make_remote(self, late_init=False): - from micro_manager.micro_simulation import MicroSimulationRemote - - conn = MagicMock() - conn.recv.return_value = None - return ( - MicroSimulationRemote( - gid=0, - late_init=late_init, - num_ranks=1, - conn=conn, - cls_path="dummy_path", - sim_cls=MinimalSim, - ), - conn, - ) - - def test_get_set_global_id(self): - remote, _ = self._make_remote() - self.assertEqual(remote.get_global_id(), 0) - remote.set_global_id(42) - self.assertEqual(remote.get_global_id(), 42) - - def test_solve_returns_worker0_result(self): - remote, conn = self._make_remote() - conn.recv.return_value = {"out": 99} - result = remote.solve({"in": 1}, 0.1) - self.assertEqual(result, {"out": 99}) - - def test_get_state_returns_dict_keyed_by_worker(self): - remote, conn = self._make_remote() - conn.recv.return_value = {"gid": 0, "state": "s"} - state = remote.get_state() - self.assertIn(0, state) - self.assertEqual(state[0], {"gid": 0, "state": "s"}) - - def test_set_state_updates_gid_from_worker0(self): - remote, conn = self._make_remote() - conn.recv.return_value = 7 - remote.set_state({0: {"gid": 7, "state": "s"}}) - self.assertEqual(remote.get_global_id(), 7) - - def test_initialize_returns_worker0_result(self): - remote, conn = self._make_remote() - conn.recv.return_value = {"init": True} - result = remote.initialize() - self.assertEqual(result, {"init": True}) - - def test_output_returns_worker0_result(self): - remote, conn = self._make_remote() - conn.recv.return_value = None - result = remote.output() - self.assertIsNone(result) - - def test_requires_initialize_false_for_minimal_sim(self): - remote, _ = self._make_remote() - self.assertFalse(remote.requires_initialize()) - - def test_requires_initialize_true_for_sim_with_initialize(self): - from micro_manager.micro_simulation import MicroSimulationRemote - - conn = MagicMock() - conn.recv.return_value = None - remote = MicroSimulationRemote( - gid=0, - late_init=False, - num_ranks=1, - conn=conn, - cls_path="dummy_path", - sim_cls=SimWithInitialize, - ) - self.assertTrue(remote.requires_initialize()) - - def test_requires_output_false_for_minimal_sim(self): - remote, _ = self._make_remote() - self.assertFalse(remote.requires_output()) - - def test_requires_output_true_for_sim_with_output(self): - from micro_manager.micro_simulation import MicroSimulationRemote - - conn = MagicMock() - conn.recv.return_value = None - remote = MicroSimulationRemote( - gid=0, - late_init=False, - num_ranks=1, - conn=conn, - cls_path="dummy_path", - sim_cls=SimWithOutput, - ) - self.assertTrue(remote.requires_output()) - - def test_destroy_sends_delete_task_to_all_workers(self): - remote, conn = self._make_remote() - conn.recv.return_value = None - remote.destroy() - conn.send.assert_called() - conn.recv.assert_called() - - def test_late_init_uses_construct_late_task(self): - from micro_manager.micro_simulation import MicroSimulationRemote - from micro_manager.tasking.task import ConstructLateTask - - conn = MagicMock() - conn.recv.return_value = None - remote = MicroSimulationRemote( - gid=5, - late_init=True, - num_ranks=1, - conn=conn, - cls_path="dummy_path", - sim_cls=MinimalSim, - ) - sent_task = conn.send.call_args_list[0][0][1] - self.assertEqual(sent_task[0], "ConstructLateTask") - - if __name__ == "__main__": unittest.main() From 0379c4ab3fab6d6410d214d126a6b32ba31f801a Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Sat, 14 Mar 2026 19:31:56 +0530 Subject: [PATCH 25/27] style: apply black formatting to test_micro_simulation.py --- tests/unit/test_micro_simulation.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/test_micro_simulation.py b/tests/unit/test_micro_simulation.py index e8ee426f..b0d2fbf8 100644 --- a/tests/unit/test_micro_simulation.py +++ b/tests/unit/test_micro_simulation.py @@ -125,7 +125,6 @@ class SimWithExtra(MinimalSim): self.assertEqual(local.extra_attr, "hello") - class TestMicroSimulationRemote(unittest.TestCase): def _make_remote(self, late_init=False): from micro_manager.micro_simulation import MicroSimulationRemote @@ -244,7 +243,6 @@ def test_late_init_uses_construct_late_task(self): self.assertEqual(sent_task[0], "ConstructLateTask") - class TestCreateSimulationClass(unittest.TestCase): def test_valid_class(self): log = MagicMock() @@ -373,5 +371,6 @@ def test_call_creates_wrapper(self): wrapper = self.sim_cls(0) self.assertIsNotNone(wrapper) + if __name__ == "__main__": unittest.main() From ec6848bae5275a5411b63fbb8bc6cef55410a16a Mon Sep 17 00:00:00 2001 From: AdityaGupta716 Date: Sat, 14 Mar 2026 21:59:16 +0530 Subject: [PATCH 26/27] fix: use raw docstring in interpolation.py to suppress SyntaxWarning --- micro_manager/interpolation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/micro_manager/interpolation.py b/micro_manager/interpolation.py index 966d961e..ee93a894 100644 --- a/micro_manager/interpolation.py +++ b/micro_manager/interpolation.py @@ -46,7 +46,7 @@ def get_nearest_neighbor_indices( return neighbor_indices def interpolate(self, neighbors: np.ndarray, point: np.ndarray, values): - """ + r""" Interpolate a value at a point using inverse distance weighting. (https://en.wikipedia.org/wiki/Inverse_distance_weighting) .. math:: f(x) = (\sum_{i=1}^{n} \frac{f_i}{\Vert x_i - x \Vert^2}) / (\sum_{j=1}^{n} \frac{1}{\Vert x_j - x \Vert^2}) From 14a1d123941de028170432534fefc8937c341ec1 Mon Sep 17 00:00:00 2001 From: Alex Hocks Date: Sat, 14 Mar 2026 21:19:42 +0100 Subject: [PATCH 27/27] add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 794b5105..eea18d91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## latest +- Added coverage testing and simulation interface tests [#225](https://github.com/precice/micro-manager/pull/225) - Added `--test-dependencies` CLI flag to check if all required dependencies are correctly installed, with clear error messages listing missing packages and how to fix them [#221](https://github.com/precice/micro-manager/pull/221) - Added load balancing based on micro simulation solve timings [#228](https://github.com/precice/micro-manager/pull/228) - Fixed invalid value in division warning in L1rel/L2rel adaptivity when data contains zeros [#234](https://github.com/precice/micro-manager/pull/234)