shithub: aubio

Download patch

ref: 0045668fdecf33e793f270f6a251b07224b7ad40
parent: a617bf31cb29614eae92649034696100726ba87d
parent: 74c1fb9a63dfa46c0591816bd2f450c5f2dba751
author: Paul Brossier <piem@piem.org>
date: Wed Dec 19 13:29:13 EST 2018

Merge branch 'feature/pytest' (closes #163)

--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -75,4 +75,4 @@
 
 test_script:
   - "python python\\demos\\demo_create_test_sounds.py"
-  - "nose2 --verbose"
+  - "pytest --verbose"
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -19,16 +19,16 @@
   command: |
     pip install --user dist/aubio*.whl
 
-test-nose2: &test-nose2
+test-pytest: &test-pytest
   name: Test python wheel
   command: |
     make create_test_sounds
-    PATH=/home/circleci/.local/bin:$PATH nose2 -v
+    PATH=/home/circleci/.local/bin:$PATH pytest -v
 
-test-nose2-nosounds: &test-nose2-nosounds
+test-pytest-nosounds: &test-pytest-nosounds
   name: Test python wheel
   command: |
-    PATH=/home/circleci/.local/bin:$PATH nose2 -v
+    PATH=/home/circleci/.local/bin:$PATH pytest -v
 
 uninstall-wheel: &uninstall-wheel
   name: Uninstall python wheel
@@ -47,7 +47,7 @@
       - run: *pip-install
       - run: *build-wheel
       - run: *install-wheel
-      - run: *test-nose2
+      - run: *test-pytest
       - run: *uninstall-wheel
       - store_artifacts:
           path: dist/
@@ -61,7 +61,7 @@
       - run: *pip-install
       - run: *build-wheel
       - run: *install-wheel
-      - run: *test-nose2
+      - run: *test-pytest
       - run: *uninstall-wheel
       - store_artifacts:
           path: dist/
@@ -75,7 +75,7 @@
       - run: *pip-install
       - run: *build-wheel
       - run: *install-wheel
-      - run: *test-nose2
+      - run: *test-pytest
       - run: *uninstall-wheel
       - store_artifacts:
           path: dist/
@@ -88,7 +88,7 @@
       - run: *pip-install
       - run: *build-wheel
       - run: *install-wheel
-      - run: *test-nose2-nosounds
+      - run: *test-pytest-nosounds
       - run: *uninstall-wheel
       - store_artifacts:
           path: dist/
--- a/.travis.yml
+++ b/.travis.yml
@@ -85,6 +85,7 @@
   - travis_retry make getwaf expandwaf deps_python
   - which pip
   - pip --version
+  - pip install coverage
 
 script:
   - make create_test_sounds
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -6,16 +6,14 @@
 recursive-include waflib *.py
 include Makefile wscript */wscript_build
 include aubio.pc.in
-include nose2.cfg
 include requirements.txt
 include src/*.c src/*.h
 include src/*/*.c src/*/*.h
 include examples/*.c examples/*.h
-include tests/*.h tests/*/*.c tests/*/*/*.c
+recursive-include tests *.h *.c *.py
 include python/ext/*.h
 recursive-include python *.py
 include python/README.md
-include python/tests/run_all_tests
 include python/tests/eval_pitch
 include python/tests/*.expected
 include doc/*.txt doc/*.rst doc/*.cfg doc/Makefile doc/make.bat doc/conf.py
--- a/Makefile
+++ b/Makefile
@@ -35,8 +35,8 @@
 DATAROOTDIR?=$(PREFIX)/share
 MANDIR?=$(DATAROOTDIR)/man
 
-# default nose2 command
-NOSE2?=nose2 -N 4 --verbose
+# default python test command
+PYTEST?=pytest --verbose
 
 SOX=sox
 
@@ -140,9 +140,7 @@
 test_python: export PYTHONPATH=$(PYDESTDIR)/$(LIBDIR)
 test_python: local_dylib
 	# run test with installed package
-	# ./python/tests/run_all_tests --verbose
-	# run with nose2, multiple processes
-	$(NOSE2)
+	$(PYTEST)
 
 clean_python:
 	./setup.py clean
@@ -253,7 +251,7 @@
 	# build and test python
 	pip install -v -e .
 	# run tests, with python coverage
-	coverage run `which nose2`
+	coverage run `which pytest`
 	# capture coverage again
 	lcov $(LCOVOPTS) --capture --no-external --directory . \
 		--output-file build/coverage_python.info
--- a/doc/python_module.rst
+++ b/doc/python_module.rst
@@ -89,14 +89,15 @@
 Python tests
 ------------
 
-A number of Python tests are provided in the `python tests`_. To run them,
-install `nose2`_ and run the script ``python/tests/run_all_tests``:
+A number of Python tests are provided in the `python/tests`_ folder. To run
+them, install `pytest`_ and run it from the aubio source directory:
 
 .. code-block:: console
 
-    $ pip install nose2
-    $ ./python/tests/run_all_tests
+    $ pip install pytest
+    $ git clone https://git.aubio.org/aubio/aubio
+    $ cd aubio
+    $ pytest
 
-.. _demo_filter.py: https://github.com/aubio/aubio/blob/master/python/demos/demo_filter.py
-.. _python tests: https://github.com/aubio/aubio/blob/master/python/tests
-.. _nose2: https://github.com/nose-devs/nose2
+.. _python/tests: https://github.com/aubio/aubio/blob/master/python/tests
+.. _pytest: https://pytest.org
--- a/nose2.cfg
+++ /dev/null
@@ -1,6 +1,0 @@
-[unittest]
-start-dir = python/tests/
-plugins = nose2.plugins.mp
-
-[multiprocess]
-always-on = false
--- a/python/README.md
+++ b/python/README.md
@@ -27,7 +27,7 @@
 Demos
 -----
 
-Some examples are available in the [`python/demos`][demos_dir] folder. Each
+Some examples are available in the [`python/demos` folder][demos_dir]. Each
 script is a command line program which accepts one ore more argument.
 
 **Notes**: installing additional modules is required to run some of the demos.
--- a/python/tests/__init__.py
+++ /dev/null
@@ -1,1 +1,0 @@
-
--- /dev/null
+++ b/python/tests/_tools.py
@@ -1,0 +1,41 @@
+"""
+This file imports test methods from different testing modules, in this
+order:
+
+    - try importing 'pytest'
+    - if it fails, fallback to 'numpy.testing'
+
+Nose2 support was removed because of lacking assertWarns on py2.7.
+
+"""
+
+import sys
+
+_has_pytest = False
+
+# check if we have pytest
+try:
+    import pytest
+    parametrize = pytest.mark.parametrize
+    assert_raises = pytest.raises
+    assert_warns = pytest.warns
+    skipTest = pytest.skip
+    _has_pytest = True
+    def run_module_suite():
+        import sys, pytest
+        pytest.main(sys.argv)
+except:
+    pass
+
+# otherwise fallback on numpy.testing
+if not _has_pytest:
+    from numpy.testing import dec, assert_raises, assert_warns
+    from numpy.testing import SkipTest
+    parametrize = dec.parametrize
+    def skipTest(msg):
+        raise SkipTest(msg)
+    from numpy.testing import run_module_suite
+
+# always use numpy's assert_equal
+import numpy
+assert_equal = numpy.testing.assert_equal
--- a/python/tests/run_all_tests
+++ /dev/null
@@ -1,5 +1,0 @@
-#! /usr/bin/env python
-
-if __name__ == '__main__':
-    import nose2.main
-    nose2.discover()
--- a/python/tests/test_aubio.py
+++ b/python/tests/test_aubio.py
@@ -1,6 +1,5 @@
 #! /usr/bin/env python
 
-from unittest import main
 from numpy.testing import TestCase
 
 class aubiomodule_test_case(TestCase):
@@ -15,5 +14,5 @@
         self.assertEqual('0', aubio.version[0])
 
 if __name__ == '__main__':
+    from unittest import main
     main()
-
--- a/python/tests/test_aubio_cmd.py
+++ b/python/tests/test_aubio_cmd.py
@@ -1,8 +1,7 @@
 #! /usr/bin/env python
 
-import aubio.cmd
-from nose2 import main
 from numpy.testing import TestCase
+import aubio.cmd
 
 class aubio_cmd(TestCase):
 
@@ -31,4 +30,5 @@
                 "3200\t")
 
 if __name__ == '__main__':
+    from unittest import main
     main()
--- a/python/tests/test_aubio_cut.py
+++ b/python/tests/test_aubio_cut.py
@@ -1,7 +1,6 @@
 #! /usr/bin/env python
 
 import aubio.cut
-from nose2 import main
 from numpy.testing import TestCase
 
 class aubio_cut(TestCase):
@@ -13,4 +12,5 @@
         assert self.a_parser.parse_args(['-v']).verbose
 
 if __name__ == '__main__':
+    from unittest import main
     main()
--- a/python/tests/test_cvec.py
+++ b/python/tests/test_cvec.py
@@ -1,6 +1,5 @@
 #! /usr/bin/env python
 
-from unittest import main
 import numpy as np
 from numpy.testing import TestCase, assert_equal
 from aubio import cvec, fvec, float_type
@@ -141,4 +140,5 @@
             a.norm = np.zeros((512//2+1, 2), dtype = float_type)
 
 if __name__ == '__main__':
+    from unittest import main
     main()
--- a/python/tests/test_dct.py
+++ b/python/tests/test_dct.py
@@ -66,3 +66,7 @@
                 aubio.dct(size)
         except AssertionError:
             self.skipTest('creating aubio.dct with size %d did not fail' % size)
+
+if __name__ == '__main__':
+    from unittest import main
+    main()
--- a/python/tests/test_fft.py
+++ b/python/tests/test_fft.py
@@ -1,6 +1,5 @@
 #! /usr/bin/env python
 
-from unittest import main
 from numpy.testing import TestCase
 from numpy.testing import assert_equal, assert_almost_equal
 import numpy as np
@@ -212,4 +211,5 @@
             fft(win_s)
 
 if __name__ == '__main__':
+    from unittest import main
     main()
--- a/python/tests/test_filter.py
+++ b/python/tests/test_filter.py
@@ -1,9 +1,8 @@
 #! /usr/bin/env python
 
-from unittest import main
 from numpy.testing import TestCase, assert_equal, assert_almost_equal
 from aubio import fvec, digital_filter
-from .utils import array_from_text_file
+from utils import array_from_text_file
 
 class aubio_filter_test_case(TestCase):
 
@@ -94,4 +93,5 @@
             digital_filter(-1)
 
 if __name__ == '__main__':
+    from unittest import main
     main()
--- a/python/tests/test_filterbank.py
+++ b/python/tests/test_filterbank.py
@@ -4,7 +4,7 @@
 from numpy.testing import TestCase, assert_equal, assert_almost_equal
 
 from aubio import cvec, filterbank, float_type
-from .utils import array_from_text_file
+from utils import array_from_text_file
 
 class aubio_filterbank_test_case(TestCase):
 
@@ -87,5 +87,5 @@
             f(cvec(256))
 
 if __name__ == '__main__':
-    import nose2
-    nose2.main()
+    from unittest import main
+    main()
--- a/python/tests/test_filterbank_mel.py
+++ b/python/tests/test_filterbank_mel.py
@@ -2,12 +2,10 @@
 
 import numpy as np
 from numpy.testing import TestCase, assert_equal, assert_almost_equal
+from _tools import assert_warns
 
 from aubio import fvec, cvec, filterbank, float_type
 
-import warnings
-warnings.filterwarnings('ignore', category=UserWarning, append=True)
-
 class aubio_filterbank_mel_test_case(TestCase):
 
     def test_slaney(self):
@@ -75,8 +73,8 @@
         samplerate = 22050
         freq_list = [0, samplerate//4, samplerate // 2 + 1]
         f = filterbank(len(freq_list)-2, 1024)
-        # TODO add assert_warns
-        f.set_triangle_bands(fvec(freq_list), samplerate)
+        with assert_warns(UserWarning):
+            f.set_triangle_bands(fvec(freq_list), samplerate)
 
     def test_triangle_freqs_with_not_enough_filters(self):
         """make sure set_triangle_bands warns when not enough filters"""
@@ -83,8 +81,8 @@
         samplerate = 22050
         freq_list = [0, 100, 1000, 4000, 8000, 10000]
         f = filterbank(len(freq_list)-3, 1024)
-        # TODO add assert_warns
-        f.set_triangle_bands(fvec(freq_list), samplerate)
+        with assert_warns(UserWarning):
+            f.set_triangle_bands(fvec(freq_list), samplerate)
 
     def test_triangle_freqs_with_too_many_filters(self):
         """make sure set_triangle_bands warns when too many filters"""
@@ -91,8 +89,8 @@
         samplerate = 22050
         freq_list = [0, 100, 1000, 4000, 8000, 10000]
         f = filterbank(len(freq_list)-1, 1024)
-        # TODO add assert_warns
-        f.set_triangle_bands(fvec(freq_list), samplerate)
+        with assert_warns(UserWarning):
+            f.set_triangle_bands(fvec(freq_list), samplerate)
 
     def test_triangle_freqs_with_double_value(self):
         """make sure set_triangle_bands works with 2 duplicate freqs"""
@@ -99,8 +97,8 @@
         samplerate = 22050
         freq_list = [0, 100, 1000, 4000, 4000, 4000, 10000]
         f = filterbank(len(freq_list)-2, 1024)
-        # TODO add assert_warns
-        f.set_triangle_bands(fvec(freq_list), samplerate)
+        with assert_warns(UserWarning):
+            f.set_triangle_bands(fvec(freq_list), samplerate)
 
     def test_triangle_freqs_with_triple(self):
         """make sure set_triangle_bands works with 3 duplicate freqs"""
@@ -107,9 +105,10 @@
         samplerate = 22050
         freq_list = [0, 100, 1000, 4000, 4000, 4000, 10000]
         f = filterbank(len(freq_list)-2, 1024)
-        # TODO add assert_warns
-        f.set_triangle_bands(fvec(freq_list), samplerate)
+        with assert_warns(UserWarning):
+            f.set_triangle_bands(fvec(freq_list), samplerate)
 
+
     def test_triangle_freqs_without_norm(self):
         """make sure set_triangle_bands works without """
         samplerate = 22050
@@ -168,5 +167,5 @@
 
 
 if __name__ == '__main__':
-    import nose2
-    nose2.main()
+    from unittest import main
+    main()
--- a/python/tests/test_fvec.py
+++ b/python/tests/test_fvec.py
@@ -1,6 +1,5 @@
 #! /usr/bin/env python
 
-from unittest import main
 import numpy as np
 from numpy.testing import TestCase, assert_equal, assert_almost_equal
 from aubio import fvec, zero_crossing_rate, alpha_norm, min_removal
@@ -148,4 +147,5 @@
         del c
 
 if __name__ == '__main__':
+    from unittest import main
     main()
--- a/python/tests/test_fvec_shift.py
+++ b/python/tests/test_fvec_shift.py
@@ -30,6 +30,6 @@
     def test_can_shift_fvec_odd(self):
         self.run_shift_ishift(7)
 
-from unittest import main
 if __name__ == '__main__':
+    from unittest import main
     main()
--- a/python/tests/test_hztomel.py
+++ b/python/tests/test_hztomel.py
@@ -3,6 +3,7 @@
 from unittest import main
 from numpy.testing import TestCase
 from numpy.testing import assert_equal, assert_almost_equal
+from _tools import assert_warns
 import numpy as np
 import aubio
 
@@ -37,12 +38,12 @@
             assert_almost_equal(meltohz(hztomel(f)) - f, 0, decimal=1)
 
     def test_meltohz_negative(self):
-        # TODO add assert_warns
-        assert_equal(meltohz(-1), 0)
+        with assert_warns(UserWarning):
+            assert_equal(meltohz(-1), 0)
 
     def test_hztomel_negative(self):
-        # TODO add assert_warns
-        assert_equal(hztomel(-1), 0)
+        with assert_warns(UserWarning):
+            assert_equal(hztomel(-1), 0)
 
 
 class aubio_hztomel_htk_test_case(TestCase):
@@ -57,14 +58,16 @@
         assert_almost_equal(hztomel(6300, htk=True), 2595., decimal=1)
 
     def test_meltohz_negative(self):
-        # TODO add assert_warns
-        assert_equal(meltohz(-1, htk=True), 0)
+        with assert_warns(UserWarning):
+            assert_equal(meltohz(-1, htk=True), 0)
         assert_almost_equal(meltohz(2000, htk=True), 3428.7, decimal=1)
         assert_almost_equal(meltohz(1000, htk=True), 1000., decimal=1)
 
     def test_hztomel_negative(self):
-        # TODO add assert_warns
-        assert_equal(hztomel(-1, htk=True), 0)
+        with assert_warns(UserWarning):
+            assert_equal(meltohz(-1, htk=True), 0)
+        with assert_warns(UserWarning):
+            assert_equal(hztomel(-1, htk=True), 0)
         assert_almost_equal(hztomel(1000, htk=True), 1000., decimal=1)
 
     def test_hztomel_htk(self):
--- a/python/tests/test_mathutils.py
+++ b/python/tests/test_mathutils.py
@@ -1,6 +1,5 @@
 #! /usr/bin/env python
 
-from unittest import main
 from numpy.testing import TestCase, assert_equal
 from numpy import array, arange, isnan, isinf
 from aubio import bintomidi, miditobin, freqtobin, bintofreq, freqtomidi, miditofreq
@@ -101,4 +100,5 @@
         assert_equal ( array(b) < 0, False )
 
 if __name__ == '__main__':
+    from unittest import main
     main()
--- a/python/tests/test_mfcc.py
+++ b/python/tests/test_mfcc.py
@@ -1,7 +1,6 @@
 #! /usr/bin/env python
 
-from nose2 import main
-from nose2.tools import params
+from _tools import parametrize, assert_raises
 from numpy import random, count_nonzero
 from numpy.testing import TestCase
 from aubio import mfcc, cvec, float_type
@@ -15,28 +14,21 @@
 new_params = ['buf_size', 'n_filters', 'n_coeffs', 'samplerate']
 new_deflts = [1024, 40, 13, 44100]
 
-class aubio_mfcc(TestCase):
+class Test_aubio_mfcc(object):
 
-    def setUp(self):
-        self.o = mfcc()
+    members_args = 'name'
 
-    def test_default_creation(self):
-        pass
-
-    def test_delete(self):
-        del self.o
-
-    @params(*new_params)
+    @parametrize(members_args, new_params)
     def test_read_only_member(self, name):
-        o = self.o
-        with self.assertRaises((TypeError, AttributeError)):
+        o = mfcc()
+        with assert_raises((TypeError, AttributeError)):
             setattr(o, name, 0)
 
-    @params(*zip(new_params, new_deflts))
+    @parametrize('name, expected', zip(new_params, new_deflts))
     def test_default_param(self, name, expected):
         """ test mfcc.{:s} = {:d} """.format(name, expected)
-        o = self.o
-        self.assertEqual( getattr(o, name), expected)
+        o = mfcc()
+        assert getattr(o, name) == expected
 
 class aubio_mfcc_wrong_params(TestCase):
 
@@ -82,9 +74,9 @@
         #print coeffs
 
 
-class aubio_mfcc_all_parameters(TestCase):
+class Test_aubio_mfcc_all_parameters(object):
 
-    @params(
+    run_values = [
             (2048, 40, 13, 44100),
             (1024, 40, 13, 44100),
             (512, 40, 13, 44100),
@@ -100,7 +92,10 @@
             #(1024, 30, 20, 44100),
             (1024, 40, 40, 44100),
             (1024, 40, 3, 44100),
-            )
+            ]
+    run_args = ['buf_size', 'n_filters', 'n_coeffs', 'samplerate']
+
+    @parametrize(run_args, run_values)
     def test_run_with_params(self, buf_size, n_filters, n_coeffs, samplerate):
         " check mfcc can run with reasonable parameters "
         o = mfcc(buf_size, n_filters, n_coeffs, samplerate)
@@ -148,4 +143,5 @@
         assert m.get_scale() == 1
 
 if __name__ == '__main__':
-    main()
+    from _tools import run_module_suite
+    run_module_suite()
--- a/python/tests/test_midi2note.py
+++ b/python/tests/test_midi2note.py
@@ -2,8 +2,7 @@
 # -*- coding: utf-8 -*-
 
 from aubio import midi2note
-from nose2.tools import params
-import unittest
+from _tools import parametrize, assert_raises
 
 list_of_known_midis = (
         ( 0, 'C-1' ),
@@ -15,31 +14,31 @@
         ( 127, 'G9' ),
         )
 
-class midi2note_good_values(unittest.TestCase):
+class Test_midi2note_good_values(object):
 
-    @params(*list_of_known_midis)
+    @parametrize('midi, note', list_of_known_midis)
     def test_midi2note_known_values(self, midi, note):
         " known values are correctly converted "
-        self.assertEqual ( midi2note(midi), note )
+        assert midi2note(midi) == (note)
 
-class midi2note_wrong_values(unittest.TestCase):
+class Test_midi2note_wrong_values(object):
 
     def test_midi2note_negative_value(self):
         " fails when passed a negative value "
-        self.assertRaises(ValueError, midi2note, -2)
+        assert_raises(ValueError, midi2note, -2)
 
     def test_midi2note_large(self):
         " fails when passed a value greater than 127 "
-        self.assertRaises(ValueError, midi2note, 128)
+        assert_raises(ValueError, midi2note, 128)
 
     def test_midi2note_floating_value(self):
         " fails when passed a floating point "
-        self.assertRaises(TypeError, midi2note, 69.2)
+        assert_raises(TypeError, midi2note, 69.2)
 
     def test_midi2note_character_value(self):
         " fails when passed a value that can not be transformed to integer "
-        self.assertRaises(TypeError, midi2note, "a")
+        assert_raises(TypeError, midi2note, "a")
 
 if __name__ == '__main__':
-    import nose2
-    nose2.main()
+    from _tools import run_module_suite
+    run_module_suite()
--- a/python/tests/test_musicutils.py
+++ b/python/tests/test_musicutils.py
@@ -1,9 +1,8 @@
 #! /usr/bin/env python
 
-from unittest import main
 import numpy as np
 from numpy.testing import TestCase
-from numpy.testing.utils import assert_equal, assert_almost_equal
+from numpy.testing import assert_equal, assert_almost_equal
 from aubio import window, level_lin, db_spl, silence_detection, level_detection
 from aubio import fvec, float_type
 
@@ -85,4 +84,5 @@
         assert level_detection(ones(1024, dtype = float_type), -70) == 0
 
 if __name__ == '__main__':
+    from unittest import main
     main()
--- a/python/tests/test_note2midi.py
+++ b/python/tests/test_note2midi.py
@@ -4,8 +4,8 @@
 from __future__ import unicode_literals
 
 from aubio import note2midi, freq2note, note2freq, float_type
-from nose2.tools import params
-import unittest
+from numpy.testing import TestCase
+from _tools import parametrize, assert_raises, skipTest
 
 list_of_known_notes = (
         ( 'C-1', 0 ),
@@ -44,29 +44,32 @@
         ( '2' ),
         )
 
-class note2midi_good_values(unittest.TestCase):
+class Test_note2midi_good_values(object):
 
-    @params(*list_of_known_notes)
+    @parametrize('note, midi', list_of_known_notes)
     def test_note2midi_known_values(self, note, midi):
         " known values are correctly converted "
-        self.assertEqual ( note2midi(note), midi )
+        assert note2midi(note) == midi
 
-    @params(*list_of_known_notes_with_unicode_issues)
+    @parametrize('note, midi', list_of_known_notes_with_unicode_issues)
     def test_note2midi_known_values_with_unicode_issues(self, note, midi):
-        " known values are correctly converted, unless decoding is expected to fail"
+        " difficult values are correctly converted unless expected failure "
         try:
-            self.assertEqual ( note2midi(note), midi )
+            assert note2midi(note) == midi
         except UnicodeEncodeError as e:
+            # platforms with decoding failures include:
+            # - osx: python <= 2.7.10
+            # - win: python <= 2.7.12
             import sys
-            strfmt = "len(u'\\U0001D12A') != 1, excpected decoding failure | {:s} | {:s} {:s}"
-            strres = strfmt.format(e, sys.platform, sys.version)
-            # happens with: darwin 2.7.10, windows 2.7.12
+            strmsg = "len(u'\\U0001D12A') != 1, expected decoding failure"
+            strmsg += " | upgrade to Python 3 to fix"
+            strmsg += " | {:s} | {:s} {:s}"
             if len('\U0001D12A') != 1 and sys.version[0] == '2':
-                self.skipTest(strres + " | upgrade to Python 3 to fix")
+                skipTest(strmsg.format(repr(e), sys.platform, sys.version))
             else:
                 raise
 
-class note2midi_wrong_values(unittest.TestCase):
+class note2midi_wrong_values(TestCase):
 
     def test_note2midi_missing_octave(self):
         " fails when passed only one character"
@@ -104,12 +107,14 @@
         " fails when passed a note with a note name longer than expected"
         self.assertRaises(ValueError, note2midi, 'CB+-3')
 
-    @params(*list_of_unknown_notes)
+class Test_note2midi_unknown_values(object):
+
+    @parametrize('note', list_of_unknown_notes)
     def test_note2midi_unknown_values(self, note):
         " unknown values throw out an error "
-        self.assertRaises(ValueError, note2midi, note)
+        assert_raises(ValueError, note2midi, note)
 
-class freq2note_simple_test(unittest.TestCase):
+class freq2note_simple_test(TestCase):
 
     def test_freq2note_above(self):
         " make sure freq2note(441) == A4 "
@@ -119,7 +124,7 @@
         " make sure freq2note(439) == A4 "
         self.assertEqual("A4", freq2note(439))
 
-class note2freq_simple_test(unittest.TestCase):
+class note2freq_simple_test(TestCase):
 
     def test_note2freq(self):
         " make sure note2freq('A3') == 220"
@@ -133,5 +138,5 @@
             self.assertLess(abs(note2freq("A4")-440), 1.e-12)
 
 if __name__ == '__main__':
-    import nose2
-    nose2.main()
+    from _tools import run_module_suite
+    run_module_suite()
--- a/python/tests/test_notes.py
+++ b/python/tests/test_notes.py
@@ -1,9 +1,12 @@
 #! /usr/bin/env python
 
-from unittest import main
 from numpy.testing import TestCase, assert_equal, assert_almost_equal
-from aubio import notes
+from aubio import notes, source
+import numpy as np
+from utils import list_all_sounds
 
+list_of_sounds = list_all_sounds('sounds')
+
 AUBIO_DEFAULT_NOTES_SILENCE = -70.
 AUBIO_DEFAULT_NOTES_RELEASE_DROP = 10.
 AUBIO_DEFAULT_NOTES_MINIOI_MS = 30.
@@ -52,14 +55,9 @@
         with self.assertRaises(ValueError):
             self.o.set_release_drop(val)
 
-from .utils import list_all_sounds
-list_of_sounds = list_all_sounds('sounds')
-
 class aubio_notes_sinewave(TestCase):
 
     def analyze_file(self, filepath, samplerate=0):
-        from aubio import source
-        import numpy as np
         win_s = 512 # fft size
         hop_s = 256 # hop size
 
@@ -92,4 +90,5 @@
                 assert_equal (results[0][1], [69, 123, -1])
 
 if __name__ == '__main__':
+    from unittest import main
     main()
--- a/python/tests/test_onset.py
+++ b/python/tests/test_onset.py
@@ -1,6 +1,5 @@
 #! /usr/bin/env python
 
-from unittest import main
 from numpy.testing import TestCase, assert_equal, assert_almost_equal
 from aubio import onset, fvec
 
@@ -116,4 +115,5 @@
 
 
 if __name__ == '__main__':
+    from unittest import main
     main()
--- a/python/tests/test_phasevoc.py
+++ b/python/tests/test_phasevoc.py
@@ -1,9 +1,8 @@
 #! /usr/bin/env python
 
 from numpy.testing import TestCase, assert_equal, assert_array_less
+from _tools import parametrize
 from aubio import fvec, cvec, pvoc, float_type
-from nose2 import main
-from nose2.tools import params
 import numpy as np
 
 if float_type == 'float32':
@@ -18,7 +17,7 @@
 def create_noise(hop_s):
     return np.random.rand(hop_s).astype(float_type) * 2. - 1.
 
-class aubio_pvoc_test_case(TestCase):
+class Test_aubio_pvoc_test_case(object):
     """ pvoc object test case """
 
     def test_members_automatic_sizes_default(self):
@@ -65,7 +64,8 @@
             r = f.rdo(s)
             assert_equal ( t, 0.)
 
-    @params(
+    resynth_noise_args = "hop_s, ratio"
+    resynth_noise_values = [
             ( 256, 8),
             ( 256, 4),
             ( 256, 2),
@@ -87,13 +87,16 @@
             (8192, 8),
             (8192, 4),
             (8192, 2),
-            )
+            ]
+
+    @parametrize(resynth_noise_args, resynth_noise_values)
     def test_resynth_steps_noise(self, hop_s, ratio):
         """ check the resynthesis of a random signal is correct """
         sigin = create_noise(hop_s)
         self.reconstruction(sigin, hop_s, ratio)
 
-    @params(
+    resynth_sine_args = "samplerate, hop_s, ratio, freq"
+    resynth_sine_values = [
             (44100,  256, 8,   441),
             (44100,  256, 4,  1203),
             (44100,  256, 2,  3045),
@@ -108,7 +111,9 @@
             (22050,  256, 8,   445),
             (96000, 1024, 8, 47000),
             (96000, 1024, 8,    20),
-            )
+            ]
+
+    @parametrize(resynth_sine_args, resynth_sine_values)
     def test_resynth_steps_sine(self, samplerate, hop_s, ratio, freq):
         """ check the resynthesis of a sine is correct """
         sigin = create_sine(hop_s, freq, samplerate)
@@ -199,5 +204,5 @@
             self.skipTest('creating aubio.pvoc with size %d did not fail' % win_s)
 
 if __name__ == '__main__':
+    from unittest import main
     main()
-
--- a/python/tests/test_pitch.py
+++ b/python/tests/test_pitch.py
@@ -1,7 +1,6 @@
 #! /usr/bin/env python
 
-from unittest import TestCase, main
-from numpy.testing import assert_equal
+from numpy.testing import TestCase, assert_equal
 from numpy import sin, arange, mean, median, isnan, pi
 from aubio import fvec, pitch, freqtomidi, float_type
 
@@ -116,10 +115,11 @@
 
 for algo in pitch_algorithms:
     for mode in signal_modes:
-        test_method = create_test (algo, mode)
-        test_method.__name__ = 'test_pitch_%s_%d_%d_%dHz_sin_%.0f' % ( algo,
+        _test_method = create_test (algo, mode)
+        _test_method.__name__ = 'test_pitch_%s_%d_%d_%dHz_sin_%.0f' % ( algo,
                 mode[0], mode[1], mode[2], mode[3] )
-        setattr (aubio_pitch_Sinusoid, test_method.__name__, test_method)
+        setattr (aubio_pitch_Sinusoid, _test_method.__name__, _test_method)
 
 if __name__ == '__main__':
+    from unittest import main
     main()
--- a/python/tests/test_sink.py
+++ b/python/tests/test_sink.py
@@ -1,14 +1,10 @@
 #! /usr/bin/env python
 
-from nose2 import main
-from nose2.tools import params
 from numpy.testing import TestCase
 from aubio import fvec, source, sink
-from .utils import list_all_sounds, get_tmp_sink_path, del_tmp_sink_path
+from utils import list_all_sounds, get_tmp_sink_path, del_tmp_sink_path
+from _tools import parametrize, skipTest, assert_raises
 
-import warnings
-warnings.filterwarnings('ignore', category=UserWarning, append=True)
-
 list_of_sounds = list_all_sounds('sounds')
 samplerates = [0, 44100, 8000, 32000]
 hop_sizes = [512, 1024, 64]
@@ -23,30 +19,26 @@
         for samplerate in samplerates:
             all_params.append((hop_size, samplerate, soundfile))
 
-class aubio_sink_test_case(TestCase):
+class Test_aubio_sink(object):
 
-    def setUp(self):
-        if not len(list_of_sounds):
-            self.skipTest('add some sound files in \'python/tests/sounds\'')
-
     def test_wrong_filename(self):
-        with self.assertRaises(RuntimeError):
+        with assert_raises(RuntimeError):
             sink('')
 
     def test_wrong_samplerate(self):
-        with self.assertRaises(RuntimeError):
+        with assert_raises(RuntimeError):
             sink(get_tmp_sink_path(), -1)
 
     def test_wrong_samplerate_too_large(self):
-        with self.assertRaises(RuntimeError):
+        with assert_raises(RuntimeError):
             sink(get_tmp_sink_path(), 1536001, 2)
 
     def test_wrong_channels(self):
-        with self.assertRaises(RuntimeError):
+        with assert_raises(RuntimeError):
             sink(get_tmp_sink_path(), 44100, -1)
 
     def test_wrong_channels_too_large(self):
-        with self.assertRaises(RuntimeError):
+        with assert_raises(RuntimeError):
             sink(get_tmp_sink_path(), 44100, 202020)
 
     def test_many_sinks(self):
@@ -66,13 +58,13 @@
             g.close()
         shutil.rmtree(tmpdir)
 
-    @params(*all_params)
+    @parametrize('hop_size, samplerate, path', all_params)
     def test_read_and_write(self, hop_size, samplerate, path):
-
         try:
             f = source(path, samplerate, hop_size)
         except RuntimeError as e:
-            self.skipTest('failed opening with hop_s = {:d}, samplerate = {:d} ({:s})'.format(hop_size, samplerate, str(e)))
+            err_msg = '{:s} (hop_s = {:d}, samplerate = {:d})'
+            skipTest(err_msg.format(str(e), hop_size, samplerate))
         if samplerate == 0: samplerate = f.samplerate
         sink_path = get_tmp_sink_path()
         g = sink(sink_path, samplerate)
@@ -84,12 +76,13 @@
             if read < f.hop_size: break
         del_tmp_sink_path(sink_path)
 
-    @params(*all_params)
+    @parametrize('hop_size, samplerate, path', all_params)
     def test_read_and_write_multi(self, hop_size, samplerate, path):
         try:
             f = source(path, samplerate, hop_size)
         except RuntimeError as e:
-            self.skipTest('failed opening with hop_s = {:d}, samplerate = {:d} ({:s})'.format(hop_size, samplerate, str(e)))
+            err_msg = '{:s} (hop_s = {:d}, samplerate = {:d})'
+            skipTest(err_msg.format(str(e), hop_size, samplerate))
         if samplerate == 0: samplerate = f.samplerate
         sink_path = get_tmp_sink_path()
         g = sink(sink_path, samplerate, channels = f.channels)
@@ -125,4 +118,5 @@
                 g(vec, 128)
 
 if __name__ == '__main__':
-    main()
+    from _tools import run_module_suite
+    run_module_suite()
--- a/python/tests/test_slicing.py
+++ b/python/tests/test_slicing.py
@@ -1,10 +1,9 @@
 #! /usr/bin/env python
 
-from unittest import main
 from numpy.testing import TestCase, assert_equal
 from aubio import slice_source_at_stamps
-from .utils import count_files_in_directory, get_default_test_sound
-from .utils import count_samples_in_directory, count_samples_in_file
+from utils import count_files_in_directory, get_default_test_sound
+from utils import count_samples_in_directory, count_samples_in_file
 
 import tempfile
 import shutil
@@ -167,4 +166,5 @@
         shutil.rmtree(self.output_dir)
 
 if __name__ == '__main__':
+    from unittest import main
     main()
--- a/python/tests/test_source.py
+++ b/python/tests/test_source.py
@@ -1,19 +1,17 @@
 #! /usr/bin/env python
 
-from nose2 import main
-from nose2.tools import params
+
 from numpy.testing import TestCase, assert_equal
 from aubio import source
-from .utils import list_all_sounds
+from utils import list_all_sounds
+import unittest
+from _tools import parametrize, assert_raises, assert_equal, skipTest
 
-import warnings
-warnings.filterwarnings('ignore', category=UserWarning, append=True)
-
 list_of_sounds = list_all_sounds('sounds')
 samplerates = [0, 44100, 8000, 32000]
 hop_sizes = [512, 1024, 64]
 
-path = None
+default_test_sound = len(list_of_sounds) and list_of_sounds[0] or None
 
 all_params = []
 for soundfile in list_of_sounds:
@@ -21,17 +19,13 @@
         for samplerate in samplerates:
             all_params.append((hop_size, samplerate, soundfile))
 
+no_sounds_msg = "no test sounds, add some in 'python/tests/sounds/'!"
 
-class aubio_source_test_case_base(TestCase):
+_debug = False
 
-    def setUp(self):
-        if not len(list_of_sounds):
-            self.skipTest('add some sound files in \'python/tests/sounds\'')
-        self.default_test_sound = list_of_sounds[0]
+class Test_aubio_source_test_case(object):
 
-class aubio_source_test_case(aubio_source_test_case_base):
-
-    @params(*list_of_sounds)
+    @parametrize('filename', list_of_sounds)
     def test_close_file(self, filename):
         samplerate = 0 # use native samplerate
         hop_size = 256
@@ -38,7 +32,7 @@
         f = source(filename, samplerate, hop_size)
         f.close()
 
-    @params(*list_of_sounds)
+    @parametrize('filename', list_of_sounds)
     def test_close_file_twice(self, filename):
         samplerate = 0 # use native samplerate
         hop_size = 256
@@ -46,7 +40,7 @@
         f.close()
         f.close()
 
-class aubio_source_read_test_case(aubio_source_test_case_base):
+class Test_aubio_source_read(object):
 
     def read_from_source(self, f):
         total_frames = 0
@@ -56,40 +50,44 @@
             if read < f.hop_size:
                 assert_equal(samples[read:], 0)
                 break
-        #result_str = "read {:.2f}s ({:d} frames in {:d} blocks at {:d}Hz) from {:s}"
-        #result_params = total_frames / float(f.samplerate), total_frames, total_frames//f.hop_size, f.samplerate, f.uri
-        #print (result_str.format(*result_params))
+        if _debug:
+            result_str = "read {:.2f}s ({:d} frames"
+            result_str += " in {:d} blocks at {:d}Hz) from {:s}"
+            result_params = total_frames / float(f.samplerate), total_frames, \
+                    total_frames//f.hop_size, f.samplerate, f.uri
+            print (result_str.format(*result_params))
         return total_frames
 
-    @params(*all_params)
+    @parametrize('hop_size, samplerate, soundfile', all_params)
     def test_samplerate_hopsize(self, hop_size, samplerate, soundfile):
         try:
             f = source(soundfile, samplerate, hop_size)
         except RuntimeError as e:
-            self.skipTest('failed opening with hop_s = {:d}, samplerate = {:d} ({:s})'.format(hop_size, samplerate, str(e)))
+            err_msg = 'failed opening with hop_s={:d}, samplerate={:d} ({:s})'
+            skipTest(err_msg.format(hop_size, samplerate, str(e)))
         assert f.samplerate != 0
         read_frames = self.read_from_source(f)
         if 'f_' in soundfile and samplerate == 0:
             import re
-            f = re.compile('.*_\([0:9]*f\)_.*')
+            f = re.compile(r'.*_\([0:9]*f\)_.*')
             match_f = re.findall('([0-9]*)f_', soundfile)
             if len(match_f) == 1:
                 expected_frames = int(match_f[0])
-                self.assertEqual(expected_frames, read_frames)
+                assert_equal(expected_frames, read_frames)
 
-    @params(*list_of_sounds)
+    @parametrize('p', list_of_sounds)
     def test_samplerate_none(self, p):
         f = source(p)
         assert f.samplerate != 0
         self.read_from_source(f)
 
-    @params(*list_of_sounds)
+    @parametrize('p', list_of_sounds)
     def test_samplerate_0(self, p):
         f = source(p, 0)
         assert f.samplerate != 0
         self.read_from_source(f)
 
-    @params(*list_of_sounds)
+    @parametrize('p', list_of_sounds)
     def test_zero_hop_size(self, p):
         f = source(p, 0, 0)
         assert f.samplerate != 0
@@ -96,7 +94,7 @@
         assert f.hop_size != 0
         self.read_from_source(f)
 
-    @params(*list_of_sounds)
+    @parametrize('p', list_of_sounds)
     def test_seek_to_half(self, p):
         from random import randint
         f = source(p, 0, 0)
@@ -108,7 +106,7 @@
         b = self.read_from_source(f)
         assert a == b + c
 
-    @params(*list_of_sounds)
+    @parametrize('p', list_of_sounds)
     def test_duration(self, p):
         total_frames = 0
         f = source(p)
@@ -117,43 +115,44 @@
             _, read = f()
             total_frames += read
             if read < f.hop_size: break
-        self.assertEqual(duration, total_frames)
+        assert_equal (duration, total_frames)
 
 
-class aubio_source_test_wrong_params(TestCase):
+class Test_aubio_source_wrong_params(object):
 
     def test_wrong_file(self):
-        with self.assertRaises(RuntimeError):
+        with assert_raises(RuntimeError):
             source('path_to/unexisting file.mp3')
 
-class aubio_source_test_wrong_params_with_file(aubio_source_test_case_base):
+@unittest.skipIf(default_test_sound is None, no_sounds_msg)
+class Test_aubio_source_wrong_params_with_file(TestCase):
 
     def test_wrong_samplerate(self):
-        with self.assertRaises(ValueError):
-            source(self.default_test_sound, -1)
+        with assert_raises(ValueError):
+            source(default_test_sound, -1)
 
     def test_wrong_hop_size(self):
-        with self.assertRaises(ValueError):
-            source(self.default_test_sound, 0, -1)
+        with assert_raises(ValueError):
+            source(default_test_sound, 0, -1)
 
     def test_wrong_channels(self):
-        with self.assertRaises(ValueError):
-            source(self.default_test_sound, 0, 0, -1)
+        with assert_raises(ValueError):
+            source(default_test_sound, 0, 0, -1)
 
     def test_wrong_seek(self):
-        f = source(self.default_test_sound)
-        with self.assertRaises(ValueError):
+        f = source(default_test_sound)
+        with assert_raises(ValueError):
             f.seek(-1)
 
     def test_wrong_seek_too_large(self):
-        f = source(self.default_test_sound)
+        f = source(default_test_sound)
         try:
-            with self.assertRaises(ValueError):
+            with assert_raises(ValueError):
                 f.seek(f.duration + f.samplerate * 10)
-        except AssertionError:
-            self.skipTest('seeking after end of stream failed raising ValueError')
+        except:
+            skipTest('seeking after end of stream failed raising ValueError')
 
-class aubio_source_readmulti_test_case(aubio_source_read_test_case):
+class Test_aubio_source_readmulti(Test_aubio_source_read):
 
     def read_from_source(self, f):
         total_frames = 0
@@ -163,15 +162,18 @@
             if read < f.hop_size:
                 assert_equal(samples[:,read:], 0)
                 break
-        #result_str = "read {:.2f}s ({:d} frames in {:d} channels and {:d} blocks at {:d}Hz) from {:s}"
-        #result_params = total_frames / float(f.samplerate), total_frames, f.channels, int(total_frames/f.hop_size), f.samplerate, f.uri
-        #print (result_str.format(*result_params))
+        if _debug:
+            result_str = "read {:.2f}s ({:d} frames in {:d} channels"
+            result_str += " and {:d} blocks at {:d}Hz) from {:s}"
+            result_params = total_frames / float(f.samplerate), total_frames, \
+                    f.channels, int(total_frames/f.hop_size), \
+                    f.samplerate, f.uri
+            print (result_str.format(*result_params))
         return total_frames
 
-class aubio_source_with(aubio_source_test_case_base):
+class Test_aubio_source_with(object):
 
-    #@params(*list_of_sounds)
-    @params(*list_of_sounds)
+    @parametrize('filename', list_of_sounds)
     def test_read_from_mono(self, filename):
         total_frames = 0
         hop_size = 2048
@@ -185,4 +187,5 @@
             assert_equal(total_frames, input_source.duration)
 
 if __name__ == '__main__':
-    main()
+    from _tools import run_module_suite
+    run_module_suite()
--- a/python/tests/test_source_channels.py
+++ b/python/tests/test_source_channels.py
@@ -8,7 +8,7 @@
 import aubio
 import numpy as np
 from numpy.testing import assert_equal
-from .utils import get_tmp_sink_path
+from utils import get_tmp_sink_path
 
 class aubio_source_test_case(unittest.TestCase):
 
--- a/python/tests/test_specdesc.py
+++ b/python/tests/test_specdesc.py
@@ -1,6 +1,5 @@
 #! /usr/bin/env python
 
-from unittest import main
 from numpy.testing import TestCase, assert_equal, assert_almost_equal
 from numpy import random, arange, log, zeros
 from aubio import specdesc, cvec, float_type
@@ -229,4 +228,5 @@
             specdesc("unknown", 512)
 
 if __name__ == '__main__':
+    from unittest import main
     main()
--- a/python/tests/test_zero_crossing_rate.py
+++ b/python/tests/test_zero_crossing_rate.py
@@ -1,6 +1,5 @@
 #! /usr/bin/env python
 
-from unittest import main
 from numpy.testing import TestCase
 from aubio import fvec, zero_crossing_rate
 
@@ -43,4 +42,5 @@
         self.assertEqual(2./buf_size, zero_crossing_rate(self.vector))
 
 if __name__ == '__main__':
+    from unittest import main
     main()
--- a/python/tests/utils.py
+++ b/python/tests/utils.py
@@ -8,11 +8,8 @@
 DEFAULT_SOUND = '22050Hz_5s_brownnoise.wav'
 
 def array_from_text_file(filename, dtype = 'float'):
-    filename = os.path.join(os.path.dirname(__file__), filename)
-    with open(filename) as f:
-        lines = f.readlines()
-    return np.array([line.split() for line in lines],
-            dtype = dtype)
+    realpathname = os.path.join(os.path.dirname(__file__), filename)
+    return np.loadtxt(realpathname, dtype = dtype)
 
 def list_all_sounds(rel_dir):
     datadir = os.path.join(os.path.dirname(__file__), rel_dir)
@@ -38,13 +35,10 @@
     try:
         os.unlink(path)
     except WindowsError as e:
-        print("deleting {:s} failed ({:s}), reopening".format(path, repr(e)))
-        with open(path, 'wb') as f:
-            f.close()
-        try:
-            os.unlink(path)
-        except WindowsError as f:
-            print("deleting {:s} failed ({:s}), aborting".format(path, repr(e)))
+        # removing the temporary directory sometimes fails on windows
+        import warnings
+        errmsg = "failed deleting temporary file {:s} ({:s})"
+        warnings.warn(UserWarning(errmsg.format(path, repr(e))))
 
 def array_from_yaml_file(filename):
     import yaml
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,2 @@
 numpy
-nose2
+pytest
--- a/setup.py
+++ b/setup.py
@@ -95,8 +95,4 @@
             'aubiocut = aubio.cut:main',
         ],
     },
-    test_suite = 'nose2.collector.collector',
-    extras_require = {
-        'tests': ['numpy'],
-        },
     )