Skip to content

Commit

Permalink
Merge pull request #761 from apdavison/pytest
Browse files Browse the repository at this point in the history
Migrate test suite from nose to pytest
  • Loading branch information
apdavison committed Oct 16, 2022
2 parents d76b4d0 + 6b4be03 commit 9dfb42b
Show file tree
Hide file tree
Showing 46 changed files with 769 additions and 858 deletions.
34 changes: 28 additions & 6 deletions .github/workflows/full-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
- name: Install basic Python dependencies
run: |
python -m pip install --upgrade pip
python -m pip install coverage coveralls nose-testconfig
python -m pip install pytest pytest-cov coveralls
python -m pip install mpi4py
python -m pip install -r requirements.txt
- name: Install Brian 2
Expand All @@ -53,10 +53,32 @@ jobs:
make install
- name: Install PyNN itself
run: |
python setup.py install
- name: Run unit tests
pip install -e .
- name: Compile NEURON mechanisms (Linux)
if: startsWith(matrix.os, 'ubuntu')
run: |
pushd pyNN/neuron/nmodl
nrnivmodl
popd
- name: Run unit and system tests
run: |
nosetests --nologcapture --where=test/unittests --verbosity=2 test_assembly.py test_brian.py test_connectors_parallel.py test_connectors_serial.py test_core.py test_descriptions.py test_files.py test_idmixin.py test_lowlevelapi.py test_nest.py test_neuron.py test_parameters.py test_population.py test_populationview.py test_projection.py test_random.py test_recording.py test_simulation_control.py test_space.py test_standardmodels.py test_utility_functions.py
- name: Run system tests
pytest -v --cov=pyNN --cov-report=term test
- name: Upload coverage data
run: |
coveralls --service=github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_FLAG_NAME: ${{ matrix.test-name }}
COVERALLS_PARALLEL: true
coveralls:
name: Indicate completion to coveralls.io
needs: test
runs-on: ubuntu-latest
container: python:3-slim
steps:
- name: Finished
run: |
nosetests --nologcapture --where=test/system --verbosity=2 test_nest.py test_brian2.py test_neuron.py
pip3 install --upgrade coveralls
coveralls --service=github --finish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
6 changes: 3 additions & 3 deletions ci/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
set -e # stop execution in case of errors

pip install -r requirements.txt
pip install coverage coveralls
pip install nose-testconfig
pip install coveralls
pip install pytest pytest-cov
source ci/install_brian.sh
source ci/install_nest.sh
source ci/install_neuron.sh
python setup.py install
python setup.py develop
6 changes: 4 additions & 2 deletions ci/test_script.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
set -e # stop execution in case of errors

if [ "$TRAVIS_PYTHON_VERSION" == "3.9" ]; then
python setup.py nosetests --verbose --nologcapture --with-coverage --cover-package=pyNN --tests=test;
pytest --verbose --cov=pyNN
else
python setup.py nosetests --verbose --nologcapture -e backends --tests=test/unittests
pytest --verbose test/unittests
fi

exit $?
2 changes: 1 addition & 1 deletion doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class MockNESTModule(mock.Mock):
# The short X.Y version.
version = '0.10'
# The full version, including alpha/beta/rc tags.
release = '0.10.1'
release = '0.10.2.dev'

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
Expand Down
43 changes: 16 additions & 27 deletions doc/developers/contributing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Requirements
In addition to the requirements listed in :doc:`../installation`, you will need to
install:

* nose_
* pytest_
* mock_
* coverage_

Expand Down Expand Up @@ -86,15 +86,14 @@ We try to stay fairly close to PEP8_. Please note in particular:
Testing
=======

Running the PyNN test suite requires the *nose_*, *mock_* and *nose-testconfig* packages,
and optionally the *coverage_* package. To run the entire test suite, in the
``test`` subdirectory of the source tree::
Running the PyNN test suite requires the *pytest_* package,
and optionally the *pytest-cov* package. To run the entire test suite::

