diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9fcbab27..63fa0fe1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -24,22 +24,43 @@ variables: value: $BLUECONFIGS_BRANCH description: 'Name of the blueconfigs branch to test against' -python3-base: +test-unit: extends: .tox-template variables: - TOXENV: flake8, py3 + TOXENV: flake8, unit -py38-full-spack: +test-integration: extends: .tox-template variables: - TOXENV: bb5 + TOXENV: integration + +test-integration-e2e: + extends: .tox-template + variables: + TOXENV: bbp-model + TOX_OPTIONS: "-- tests/integration-e2e" + # modules need to be loaded in this certain order to avoid mixing of libraries + EXTRA_MODULES: + unstable:neurodamus-neocortex + +test-scientific: + extends: .tox-template + variables: + TOXENV: bbp-model + TOX_OPTIONS: "-- tests/scientific" # modules need to be loaded in this certain order to avoid mixing of libraries EXTRA_MODULES: - unstable:py-psutil unstable:intel-oneapi-compilers unstable:py-bluepy unstable:neurodamus-neocortex - unstable:py-neurodamus # needed for dependencies + +test-scientific-ngv: + extends: .tox-template + variables: + TOXENV: bbp-model + TOX_OPTIONS: "-- tests/scientific-ngv" + EXTRA_MODULES: + unstable:neurodamus-neocortex-multiscale set_alt_branches: script: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba1415d3..231906a8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,7 @@ to our [GitHub Repository][github]. Even better, you can [submit a Pull Request] # Missing a Feature? You can *request* a new feature by [submitting an issue](#issues) to our GitHub Repository. -If you would like to *implement* a new feature, please submit an issue with a proposal for your +If you would like to *implement* a new feature, please submit an issue with a proposal for your work first, to be sure that we can use it. Please consider what kind of change it is: @@ -118,23 +118,65 @@ This section applies to both Python versions 2 and 3. It is recommended to use `virtualenv` to develop in a sandbox environment: -``` +```sh virtualenv venv . venv/bin/activate -pip install -r tests/requirement_tests.txt + +# Install neurodamus in development mode +pip install -e . + +# Install test requirements +pip install -r tests/requirements.txt +``` + +## Testing + +There are several test groups in Neurodamus, from plain unit tests to integration and system tests. + +While developing you may want to run unit tests very frequently and thus we suggest running the base +tests using pytest directly from the dev environment. +```sh +pytest tests/unit ``` -## Build +For the next stage testing we suggest using the provided tox environments -Run the following command to build incrementally the project: `pip install -e .` +```sh +# Integration tests +tox -e integration +``` + +System and scientific tests require Blue Brain models. They therefore depend on neurodamus-neocortex +`special` builds and should be launched as follows: -## Test +```sh +module load unstable neurodamus-neocortex py-neurodamus +# Integration-e2e tests +tox -e bbp-model -- tests/integration-e2e + +# Scientific tests +tox -e bbp-model -- tests/scientific +``` -Run the following command to run the Python unit-tests: `pytest tests` +### Adding more tests + +We kindly ask contributors to add tests alongside their new features or enhancements. With the +previous setup in mind, consider adding test to one or more groups: + + - `tests/unit`: For unit tests of functions with little dependencies. Tests get a few shared mocks, +namely for NEURON and MPI. + - `tests/integration`: For integration tests. Please place here tests around a component which + might depend on a number of functions. Tests here can rely on NEURON and the other base + dependencies. Additionally tests are provided a `special` with synapse mechanisms so that + Neurodamus can be fully initialized. + - `tests/integration-e2e`: Place tests here that require launching a top-level Neurodamus instance. + Examples of it might be testing modes of operation, parameter handling, or simply larger + integration tests which are validated according to the results. + - `tests/scientific[-ngv]`: Should contain tests which validate essential scientific features + implemented in Neurodamus, namely creation of synapses, replay, NGV, neurodamulation, etc. ## Coding conventions The code coverage of the Python unit-tests may not decrease over time. It means that every change must go with their corresponding Python unit-tests to -validate the library behavior as well as to demonstrate the API usage. - +validate the library behavior as well as to demonstrate the API usage. diff --git a/ci/build_ndcore.sh b/ci/build_ndcore.sh index e2545e61..d945288c 100755 --- a/ci/build_ndcore.sh +++ b/ci/build_ndcore.sh @@ -12,7 +12,11 @@ fi # Get the common synapses COMMON_DIR=_common -git clone git@bbpgitlab.epfl.ch:hpc/sim/models/common.git $COMMON_DIR --depth=1 +if [ -d "$COMMON_DIR" ]; then + ( cd "$COMMON_DIR" && git pull --quiet ) +else + git clone git@bbpgitlab.epfl.ch:hpc/sim/models/common.git $COMMON_DIR --depth=1 +fi MOD_BUILD_DIR="mods.tmp" mkdir -p $MOD_BUILD_DIR diff --git a/setup.cfg b/setup.cfg index 14c8b0ea..a411c76d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,7 +17,7 @@ classifier = License :: Other/Proprietary License [tool:pytest] -addopts = tests --verbose +addopts = --verbose markers = slow: marks tests as slow [aliases] @@ -36,6 +36,6 @@ formats = bdist_wheel [flake8] exclude = .*, __pycache__, .eggs, *.egg, build, dist, docs, venv, *.egg-info, _benchmarks, core -ignore = E127, E221, E226, E701, W503, W504, E731 +ignore = E127, E221, E226, E701, W503, W504, E731, PT001, PT023 max-line-length = 100 # max-complexity = 12 diff --git a/tests/conftest.py b/tests/conftest.py index aa56191d..ad4bf464 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,16 @@ USECASE3 = Path(__file__).parent.absolute() / "simulations" / "usecase3" +@pytest.fixture(scope="session") +def rootdir(request): + return request.config.rootdir + + +@pytest.fixture(scope="session", name="USECASE3") +def usecase3_path(): + return USECASE3 + + @pytest.fixture def sonata_config(): return dict( diff --git a/tests/integration-e2e/conftest.py b/tests/integration-e2e/conftest.py new file mode 100644 index 00000000..7cea024d --- /dev/null +++ b/tests/integration-e2e/conftest.py @@ -0,0 +1,8 @@ +import os +import pytest + +assert os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), "Test requires loading a neocortex model to run" + +pytestmark = [ + pytest.mark.forked, +] diff --git a/tests/test_cli_opts.py b/tests/integration-e2e/test_cli_opts.py similarity index 86% rename from tests/test_cli_opts.py rename to tests/integration-e2e/test_cli_opts.py index 53d7ff23..5d3d8249 100644 --- a/tests/test_cli_opts.py +++ b/tests/integration-e2e/test_cli_opts.py @@ -1,5 +1,4 @@ import json -import os from pathlib import Path import pytest import shutil @@ -7,14 +6,11 @@ import tempfile -SIM_DIR = Path(__file__).parent.absolute() / "simulations" / "v5_sonata" +SIM_DIR = Path(__file__).parent.parent.absolute() / "simulations" / "v5_sonata" CONFIG_FILE_MINI = "simulation_config_mini.json" @pytest.mark.slow -@pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run") def test_save_restore_cli(): with open(SIM_DIR / CONFIG_FILE_MINI, "r") as f: sim_config_data = json.load(f) @@ -47,9 +43,6 @@ def test_save_restore_cli(): subprocess.run(command, check=True) -@pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run") def test_cli_prcellgid(): test_folder = tempfile.TemporaryDirectory("cli-test-prcellgid") # auto removed test_folder_path = Path(test_folder.name) diff --git a/tests/test_connection.py b/tests/integration-e2e/test_connection.py similarity index 81% rename from tests/test_connection.py rename to tests/integration-e2e/test_connection.py index 67a2cf9a..fa075a88 100644 --- a/tests/test_connection.py +++ b/tests/integration-e2e/test_connection.py @@ -1,18 +1,14 @@ -import os import pytest from pathlib import Path from neurodamus.io.synapse_reader import SynapseParameters from neurodamus.node import Node -SIM_DIR = Path(__file__).parent.absolute() / "simulations" / "v5_sonata" +SIM_DIR = Path(__file__).parent.parent.absolute() / "simulations" / "v5_sonata" CONFIG_FILE_MINI = SIM_DIR / "simulation_config_mini.json" @pytest.mark.slow -@pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run") # This test is to mimic the error reported in HPCTM-1687 during connection.add_syanpses() # when detecting conn._synapse_params with more than one element is not None def test_add_synapses(): diff --git a/tests/integration-e2e/test_dry_run_worflow.py b/tests/integration-e2e/test_dry_run_worflow.py new file mode 100644 index 00000000..cdd217b7 --- /dev/null +++ b/tests/integration-e2e/test_dry_run_worflow.py @@ -0,0 +1,25 @@ + +def test_dry_run_workflow(USECASE3): + """ + Test that the dry run mode works + """ + + from neurodamus import Neurodamus + nd = Neurodamus( + str(USECASE3 / "simulation_sonata.json"), + dry_run=True + ) + + nd.run() + + assert 20.0 <= nd._dry_run_stats.cell_memory_total <= 30.0 + assert 0.0 <= nd._dry_run_stats.synapse_memory_total <= 1.0 + assert 80.0 <= nd._dry_run_stats.base_memory <= 120.0 + expected_items = { + 'L4_PC-dSTUT': 2, + 'L4_MC-dSTUT': 1, + 'L4_MC-dNAC': 1, + 'L5_PC-dSTUT': 1 + } + assert nd._dry_run_stats.metype_counts == expected_items + assert nd._dry_run_stats.suggest_nodes(0.3) > 0 diff --git a/tests/test_error_handling.py b/tests/integration-e2e/test_error_handling.py similarity index 51% rename from tests/test_error_handling.py rename to tests/integration-e2e/test_error_handling.py index dbdf7d4b..27c10cfc 100644 --- a/tests/test_error_handling.py +++ b/tests/integration-e2e/test_error_handling.py @@ -1,21 +1,8 @@ -import os import pytest -from pathlib import Path -USECASE3 = Path(__file__).parent.absolute() / "simulations" / "usecase3" -SONATA_CONF_FILE = str(USECASE3 / "simulation_sonata.json") - -pytestmark = [ - pytest.mark.forked, - pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run" - ) -] - - -def test_handling_neuron_exceptions(): +def test_handling_neuron_exceptions(USECASE3): + SONATA_CONF_FILE = str(USECASE3 / "simulation_sonata.json") from neurodamus import Node n = Node(SONATA_CONF_FILE) n.load_targets() diff --git a/tests/test_loadbalance.py b/tests/integration-e2e/test_loadbalance.py similarity index 93% rename from tests/test_loadbalance.py rename to tests/integration-e2e/test_loadbalance.py index 39e58599..4fa375c8 100644 --- a/tests/test_loadbalance.py +++ b/tests/integration-e2e/test_loadbalance.py @@ -1,3 +1,5 @@ +"""Tests load balance.""" +# Since a good deal of load balance tests are e2e we put all of them together in this group import logging import os import pytest @@ -5,9 +7,7 @@ import tempfile from pathlib import Path -SIM_DIR = Path(__file__).parent.absolute() / "simulations" - -pytestmark = pytest.mark.forked +SIM_DIR = Path(__file__).parent.parent.absolute() / "simulations" @pytest.fixture @@ -69,10 +69,6 @@ def circuit_conf(): ) -@pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run" -) def test_load_balance_integrated(target_manager_hoc, circuit_conf): """Comprehensive test using real cells and deriving cx for a sub-target """ @@ -102,10 +98,6 @@ def test_load_balance_integrated(target_manager_hoc, circuit_conf): assert not lbal._reuse_cell_complexity(TargetSpec(None)) -@pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run" -) def test_multisplit(target_manager_hoc, circuit_conf, capsys): """Comprehensive test using real cells, multi-split and complexity derivation """ @@ -146,10 +138,6 @@ def test_multisplit(target_manager_hoc, circuit_conf, capsys): assert "Target VerySmall is a subset of the target Small" in captured.out -@pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run" -) def test_loadbal_integration(): """Ensure given the right files are in the lbal dir, the correct situation is detected """ diff --git a/tests/test_multicycle_runs.py b/tests/integration-e2e/test_multicycle_runs.py similarity index 87% rename from tests/test_multicycle_runs.py rename to tests/integration-e2e/test_multicycle_runs.py index adbd3d1b..c44d2de0 100644 --- a/tests/test_multicycle_runs.py +++ b/tests/integration-e2e/test_multicycle_runs.py @@ -3,18 +3,14 @@ import pytest from pathlib import Path -SIM_DIR = Path(__file__).parent.absolute() / "simulations" +SIM_DIR = Path(__file__).parent.parent.absolute() / "simulations" @pytest.fixture(autouse=True) -def change_test_dir(monkeypatch, tmp_path): +def _change_test_dir(monkeypatch, tmp_path): monkeypatch.chdir(str(SIM_DIR / "usecase3")) -@pytest.mark.forked -@pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test initialize internal structure and clashes with other tests") def test_nodeset_target_generate_subtargets(): from neurodamus.core.nodeset import NodeSet from neurodamus.target_manager import NodesetTarget @@ -45,10 +41,6 @@ def test_nodeset_target_generate_subtargets(): assert np.array_equal(subtargets[2][1].get_gids(), np.array([1002])) -@pytest.mark.forked -@pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test initialize internal structure and clashes with other tests") def test_hoc_target_generate_subtargets(): from neurodamus.target_manager import _HocTarget @@ -94,10 +86,6 @@ def _read_sonata_spike_file(spike_file): return timestamps, spike_gids -@pytest.mark.forked -@pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run") def test_v5_sonata_multisteps(): import numpy.testing as npt from neurodamus import Neurodamus @@ -126,10 +114,6 @@ def test_v5_sonata_multisteps(): npt.assert_allclose(timestamps, obtained_timestamps) -@pytest.mark.forked -@pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run") def test_usecase3_sonata_multisteps(): import numpy.testing as npt from neurodamus import Neurodamus diff --git a/tests/test_plugin.py b/tests/integration-e2e/test_plugin.py similarity index 98% rename from tests/test_plugin.py rename to tests/integration-e2e/test_plugin.py index 339f5ba9..fc38010c 100644 --- a/tests/test_plugin.py +++ b/tests/integration-e2e/test_plugin.py @@ -20,7 +20,7 @@ # # Launching of the engine as a test # -SIM_DIR = Path(__file__).parent.absolute() / "simulations" +SIM_DIR = Path(__file__).parent.parent.absolute() / "simulations" class ACellType(BaseCell): diff --git a/tests/test_synapse_reader.py b/tests/integration-e2e/test_synapse_reader.py similarity index 87% rename from tests/test_synapse_reader.py rename to tests/integration-e2e/test_synapse_reader.py index b44d3feb..7ce7b5c1 100644 --- a/tests/test_synapse_reader.py +++ b/tests/integration-e2e/test_synapse_reader.py @@ -1,18 +1,9 @@ import numpy as np import numpy.testing as npt -import os -import pytest from neurodamus.gap_junction import GapJunctionSynapseReader from pathlib import Path - -pytestmark = [ - pytest.mark.forked, - pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run" - ) -] +SIM_DIR = Path(__file__).parent.parent.absolute() / "simulations" def test_gapjunction_synreaderNRN(): @@ -35,7 +26,6 @@ def test_gapjunction_synreaderNRN(): def test_gapjunction_sonata_reader(): - SIM_DIR = Path(__file__).parent.absolute() / "simulations" sonata_file = str(SIM_DIR / "mini_thalamus_sonata/gapjunction/edges.h5") sonata_reader = GapJunctionSynapseReader.create(sonata_file, 1) syn_params_sonata = sonata_reader._load_synapse_parameters(1) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py new file mode 100644 index 00000000..8671132a --- /dev/null +++ b/tests/integration/conftest.py @@ -0,0 +1,6 @@ +import pytest +# !! NOTE: Please don't import NEURON/Neurodamus at module level +# pytest weird discovery system will trigger NEURON init and open a can of worms + +# Make all tests run forked +pytestmark = pytest.mark.forked diff --git a/tests/test_cell.py b/tests/integration/test_cell.py similarity index 84% rename from tests/test_cell.py rename to tests/integration/test_cell.py index a2c0bb98..92176741 100644 --- a/tests/test_cell.py +++ b/tests/integration/test_cell.py @@ -1,17 +1,21 @@ """ Basic tests to HL neuron for creating cells and setup a simple simulation """ -from os import path +import os import pytest - +from pathlib import Path # !! NOTE: Please don't import neron/neurodamus at module level # pytest weird discovery system will trigger Neuron init and open can of worms -# And make all tests run forked -pytestmark = pytest.mark.forked + + +@pytest.fixture(scope="session") +def morphologies_root(rootdir): + return Path(rootdir) / "tests/sample_data/morphology" @pytest.fixture -def Cell(): +def Cell(rootdir): + os.environ["HOC_LIBRARY_PATH"] = str(rootdir) + "/core/hoc" from neurodamus.core import Cell return Cell @@ -20,20 +24,15 @@ def Cell(): "morphology_path", ["C060114A7.asc", "C060114A7.h5", "merged_container.h5/C060114A7.h5"] ) -def test_load_cell(morphology_path, request): - Cell = request.getfixturevalue("Cell") - - d = path.dirname(__file__) - c = Cell(1, path.join(d, "morphology", morphology_path)) +def test_load_cell(morphologies_root, morphology_path, Cell): + c = Cell(1, str(morphologies_root / morphology_path)) assert len(c.all) == 325 assert c.axons[4].name().endswith(".axon[4]") assert c.soma.L == pytest.approx(26.11, abs=0.01) -def test_morphio_read(Cell): - d = path.dirname(__file__) - c = Cell(1, path.join(d, "morphology/simple.h5")) - +def test_morphio_read(morphologies_root, Cell): + c = Cell(1, str(morphologies_root / "simple.h5")) Cell.show_topology() assert len(c.all) == 7 assert len(list(c.h.basal)) == 3 diff --git a/tests/test_neuron.py b/tests/integration/test_neuron.py similarity index 100% rename from tests/test_neuron.py rename to tests/integration/test_neuron.py diff --git a/tests/test_sonata_config.py b/tests/integration/test_sonata_config.py similarity index 98% rename from tests/test_sonata_config.py rename to tests/integration/test_sonata_config.py index 988ad21b..0b100442 100644 --- a/tests/test_sonata_config.py +++ b/tests/integration/test_sonata_config.py @@ -3,7 +3,7 @@ from pathlib import Path from tempfile import NamedTemporaryFile -USECASE3 = Path(__file__).parent.absolute() / "simulations" / "usecase3" +USECASE3 = Path(__file__).parent.parent.absolute() / "simulations" / "usecase3" SONATA_CONF_FILE = str(USECASE3 / "simulation_sonata.json") pytestmark = pytest.mark.forked diff --git a/tests/test_sonata_output.py b/tests/integration/test_sonata_output.py similarity index 71% rename from tests/test_sonata_output.py rename to tests/integration/test_sonata_output.py index bd0c7000..c203d199 100644 --- a/tests/test_sonata_output.py +++ b/tests/integration/test_sonata_output.py @@ -2,19 +2,12 @@ import json import pytest from tempfile import NamedTemporaryFile -from neurodamus.node import Node pytestmark = pytest.mark.forked # independent processes -# Currently exclude this test from base tests because of failure, -# perhaps NEURON and log file has been initialised by some unforked test. -# TODO: Review our unit tests with marker pytest.mark.slow -# so as to exclude them from base tests by pytest -m "not slow" -@pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run") def test_sonata_logfile(sonata_config): + from neurodamus.node import Node sonata_config["output"] = {"log_file": "my_pydamus.log"} # create a tmp json file to test the user defined log_file with NamedTemporaryFile("w", suffix=".json", delete=False) as config_file: @@ -26,6 +19,7 @@ def test_sonata_logfile(sonata_config): def test_throw_spike_sort_order(sonata_config): + from neurodamus.node import Node from neurodamus.core.configuration import ConfigurationError sonata_config["output"] = {"spikes_sort_order": "by_id"} diff --git a/tests/test_stimuli.py b/tests/integration/test_stimulus2.py similarity index 65% rename from tests/test_stimuli.py rename to tests/integration/test_stimulus2.py index 90373c9d..b201818d 100644 --- a/tests/test_stimuli.py +++ b/tests/integration/test_stimulus2.py @@ -1,7 +1,7 @@ -#!/usr/bin/env python """ -Test suite for the new-gen Stimuli source (replacing TStim.hoc and parts of StimManager) +A collection of tests for advanced stimulus generated with the help of Neuron """ + import pytest from neurodamus.core import CurrentSource, ConductanceSource from neurodamus.core.random import Random123 @@ -12,48 +12,6 @@ def setup_method(self): rng = Random123(1, 2, 3) self.stim = CurrentSource(rng=rng) - def test_flat_segment(self): - self.stim.add_segment(1.2, 10) - assert list(self.stim.time_vec) == [0, 10] - assert list(self.stim.stim_vec) == [1.2, 1.2] - - def test_pulse(self): - self.stim.add_pulse(1.2, 10) - assert list(self.stim.time_vec) == [0, 0, 10, 10] - assert list(self.stim.stim_vec) == [0, 1.2, 1.2, 0] - - @pytest.mark.parametrize("base_amp", [-1, 0, 1.5]) - def test_pulse_diff_base(self, base_amp): - self.stim.add_pulse(1.2, 10, base_amp=base_amp) - assert list(self.stim.time_vec) == [0, 0, 10, 10] - assert list(self.stim.stim_vec) == [base_amp, 1.2, 1.2, base_amp] - - def test_two_pulses(self): - self.stim.add_pulse(1.2, 10) - self.stim.delay(5) - self.stim.add_pulse(0.8, 5) - assert list(self.stim.time_vec) == [0, 0, 10, 10, 15, 15, 20, 20] - assert list(self.stim.stim_vec) == [0, 1.2, 1.2, 0, 0, 0.8, 0.8, 0] - - def test_ramp(self): - self.stim.add_ramp(5, 7.5, 10) - assert list(self.stim.time_vec) == [0, 0, 10, 10] - assert list(self.stim.stim_vec) == [0, 5, 7.5, 0] - - def test_delay_ramp(self): - # When a delay is specified (in ctor or factory) base_amp is set on t=0 too - sig = CurrentSource.ramp(1, 2, 2, base_amp=-1, delay=10) - assert list(sig.time_vec) == [0, 10, 10, 12, 12] - assert list(sig.stim_vec) == [-1, -1, 1, 2, -1] - - def test_train(self): - self.stim.add_train(1.2, 10, 20, 350) - # At 10Hz pulses have T=100ms - # We end up with 4 pulses, the last one with reduced rest phase - assert list(self.stim.time_vec) == [0, 0, 20, 20, 100, 100, 120, 120, 200, 200, 220, 220, - 300, 300, 320, 320, 350] - assert list(self.stim.stim_vec) == [0, 1.2, 1.2, 0] * 4 + [0] - def test_sin(self): self.stim.add_sin(1, 0.1, 10000) assert list(self.stim.time_vec) == pytest.approx([0, 0.025, 0.05, 0.075, 0.1, 0.1]) diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 00000000..df609324 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,3 @@ +pytest==7.4 +pytest-forked==1.6 +scipy==1.10 diff --git a/tests/morphology/C060114A7.asc b/tests/sample_data/morphology/C060114A7.asc similarity index 100% rename from tests/morphology/C060114A7.asc rename to tests/sample_data/morphology/C060114A7.asc diff --git a/tests/morphology/C060114A7.h5 b/tests/sample_data/morphology/C060114A7.h5 similarity index 100% rename from tests/morphology/C060114A7.h5 rename to tests/sample_data/morphology/C060114A7.h5 diff --git a/tests/morphology/merged_container.h5 b/tests/sample_data/morphology/merged_container.h5 similarity index 100% rename from tests/morphology/merged_container.h5 rename to tests/sample_data/morphology/merged_container.h5 diff --git a/tests/morphology/simple.h5 b/tests/sample_data/morphology/simple.h5 similarity index 100% rename from tests/morphology/simple.h5 rename to tests/sample_data/morphology/simple.h5 diff --git a/tests/scientific-ngv/conftest.py b/tests/scientific-ngv/conftest.py new file mode 100644 index 00000000..7cea024d --- /dev/null +++ b/tests/scientific-ngv/conftest.py @@ -0,0 +1,8 @@ +import os +import pytest + +assert os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), "Test requires loading a neocortex model to run" + +pytestmark = [ + pytest.mark.forked, +] diff --git a/tests/test_ngv.py b/tests/scientific-ngv/test_ngv.py similarity index 61% rename from tests/test_ngv.py rename to tests/scientific-ngv/test_ngv.py index 24e33fa6..e003cac3 100644 --- a/tests/test_ngv.py +++ b/tests/scientific-ngv/test_ngv.py @@ -1,66 +1,14 @@ """ Test suite for Neurodamus NGV support """ -import os -import subprocess from pathlib import Path import libsonata import numpy as np -import pytest -from neurodamus import Neurodamus -from neurodamus.ngv import GlioVascularManager - -SIM_DIR = Path(__file__).parent.absolute() / "simulations" / "ngv" +SIM_DIR = Path(__file__).parent.parent.absolute() / "simulations" / "ngv" SONATACONFIG_FILE = SIM_DIR / "simulation_config.json" -pytestmark = [ - pytest.mark.forked, - pytest.mark.slow, - pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run", - ), -] - - -def load_neurodamus_neocortex_multiscale(): - module_output = subprocess.run( - ["module load unstable; module show neurodamus-neocortex-multiscale"], - capture_output=True, - text=True, - shell=True, - ) - nrn_mech_path = None - ld_library_path = None - for line in module_output.stderr.split("\n"): - if "NRNMECH_LIB_PATH" in line: - nrn_mech_path = line.split(" ")[2] - ld_library_path = os.path.dirname(nrn_mech_path) - if nrn_mech_path is not None: - os.environ["NRNMECH_LIB_PATH"] = nrn_mech_path - os.environ["LD_LIBRARY_PATH"] = ( - ld_library_path + ":" + os.environ.get("LD_LIBRARY_PATH", "") - ) - else: - module_output = subprocess.run( - ["module load unstable; module show neurodamus-neocortex"], - capture_output=True, - text=True, - shell=True, - ) - raise Exception( - "Right module not found. Output of 'module av neurodamus-neocortex': {}\n" - "MODULEPATH: {}".format(module_output.stderr, os.environ.get("MODULEPATH")) - ) - - -def get_manager(ndamus): - return ndamus.circuits.get_edge_manager( - "vasculature", "astrocytes", GlioVascularManager - ) - def get_R0pas(astro_id, manager): astrocyte = manager._cell_manager.gid2cell[astro_id + manager._gid_offset] @@ -99,7 +47,8 @@ def get_R0pas_ref(astro_id, manager): def test_vasccouplingB_radii(): - load_neurodamus_neocortex_multiscale() + from neurodamus import Neurodamus + from neurodamus.ngv import GlioVascularManager ndamus = Neurodamus( str(SONATACONFIG_FILE), enable_reports=False, @@ -107,7 +56,7 @@ def test_vasccouplingB_radii(): enable_coord_mapping=True, ) - manager = get_manager(ndamus) + manager = ndamus.circuits.get_edge_manager("vasculature", "astrocytes", GlioVascularManager) astro_ids = manager._astro_ids R0pas = [r for astro_id in astro_ids for r in get_R0pas(astro_id, manager)] diff --git a/tests/scientific/conftest.py b/tests/scientific/conftest.py new file mode 100644 index 00000000..eb991fd6 --- /dev/null +++ b/tests/scientific/conftest.py @@ -0,0 +1,11 @@ +import os +import pytest + +assert os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), \ + "Test requires loading a neocortex model to run" +assert os.path.isfile("/gpfs/bbp.cscs.ch/project/proj83/circuits/Bio_M/20200805/circuit.mvd3"), \ + "Circuit file not available" + +pytestmark = [ + pytest.mark.forked, +] diff --git a/tests/scientific/test_dry_run_worflow.py b/tests/scientific/test_dry_run_worflow.py deleted file mode 100644 index a714ccb4..00000000 --- a/tests/scientific/test_dry_run_worflow.py +++ /dev/null @@ -1,34 +0,0 @@ -import os -import pytest -from pathlib import Path -from collections import Counter - -USECASE3 = Path(__file__).parent.absolute() / "usecase3" - - -@pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run" -) -def test_dry_run_workflow(): - """ - Test that the dry run mode works - """ - - from neurodamus import Neurodamus - nd = Neurodamus( - str(USECASE3 / "simulation_sonata.json"), - dry_run=True - ) - - nd.run() - - assert 20.0 <= nd._dry_run_stats.cell_memory_total <= 30.0 - assert 0.0 <= nd._dry_run_stats.synapse_memory_total <= 1.0 - assert 90.0 <= nd._dry_run_stats.base_memory <= 120.0 - expected_items = Counter([('L4_PC-dSTUT', 2), - ('L4_MC-dSTUT', 1), - ('L4_MC-dNAC', 1), - ('L5_PC-dSTUT', 1)]) - assert set(nd._dry_run_stats.metype_counts.items()) == set(expected_items) - assert nd._dry_run_stats.suggest_nodes(0.3) > 0 diff --git a/tests/test_lfp.py b/tests/scientific/test_lfp.py similarity index 93% rename from tests/test_lfp.py rename to tests/scientific/test_lfp.py index 70bc6faf..50115435 100644 --- a/tests/test_lfp.py +++ b/tests/scientific/test_lfp.py @@ -4,16 +4,7 @@ import numpy as np from pathlib import Path -SIM_DIR = Path(__file__).parent.absolute() / "simulations" - -pytestmark = [ - pytest.mark.forked, - pytest.mark.slow, - pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run" - ) -] +SIM_DIR = Path(__file__).parent.parent.absolute() / "simulations" @pytest.fixture @@ -61,7 +52,7 @@ def test_file(tmpdir): incrementy -= 0.0032 electrodes_group.create_dataset("scaling_factors", dtype='f8', data=matrix) - yield test_file + return test_file def test_load_lfp_config(tmpdir, test_file): @@ -167,10 +158,6 @@ def _read_sonata_lfp_file(lfp_file): return node_ids, data -@pytest.mark.forked -@pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run") def test_v5_sonata_lfp(tmpdir, test_file): import numpy.testing as npt from neurodamus import Neurodamus diff --git a/tests/test_modification.py b/tests/scientific/test_modification.py similarity index 92% rename from tests/test_modification.py rename to tests/scientific/test_modification.py index 31bf28c0..0c073a0e 100644 --- a/tests/test_modification.py +++ b/tests/scientific/test_modification.py @@ -83,19 +83,6 @@ } """ -pytestmark = [ - pytest.mark.slow, - pytest.mark.forked, - pytest.mark.skipif( - not os.path.isfile("/gpfs/bbp.cscs.ch/project/proj83/circuits/Bio_M/20200805/circuit.mvd3"), - reason="Circuit file not available" - ), - pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run" - ) -] - @pytest.fixture(scope="module") def blueconfig(): @@ -153,7 +140,8 @@ def test_TTX_modification(blueconfig): nspike_TTX = sum(len(spikes) for spikes, _ in n._spike_vecs) log_verbose("spikes without TTX = %s, with TTX = %s", nspike_noTTX, nspike_TTX) - assert (nspike_noTTX > 0 and nspike_TTX == 0) + assert nspike_noTTX > 0 + assert nspike_TTX == 0 def test_ConfigureAllSections_modification(blueconfig): diff --git a/tests/scientific/test_multipop.py b/tests/scientific/test_multipop.py index 5cdb01ea..1b4ce1ee 100644 --- a/tests/scientific/test_multipop.py +++ b/tests/scientific/test_multipop.py @@ -35,10 +35,6 @@ def sonata_config_file(sonata_config, extra_config): os.unlink(config_file.name) -@pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run" -) @pytest.mark.parametrize("extra_config", [{ "connection_overrides": [ { @@ -82,18 +78,16 @@ def test_multipop_simple(sonata_config_file): assert set(cell_man.local_nodes.final_gids()) == set(cell_man.gid2cell) == {1001, 1002} for conn in edges_A.all_connections(): - assert conn.tgid <= 3 and conn.sgid <= 3 + assert conn.tgid <= 3 + assert conn.sgid <= 3 assert conn.synapses[0].verboseLevel == 0 for conn in edges_B.all_connections(): - assert 1000 < conn.tgid <= 1003 and 1000 < conn.sgid <= 1003 + assert 1000 < conn.tgid <= 1003 + assert 1000 < conn.sgid <= 1003 assert conn.synapses[0].verboseLevel == 1 -@pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run" -) @pytest.mark.parametrize("extra_config", [{ "connection_overrides": [ { @@ -159,19 +153,23 @@ def test_multipop_full_conn(sonata_config_file): assert edges_BA.src_cell_manager == cell_man_B for conn in edges_A.all_connections(): - assert conn.tgid <= 3 and conn.sgid <= 3 + assert conn.tgid <= 3 + assert conn.sgid <= 3 assert conn.synapses[0].verboseLevel == 0 for conn in edges_B.all_connections(): - assert 1000 < conn.tgid <= 1003 and 1000 < conn.sgid <= 1003 + assert 1000 < conn.tgid <= 1003 + assert 1000 < conn.sgid <= 1003 assert conn.synapses[0].verboseLevel == 1 for conn in edges_BA.all_connections(): - assert 0 < conn.tgid <= 3 and 1000 < conn.sgid <= 1003 + assert 0 < conn.tgid <= 3 + assert 1000 < conn.sgid <= 1003 assert conn.synapses[0].verboseLevel == 2 for conn in edges_AB.all_connections(): - assert 1000 < conn.tgid <= 1003 and 0 < conn.sgid <= 3 + assert 1000 < conn.tgid <= 1003 + assert 0 < conn.sgid <= 3 assert conn.synapses[0].verboseLevel == 3 # Replay will create spikes for all instantiated cells targeting popA diff --git a/tests/test_neuromodulation.py b/tests/scientific/test_neuromodulation.py similarity index 78% rename from tests/test_neuromodulation.py rename to tests/scientific/test_neuromodulation.py index aa73c645..c9c53f23 100644 --- a/tests/test_neuromodulation.py +++ b/tests/scientific/test_neuromodulation.py @@ -1,23 +1,15 @@ import numpy as np import numpy.testing as npt import os -import pytest from pathlib import Path -pytestmark = [ - pytest.mark.forked, - pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run" - ) -] +SIM_DIR = Path(__file__).parent.parent.absolute() / "simulations" / "neuromodulation" def test_neuromodulation_sims_neuron(): import numpy.testing as npt from neurodamus import Neurodamus - SIM_DIR = Path(__file__).parent.absolute() / "simulations" / "neuromodulation" config_file = str(SIM_DIR / "BlueConfig") os.chdir(SIM_DIR) nd = Neurodamus(config_file, disable_reports=True) @@ -32,12 +24,10 @@ def test_neuromodulation_sims_neuron(): npt.assert_allclose(timestamps, obtained_timestamps) -@pytest.mark.forked def test_neuromodulation_sims_coreneuron(): from neurodamus import Neurodamus from neurodamus.replay import SpikeManager - SIM_DIR = Path(__file__).parent.absolute() / "simulations" / "neuromodulation" config_file = str(SIM_DIR / "BlueConfig") os.chdir(SIM_DIR) nd = Neurodamus(config_file, disable_reports=True, simulator="CORENEURON", diff --git a/tests/test_patched_delays.py b/tests/scientific/test_patched_delays.py similarity index 92% rename from tests/test_patched_delays.py rename to tests/scientific/test_patched_delays.py index 9e370dd5..41517f21 100644 --- a/tests/test_patched_delays.py +++ b/tests/scientific/test_patched_delays.py @@ -72,13 +72,6 @@ @pytest.mark.slow -@pytest.mark.forked -@pytest.mark.skipif( - not os.path.isfile("/gpfs/bbp.cscs.ch/project/proj83/circuits/Bio_M/20200805/circuit.mvd3"), - reason="Circuit file not available") -@pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run") def test_eager_caching(): """ A test of the impact of eager caching of synaptic parameters. BBPBGLIB-813 diff --git a/tests/scientific/test_projections.py b/tests/scientific/test_projections.py index 1d5ca9f6..a3c99692 100644 --- a/tests/scientific/test_projections.py +++ b/tests/scientific/test_projections.py @@ -45,10 +45,6 @@ def sonata_config_file(sonata_config): os.unlink(config_file.name) -@pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run" -) def test_projections(sonata_config_file): """ Test that projections are correctly set up diff --git a/tests/scientific/test_replay.py b/tests/scientific/test_replay.py index 1e4e3748..4f3eb5b7 100644 --- a/tests/scientific/test_replay.py +++ b/tests/scientific/test_replay.py @@ -2,7 +2,6 @@ import numpy import numpy.testing as npt import os -import pytest from pathlib import Path from tempfile import NamedTemporaryFile @@ -29,10 +28,6 @@ def replay_sim_config(sonata_config, replay_file): return config_file -@pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run" -) def test_replay_sim(sonata_config): from neurodamus import Neurodamus from neurodamus.core.configuration import Feature @@ -74,10 +69,6 @@ def test_replay_sim(sonata_config): # A more comprehensive example, using Sonata replay with two populations # ====================================================================== -@pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run" -) def test_replay_sonata_spikes(sonata_config): from neurodamus import Neurodamus from neurodamus.core.configuration import Feature diff --git a/tests/test_simulation.py b/tests/scientific/test_simulation.py similarity index 94% rename from tests/test_simulation.py rename to tests/scientific/test_simulation.py index a794cfb8..b590541c 100644 --- a/tests/test_simulation.py +++ b/tests/scientific/test_simulation.py @@ -5,16 +5,7 @@ import subprocess from pathlib import Path -SIM_DIR = Path(__file__).parent.absolute() / "simulations" - -pytestmark = [ - pytest.mark.forked, - pytest.mark.slow, - pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run" - ) -] +SIM_DIR = Path(__file__).parent.parent.absolute() / "simulations" @pytest.mark.skipif( diff --git a/tests/test_sonata_properties.py b/tests/scientific/test_sonata_properties.py similarity index 95% rename from tests/test_sonata_properties.py rename to tests/scientific/test_sonata_properties.py index 44146853..791ec520 100644 --- a/tests/test_sonata_properties.py +++ b/tests/scientific/test_sonata_properties.py @@ -98,20 +98,6 @@ } """ -# Prerequisites -pytestmark = [ - pytest.mark.slow, - pytest.mark.forked, - pytest.mark.skipif( - not os.path.isfile(NODE), - reason="Circuit file not available" - ), - pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run" - ) -] - @pytest.fixture(scope="module") def blueconfig(): diff --git a/tests/scientific/test_spont_minis.py b/tests/scientific/test_spont_minis.py index e3554fd8..49d8bbf0 100644 --- a/tests/scientific/test_spont_minis.py +++ b/tests/scientific/test_spont_minis.py @@ -31,10 +31,6 @@ def sonata_config_file(sonata_config): os.unlink(config_file.name) -@pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run" -) def test_spont_minis(sonata_config_file): from neurodamus.connection_manager import Nd, SynapseRuleManager from neurodamus import Neurodamus diff --git a/tests/test_synapses.py b/tests/scientific/test_synapses.py similarity index 95% rename from tests/test_synapses.py rename to tests/scientific/test_synapses.py index cb4bc906..4341eeaa 100644 --- a/tests/test_synapses.py +++ b/tests/scientific/test_synapses.py @@ -5,6 +5,7 @@ from pathlib import Path from tempfile import NamedTemporaryFile +USECASE3 = Path(__file__).parent.parent.absolute() / "simulations" / "usecase3" # BlueConfig string BC_str = """ @@ -106,13 +107,6 @@ def blueconfig1(): @pytest.mark.slow -@pytest.mark.forked -@pytest.mark.skipif( - not os.path.isfile("/gpfs/bbp.cscs.ch/project/proj83/circuits/Bio_M/20200805/circuit.mvd3"), - reason="Circuit file not available") -@pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run") @pytest.mark.skipif( "bluepy" not in os.environ.get("PYTHONPATH"), reason="Test requires bluepy run") @@ -311,13 +305,9 @@ def constrained_hill(K_half): npt.assert_allclose(scale_factors(a, b), _constrained_hill(a, b)) -@pytest.mark.skipif( - not os.environ.get("NEURODAMUS_NEOCORTEX_ROOT"), - reason="Test requires loading a neocortex model to run") def test_no_edge_creation(capsys): from neurodamus.node import Node - USECASE3 = Path(__file__).parent.absolute() / "simulations" / "usecase3" contents = """ { "manifest": { diff --git a/tests/unit/README.md b/tests/unit/README.md new file mode 100644 index 00000000..84650a49 --- /dev/null +++ b/tests/unit/README.md @@ -0,0 +1,10 @@ +## Neurodamus Unit Tests + +This folder contains the unit tests for Neurodamus. + +By definition a unit test shoult test a small functionality in isolation, which might require +the use of mocks or even monkey-patching. + +In particular tests here should not rely on processing done by dependencies, especially NEURON, +since it will not be fully configured. Those tests should instead be in the 'integration' test +folder. Nevertheless, to ease life a bit, essential libraries like numpy and scipy can be relied on. diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 00000000..1354ff4b --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1,30 @@ +import pytest +import sys +import unittest + + +class MockParallelExec: + """We need to create a replacement since a magic-mock will return garbage""" + + def allreduce(self, number, _op): + return number + + +@pytest.fixture(autouse=True, scope="module") +def _mock_neuron(): + from neurodamus.core import _mpi + from neurodamus.utils import compat + _mpi._MPI._pc = MockParallelExec() + + # Dont convert + compat.hoc_vector = lambda x: x + + class VectorMock(compat.Vector): + + def __new__(cls, len=0): + init = [0] * len + return compat.Vector.__new__(cls, "d", init) + + neuron_mock = unittest.mock.Mock() + neuron_mock.h.Vector = VectorMock + sys.modules['neuron'] = neuron_mock diff --git a/tests/test_blueconfig_parser.py b/tests/unit/test_blueconfig_parser.py similarity index 94% rename from tests/test_blueconfig_parser.py rename to tests/unit/test_blueconfig_parser.py index 8a751121..d1ba568a 100644 --- a/tests/test_blueconfig_parser.py +++ b/tests/unit/test_blueconfig_parser.py @@ -3,7 +3,7 @@ from pathlib import Path from contextlib import contextmanager -BLUECONFIG_FILE = Path(__file__).parent.absolute() / "simulations" / "complex.blueconfig" +BLUECONFIG_FILE = Path(__file__).parent.parent.absolute() / "simulations" / "complex.blueconfig" def test_blueconfig_parser(): diff --git a/tests/test_conn_manager.py b/tests/unit/test_conn_manager.py similarity index 94% rename from tests/test_conn_manager.py rename to tests/unit/test_conn_manager.py index 5a585402..2abe4265 100644 --- a/tests/test_conn_manager.py +++ b/tests/unit/test_conn_manager.py @@ -1,12 +1,8 @@ import pytest -from pathlib import Path from unittest import mock from neurodamus.connection_manager import ConnectionSet -SIM_DIR = Path(__file__).parent.absolute() / "simulations" -CIRCUIT_PATH = "/gpfs/bbp.cscs.ch/project/proj12/jenkins/cellular/circuit-2k" - class _FakeConn: def __init__(self, sgid, tgid): @@ -76,7 +72,7 @@ def test_population_all_conns(): assert expected[i] == (conn.sgid, conn.tgid) -@pytest.mark.parametrize("test_input, expected", [ +@pytest.mark.parametrize(("test_input", "expected"), [ ((1,), [(0, 1), (1, 1)]), (([1],), [(0, 1), (1, 1)]), (([1, 2],), [(0, 1), (1, 1), (1, 2)]), @@ -105,7 +101,7 @@ def test_population_delete(): assert expected[i] == (conn.sgid, conn.tgid) -@pytest.mark.parametrize("test_input, expected", [ +@pytest.mark.parametrize(("test_input", "expected"), [ ((1,), [(0, 0), (1, 0), (1, 2)]), (([1],), [(0, 0), (1, 0), (1, 2)]), (([1, 2],), [(0, 0), (1, 0)]), diff --git a/tests/test_coords.py b/tests/unit/test_coords.py similarity index 100% rename from tests/test_coords.py rename to tests/unit/test_coords.py diff --git a/tests/test_core_config.py b/tests/unit/test_core_config.py similarity index 100% rename from tests/test_core_config.py rename to tests/unit/test_core_config.py diff --git a/tests/test_dry_run.py b/tests/unit/test_dry_run.py similarity index 100% rename from tests/test_dry_run.py rename to tests/unit/test_dry_run.py diff --git a/tests/test_nodeset.py b/tests/unit/test_nodeset.py similarity index 96% rename from tests/test_nodeset.py rename to tests/unit/test_nodeset.py index 6719ef86..473a6cda 100644 --- a/tests/test_nodeset.py +++ b/tests/unit/test_nodeset.py @@ -44,7 +44,7 @@ def test_NodeSet_add(): assert set_right.offset == 4000 -@pytest.mark.parametrize("ranges1, ranges2, expected", [ +@pytest.mark.parametrize(("ranges1", "ranges2", "expected"), [ ([(0, 10), (20, 30)], [(8, 23), (28, 35)], numpy.array([8, 9, 20, 21, 22, 28, 29])), ([(0, 10), (20, 30)], [(10, 20)], []), ([(5, 10), (20, 30)], [(0, 10)], numpy.arange(5, 10)), @@ -58,7 +58,7 @@ def test_ranges_overlap(ranges1, ranges2, expected): numpy.testing.assert_array_equal(out, expected) -@pytest.mark.parametrize("ranges1, vec, expected", [ +@pytest.mark.parametrize(("ranges1", "vec", "expected"), [ ([(0, 10), (20, 30)], [1, 2, 11, 12, 19, 20, 21, 29, 30], [1, 2, 20, 21, 29]), ([(0, 10), (20, 30)], [11, 12], []), ([], [], []), diff --git a/tests/test_nodeset_target.py b/tests/unit/test_nodeset_target.py similarity index 86% rename from tests/test_nodeset_target.py rename to tests/unit/test_nodeset_target.py index 7ccbff9e..5f1274dc 100644 --- a/tests/test_nodeset_target.py +++ b/tests/unit/test_nodeset_target.py @@ -1,9 +1,13 @@ import numpy +import pytest +@pytest.mark.forked def test_get_local_gids(): - from neurodamus.core.nodeset import NodeSet + + from neurodamus.core.nodeset import NodeSet, PopulationNodes from neurodamus.target_manager import NodesetTarget + PopulationNodes.reset() nodes_popA = NodeSet([1, 2]).register_global("pop_A") nodes_popB = NodeSet([1, 2]).register_global("pop_B") local_gids = [NodeSet([1]).register_global("pop_A"), NodeSet([2]).register_global("pop_B")] diff --git a/tests/test_pyutils.py b/tests/unit/test_pyutils.py similarity index 100% rename from tests/test_pyutils.py rename to tests/unit/test_pyutils.py diff --git a/tests/test_replay_manager.py b/tests/unit/test_replay_manager.py similarity index 90% rename from tests/test_replay_manager.py rename to tests/unit/test_replay_manager.py index e9a69c0e..5ff96d80 100644 --- a/tests/test_replay_manager.py +++ b/tests/unit/test_replay_manager.py @@ -2,7 +2,7 @@ import numpy.testing as npt from pathlib import Path -SAMPLE_DATA_DIR = Path(__file__).parent.absolute() / "sample_data" +SAMPLE_DATA_DIR = Path(__file__).parent.parent.absolute() / "sample_data" @pytest.mark.forked diff --git a/tests/unit/test_stimuli.py b/tests/unit/test_stimuli.py new file mode 100644 index 00000000..39c81886 --- /dev/null +++ b/tests/unit/test_stimuli.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +""" +Test suite for the new-gen Stimuli source (replacing TStim.hoc and parts of StimManager) +""" +import pytest +from neurodamus.core import CurrentSource +from neurodamus.core.random import Random123 + + +class TestStimuli: + def setup_method(self): + rng = Random123(1, 2, 3) + self.stim = CurrentSource(rng=rng) + + def test_flat_segment(self): + self.stim.add_segment(1.2, 10) + assert list(self.stim.time_vec) == [0, 10] + assert list(self.stim.stim_vec) == [1.2, 1.2] + + def test_pulse(self): + self.stim.add_pulse(1.2, 10) + assert list(self.stim.time_vec) == [0, 0, 10, 10] + assert list(self.stim.stim_vec) == [0, 1.2, 1.2, 0] + + @pytest.mark.parametrize("base_amp", [-1, 0, 1.5]) + def test_pulse_diff_base(self, base_amp): + self.stim.add_pulse(1.2, 10, base_amp=base_amp) + assert list(self.stim.time_vec) == [0, 0, 10, 10] + assert list(self.stim.stim_vec) == [base_amp, 1.2, 1.2, base_amp] + + def test_two_pulses(self): + self.stim.add_pulse(1.2, 10) + self.stim.delay(5) + self.stim.add_pulse(0.8, 5) + assert list(self.stim.time_vec) == [0, 0, 10, 10, 15, 15, 20, 20] + assert list(self.stim.stim_vec) == [0, 1.2, 1.2, 0, 0, 0.8, 0.8, 0] + + def test_ramp(self): + self.stim.add_ramp(5, 7.5, 10) + assert list(self.stim.time_vec) == [0, 0, 10, 10] + assert list(self.stim.stim_vec) == [0, 5, 7.5, 0] + + def test_delay_ramp(self): + # When a delay is specified (in ctor or factory) base_amp is set on t=0 too + sig = CurrentSource.ramp(1, 2, 2, base_amp=-1, delay=10) + assert list(sig.time_vec) == [0, 10, 10, 12, 12] + assert list(sig.stim_vec) == [-1, -1, 1, 2, -1] + + def test_train(self): + self.stim.add_train(1.2, 10, 20, 350) + # At 10Hz pulses have T=100ms + # We end up with 4 pulses, the last one with reduced rest phase + assert list(self.stim.time_vec) == [0, 0, 20, 20, 100, 100, 120, 120, 200, 200, 220, 220, + 300, 300, 320, 320, 350] + assert list(self.stim.stim_vec) == [0, 1.2, 1.2, 0] * 4 + [0] diff --git a/tests/test_target_intersection.py b/tests/unit/test_target_intersection.py similarity index 94% rename from tests/test_target_intersection.py rename to tests/unit/test_target_intersection.py index bd6e6b58..dc9b9687 100644 --- a/tests/test_target_intersection.py +++ b/tests/unit/test_target_intersection.py @@ -93,7 +93,7 @@ def test_nodeset_gids(): local_nodes=[local_nodes_popA, local_nodes_popB] ) gids = t.gids() - npt.assert_array_equal(gids.as_numpy(), [5, 6, 1003, 1004, 1005]) + npt.assert_array_equal(gids, [5, 6, 1003, 1004, 1005]) t2 = NodesetTarget( "target-b", @@ -101,7 +101,7 @@ def test_nodeset_gids(): local_nodes=[local_nodes_popA] ) gids = t2.gids() - npt.assert_array_equal(gids.as_numpy(), [5, 6]) + npt.assert_array_equal(gids, [5, 6]) t3 = NodesetTarget( "target-c", @@ -109,7 +109,7 @@ def test_nodeset_gids(): local_nodes=[local_nodes_popA, local_nodes_popB] ) gids = t3.gids() - npt.assert_array_equal(gids.as_numpy(), [1003, 1004, 1005]) + npt.assert_array_equal(gids, [1003, 1004, 1005]) t4 = NodesetTarget( "target-d", @@ -117,4 +117,4 @@ def test_nodeset_gids(): local_nodes=[local_nodes_popA] ) gids = t4.gids() - npt.assert_array_equal(gids.as_numpy(), []) + npt.assert_array_equal(gids, []) diff --git a/tox.ini b/tox.ini index 5306ebb6..5626a33d 100644 --- a/tox.ini +++ b/tox.ini @@ -2,16 +2,21 @@ name = neurodamus [tox] -envlist = flake8, py3 +envlist = flake8, unit -[testenv] +[testenv:unit] +deps = + -r tests/requirements.txt +commands = + pytest tests/unit + + +[testenv:integration] deps = NEURON - pytest - pytest-forked - scipy morphio + -r tests/requirements.txt passenv = NEURODAMUS_NEOCORTEX_ROOT setenv = # build_ndcore.sh will install into _lib @@ -23,17 +28,14 @@ allowlist_externals = {toxinidir}/ci/build_ndcore.sh commands = {toxinidir}/ci/build_ndcore.sh {toxinidir}/core - python setup.py test + pytest -x --forked tests/integration -[testenv:bb5] +[testenv:bbp-model] # Please module load neurodamus-neocortex py-neurodamus beforehand passenv = * deps = - pytest - pytest-cov - pytest-forked - pytest-xdist + -r tests/requirements.txt setenv = PYTHONPATH={toxinidir}:{env:PYTHONPATH} HOC_LIBRARY_PATH={toxinidir}/core/hoc:{env:HOC_LIBRARY_PATH} @@ -43,7 +45,7 @@ allowlist_externals = commands = /usr/bin/echo {env:PYTHONPATH} /usr/bin/echo {env:HOC_LIBRARY_PATH} - pytest -s -x -n2 --forked --cov=neurodamus + pytest -s -x --forked --durations=5 --durations-min=15 {posargs:tests/integration-e2e} [testenv:flake8]