$ nosetests
$ pytest

To see how well the codebase is covered by the tests, run::

$ nosetests --with-coverage --cover-package=pyNN --cover-erase --cover-html
$ pytest --cov=pyNN --cov-report term --cov-report html

There are currently two sorts of tests, unit tests, which aim to exercise
small pieces of code such as individual functions and methods, and system tests,
Expand All @@ -110,31 +109,21 @@ Except when testing a specific simulator interface, unit tests should be able to
run without a simulator installed.

System tests should be written so that they can run with any of the simulators.
The suggested way to do this is to write test functions, in a separate file,
that take a simulator module as an argument, and then call these functions from
``test_neuron.py``, ``test_nest.py``, etc.
The suggested way to do this is to write test functions
that take a simulator module as an argument, and then wrap these functions
with the following decorator::

System tests defined in the scenarios directory are treated as a single test
(test_scenarios()) while running nosetests. To run only the tests within a file
@pytest.mark.parametrize("sim", (pyNN.nest, pyNN.neuron, pyNN.brian2))

To run only the tests within a file
named 'test_electrodes' located inside system/scenarios, use::

$ nosetests -s --tc=testFile:test_electrodes test_nest.py
$ pytest test/system/scenarios/test_electrodes.py

To run a single specific test named 'test_changing_electrode' located within
some file (and added to registry) inside system/scenarios, use::

$ nosetests -s --tc=testName:test_changing_electrode test_nest.py

Note that this would also run the tests specified within the simulator specific
files such as test_brian.py, test_nest.py and test_neuron.py. To avoid
this, specify the 'test_scenarios function' on the command line::

$ nosetests -s --tc=testName:test_changing_electrode test_nest.py:test_scenarios

The ``test/unsorted`` directory contains a number of old tests that are either
no longer useful or have not yet been adapted to the nose framework. These are
not part of the test suite, but we are gradually adapting those tests that are
useful and deleting the others.
$ pytest test/system/scenarios/test_electrodes.py::test_changing_electrode


Submitting code
Expand Down Expand Up @@ -190,11 +179,11 @@ in becoming release manager for PyNN, please contact us via the `mailing list`_.
When you think a release is ready, run through the following checklist one
last time:

* do all the tests pass? This means running :command:`nosetests` in
* do all the tests pass? This means running :command:`pytest` in
:file:`test/unittests` and :file:`test/system` and running :command:`make doctest` in
:file:`doc`. You should do this on at least two Linux systems -- one a very
recent version and one at least a year old, and on at least one version of
Mac OS X. You should also do this with Python 2.7 and 3.4, 3.5 or 3.6.
Mac OS X. You should also do this with multiple Python versions (3.7+).
* do all the example scripts generate the correct output? Run the
:file:`run_all_examples.py` script in :file:`examples/tools` and then visually
check the :file:`.png` files generated in :file:`examples/tools/Results`. Again,
Expand Down Expand Up @@ -254,7 +243,7 @@ If this is a final release, there are a few more steps:

.. _Sphinx: http://sphinx-doc.org/
.. _PEP8: http://www.python.org/dev/peps/pep-0008/
.. _nose: https://nose.readthedocs.org/
.. _pytest: https://docs.pytest.org
.. _mock: http://www.voidspace.org.uk/python/mock/
.. _coverage: http://nedbatchelder.com/code/coverage/
.. _`Python Package Index`: http://pypi.python.org/
Expand Down
2 changes: 1 addition & 1 deletion pyNN/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
:license: CeCILL, see LICENSE for details.
"""

__version__ = '0.10.1'
__version__ = '0.10.2.dev'
__all__ = ["common", "random", "nest", "neuron", "brian2",
"recording", "errors", "space", "descriptions",
"standardmodels", "parameters", "core", "serialization"]
11 changes: 5 additions & 6 deletions pyNN/common/populations.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ def sample(self, n, rng=None):
assert isinstance(n, int)
if not rng:
rng = random.NumpyRNG()
indices = rng.permutation(np.arange(len(self), dtype=np.int))[0:n]
indices = rng.permutation(np.arange(len(self), dtype=int))[0:n]
logger.debug("The %d cells selected have indices %s" % (n, indices))
logger.debug("%s.sample(%s)", self.label, n)
return self._get_view(indices)
Expand Down Expand Up @@ -658,7 +658,6 @@ def __init__(self, size, cellclass, cellparams=None, structure=None,
else:
raise Exception(
"A maximum of 3 dimensions is allowed. What do you think this is, string theory?")
# NEST doesn't like np.int, so to be safe we cast to Python int
size = int(reduce(operator.mul, size))
self.size = size
self.label = label or 'population%d' % Population._nPop
Expand Down Expand Up @@ -718,7 +717,7 @@ def id_to_index(self, id):
if (self.first_id > id.min()) or (self.last_id < id.max()):
raise ValueError("ids should be in the range [%d,%d], actually [%d, %d]" % (
self.first_id, self.last_id, id.min(), id.max()))
return (id - self.first_id).astype(np.int) # this assumes ids are consecutive
return (id - self.first_id).astype(int) # this assumes ids are consecutive

def id_to_local_index(self, id):
"""
Expand Down Expand Up @@ -906,7 +905,7 @@ def id_to_index(self, id):
if self._is_sorted:
return np.searchsorted(self.all_cells, id)
else:
result = np.array([], dtype=np.int)
result = np.array([], dtype=int)
for item in id:
data = np.where(self.all_cells == item)[0]
if len(data) == 0:
Expand Down Expand Up @@ -1159,7 +1158,7 @@ def id_to_index(self, id):
if self._is_sorted:
return np.searchsorted(all_cells, id)
else:
result = np.array([], dtype=np.int)
result = np.array([], dtype=int)
for item in id:
data = np.where(all_cells == item)[0]
if len(data) == 0:
Expand Down Expand Up @@ -1261,7 +1260,7 @@ def sample(self, n, rng=None):
assert isinstance(n, int)
if not rng:
rng = random.NumpyRNG()
indices = rng.permutation(np.arange(len(self), dtype=np.int))[0:n]
indices = rng.permutation(np.arange(len(self), dtype=int))[0:n]
logger.debug("The %d cells recorded have indices %s" % (n, indices))
logger.debug("%s.sample(%s)", self.label, n)
return self[indices]
Expand Down
3 changes: 2 additions & 1 deletion pyNN/connectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from pyNN.recording import files
from pyNN.parameters import LazyArray
from pyNN.standardmodels import StandardSynapseType
from pyNN.common import Population
import numpy as np
from itertools import repeat
import logging
Expand Down Expand Up @@ -252,6 +251,7 @@ def _connect_with_map(self, projection, connection_map, distance_map=None):
self._standard_connect(projection, connection_map.by_column, distance_map)

def _get_connection_map_no_self_connections(self, projection):
from pyNN.common import Population
if (isinstance(projection.pre, Population)
and isinstance(projection.post, Population)
and projection.pre == projection.post):
Expand All @@ -267,6 +267,7 @@ def _get_connection_map_no_self_connections(self, projection):
return connection_map

def _get_connection_map_no_mutual_connections(self, projection):
from pyNN.common import Population
if (isinstance(projection.pre, Population)
and isinstance(projection.post, Population)
and projection.pre == projection.post):
Expand Down
2 changes: 1 addition & 1 deletion pyNN/nest/recording.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def _get_data_arrays(self, variable, clear=False):
if variable == "times":
values = times
else:
# I'm hoping numpy optimises for the case where scale_factor = 1,
# I'm hoping numpy optimises for the case where scale_factor = 1,
# otherwise should avoid this multiplication in that case
values = events[nest_variable] * scale_factor

Expand Down
2 changes: 1 addition & 1 deletion pyNN/neuron/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def setup(timestep=DEFAULT_TIMESTEP, min_delay=DEFAULT_MIN_DELAY, **extra_params
simulator.state.min_delay = min_delay
simulator.state.max_delay = extra_params.get('max_delay', DEFAULT_MAX_DELAY)
if 'use_cvode' in extra_params:
simulator.state.record_sample_times = extra_params['use_cvode']
simulator.state.record_sample_times = extra_params['use_cvode']
simulator.state.cvode.active(int(extra_params['use_cvode']))
if 'rtol' in extra_params:
simulator.state.cvode.rtol(float(extra_params['rtol']))
Expand Down
5 changes: 1 addition & 4 deletions pyNN/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@
"""

import numpy as np
try:
from collections import Sized
except ImportError:
from collections.abc import Sized
from collections.abc import Sized
from pyNN.core import is_listlike
from pyNN import errors
from pyNN.random import RandomDistribution, NativeRNG
Expand Down
4 changes: 2 additions & 2 deletions pyNN/recording/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,9 +303,9 @@ def _get_current_segment(self, filter_ids=None, variables='all', clear=False):
times = times[mask]
id_array = id_array[mask]
segment.spiketrains = neo.spiketrainlist.SpikeTrainList.from_spike_time_array(
times, id_array,
times, id_array,
np.array(sids, dtype=int),
t_stop=t_stop,
t_stop=t_stop,
units="ms",
t_start=self._recording_start_time,
source_population=self.population.label
Expand Down
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,4 @@ lazyarray>=0.5.2
neo>=0.11.0
#git+https://github.com/NeuralEnsemble/python-neo.git@master#egg=neo
setuptools>=20.5
nose
h5py
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def find(self, command):

setup(
name="PyNN",
version="0.10.1",
version="0.10.2.dev",
packages=['pyNN', 'pyNN.nest', 'pyNN.neuron',
'pyNN.brian2', 'pyNN.common', 'pyNN.mock', 'pyNN.neuroml',
'pyNN.recording', 'pyNN.standardmodels', 'pyNN.descriptions',
Expand Down
24 changes: 0 additions & 24 deletions test/system/scenarios/__init__.py
Original file line number Diff line number Diff line change
@@ -1,24 +0,0 @@
# encoding: utf-8
from testconfig import config


if 'testFile' in config:
file_name = config['testFile']
exec("from . import ( %s )" % file_name)
else:
from . import (scenario1,
scenario2,
scenario3,
ticket166,
test_simulation_control,
test_recording,
test_cell_types,
test_electrodes,
scenario4,
test_parameter_handling,
test_procedural_api,
issue274,
test_connectors,
issue231,
test_connection_handling,
test_synapse_types)
44 changes: 44 additions & 0 deletions test/system/scenarios/fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import functools
import pytest

available_modules = {}

try:
import pyNN.neuron
available_modules["neuron"] = pyNN.neuron
except ImportError:
pass

try:
import pyNN.nest
available_modules["nest"] = pyNN.nest
except ImportError:
pass

try:
import pyNN.brian2
available_modules["brian2"] = pyNN.brian2
except ImportError:
pass


class SimulatorNotAvailable:

def __init__(self, sim_name):
self.sim_name = sim_name

def setup(self, *args, **kwargs):
pytest.skip(f"{self.sim_name} not available")


def get_simulator(sim_name):
if sim_name in available_modules:
return pytest.param(available_modules[sim_name], id=sim_name)
else:
return pytest.param(SimulatorNotAvailable(sim_name), id=sim_name)


def run_with_simulators(*sim_names):
sim_modules = (get_simulator(sim_name) for sim_name in sim_names)

return pytest.mark.parametrize("sim", sim_modules)

0 comments on commit 9dfb42b

Please sign in to comment.