From ac85c4bb09eebdbf7914d062b7efbb77673e614c Mon Sep 17 00:00:00 2001 From: Michael Clerx Date: Thu, 3 Jul 2025 17:27:11 +0100 Subject: [PATCH 1/3] Added tests to check that input arguments stay unchanged. --- datkit/tests/__init__.py | 47 ++++++++++++++++++++- datkit/tests/test_check_times.py | 18 ++++++-- datkit/tests/test_points.py | 72 ++++++++++++++++++++++++++++++-- datkit/tests/test_smoothing.py | 5 +-- datkit/tests/test_spectral.py | 14 ++++++- datkit/tests/test_tests.py | 38 +++++++++++++++++ 6 files changed, 181 insertions(+), 13 deletions(-) create mode 100755 datkit/tests/test_tests.py diff --git a/datkit/tests/__init__.py b/datkit/tests/__init__.py index 3a04bc0..addc8ed 100644 --- a/datkit/tests/__init__.py +++ b/datkit/tests/__init__.py @@ -6,8 +6,53 @@ # For copyright, sharing, and licensing, see https://github.com/myokit/datkit/ # import os - +import unittest # The test directory DIR_TEST = os.path.abspath(os.path.dirname(__file__)) + +class TestCase(unittest.TestCase): + """ + Adds methods to ``unittest.TestCase``. + """ + + def array_args_unchanged(self, func, *args): + """ + Test if sequence-type arguments are unchanged by the given function. + """ + import numpy as np + + new_args = [] + seq_args = {} + for k, arg in enumerate(args): + new_args.append(arg) + if not isinstance(arg, (str, bytes, dict)): + try: + len(arg) + except TypeError: + pass + else: + new_args[k] = np.array(arg) + seq_args[k] = np.array(arg, copy=True) + if len(seq_args) == 0: + return True + + # Call, check if unchanged + func(*new_args) + for k, b in seq_args.items(): + a = new_args[k] + if a.shape != b.shape: + return False + if np.any(a != b): + return False + return True + + def assertUnchanged(self, func, *args): + """ + Fail if the given function changes the sequence-type arguments. + """ + self.assertTrue(self.array_args_unchanged(func, *args)) + + +del os, unittest diff --git a/datkit/tests/test_check_times.py b/datkit/tests/test_check_times.py index c9a8ff0..4a1f1c7 100755 --- a/datkit/tests/test_check_times.py +++ b/datkit/tests/test_check_times.py @@ -5,14 +5,13 @@ # This file is part of Datkit. # For copyright, sharing, and licensing, see https://github.com/myokit/datkit/ # -import unittest - import numpy as np import datkit as d +import datkit.tests -class CheckTimesTest(unittest.TestCase): +class CheckTimesTest(datkit.tests.TestCase): """ Tests methods from the hidden _check_times module. """ def test_is_increasing(self): @@ -42,6 +41,10 @@ def test_is_increasing(self): x = [(0, 1), [1, 2]] self.assertRaisesRegex(ValueError, '1-d', d.is_increasing, x) + # Test if input is unchanged + self.assertUnchanged(d.is_increasing, np.linspace(0, 1, 101)) + self.assertUnchanged(d.is_increasing, (5, 4, 3, 1)) + def test_is_regularly_increasing(self): x = np.linspace(0, 1, 101) @@ -62,6 +65,10 @@ def test_is_regularly_increasing(self): self.assertTrue(d.is_regularly_increasing(-13 + np.arange(10) / 100)) + # Test if input is unchanged + self.assertUnchanged(d.is_regularly_increasing, np.linspace(0, 1, 101)) + self.assertUnchanged(d.is_regularly_increasing, np.geomspace(1, 2, 10)) + def test_sampling_interval(self): self.assertEqual(d.sampling_interval(np.arange(10)), 1) @@ -72,7 +79,6 @@ def test_sampling_interval(self): self.assertAlmostEqual( d.sampling_interval(-13 + np.arange(1000) / 100), 0.01) x = -13 + np.arange(1000) / 100 - print(len(x)) self.assertRaisesRegex(ValueError, 'two', d.sampling_interval, []) self.assertRaisesRegex(ValueError, 'two', d.sampling_interval, [0]) @@ -82,6 +88,10 @@ def test_sampling_interval(self): x = [(0, 1), [1, 2]] self.assertRaisesRegex(ValueError, '1-d', d.sampling_interval, x) + # Test if input is unchanged + self.assertUnchanged(d.sampling_interval, np.linspace(0, 1, 10)) + if __name__ == '__main__': + import unittest unittest.main() diff --git a/datkit/tests/test_points.py b/datkit/tests/test_points.py index c7b9d5c..8e433c5 100755 --- a/datkit/tests/test_points.py +++ b/datkit/tests/test_points.py @@ -5,14 +5,13 @@ # This file is part of Datkit. # For copyright, sharing, and licensing, see https://github.com/myokit/datkit/ # -import unittest - import numpy as np import datkit as d +import datkit.tests -class PointsTest(unittest.TestCase): +class PointsTest(datkit.tests.TestCase): """ Tests methods from the hidden _points module. """ def test_abs_max_on(self): @@ -25,6 +24,10 @@ def test_abs_max_on(self): self.assertEqual(d.abs_max_on(t, v, 1.5, 2), (t[99], v[99])) self.assertEqual(d.abs_max_on(t, v, 1.5, 2, False, True), (2, 1)) + t = np.linspace(0, 2, 101) + v = np.cos(t * np.pi) + self.assertUnchanged(d.abs_max_on, t, v) + def test_data_on(self): t = [0, 1, 2, 3, 4, 5, 6, 7] v = [10, 11, 12, 13, 14, 15, 16, 17] @@ -34,6 +37,10 @@ def test_data_on(self): self.assertEqual(d.data_on(t, v, t1=2, include_right=True), ([0, 1, 2], [10, 11, 12])) + t = [0, 1, 2, 3, 4, 5, 6, 7] + v = [10, 11, 12, 13, 14, 15, 16, 17] + self.assertUnchanged(d.data_on, t, v) + def test_iabs_max_on(self): t = np.linspace(0, 2, 101) v = np.cos(t * np.pi) @@ -44,6 +51,10 @@ def test_iabs_max_on(self): self.assertEqual(d.iabs_max_on(t, v, 1.5, 2), 99) self.assertEqual(d.iabs_max_on(t, v, 1.5, 2, False, True), 100) + t = np.linspace(0, 2, 30) + v = np.sin(t * np.pi) + self.assertUnchanged(d.iabs_max_on, t, v) + def test_imax_on(self): t = np.linspace(0, 2, 101) v = np.cos(t * np.pi) @@ -53,6 +64,10 @@ def test_imax_on(self): self.assertEqual(d.imax_on(t, v, 1.5, 2), 99) self.assertEqual(d.imax_on(t, v, 1.5, 2, False, True), 100) + t = np.linspace(0, 2, 31) + v = np.cos(t * np.pi + 3) + self.assertUnchanged(d.imax_on, t, v) + def test_imin_on(self): t = np.linspace(0, 2, 101) v = np.cos(t * np.pi) @@ -62,6 +77,10 @@ def test_imin_on(self): self.assertEqual(d.imin_on(t, v, 1.5, 2), 75) self.assertEqual(d.imin_on(t, v, 1.5, 2, False), 76) + t = np.linspace(0, 3, 13) + v = np.cos(t * 2 * np.pi) + self.assertUnchanged(d.imin_on, t, v) + def test_index(self): # Simple tests @@ -127,6 +146,10 @@ def test_index(self): # Any sequence is accepted self.assertEqual(d.index(tuple(times), 7.3), 49) + # Input is unchanged + self.assertUnchanged(d.index, np.arange(0, 10), 3) + self.assertUnchanged(d.index, np.arange(0, 10), 0) + def test_index_crossing(self): # Simple test @@ -165,6 +188,10 @@ def test_index_crossing(self): values = [9, 9, 8, 7, 6, 5, 5, 5, 5, 4, 3, 2, 2] self.assertEqual(d.index_crossing(values, 5), (4, 9)) + # Input is unchanged + values = [4, 5, 6, 7, 8, 6, 7, 8, 9] + self.assertUnchanged(d.index_crossing, values, 6.5) + def test_index_near(self): # Exact matches @@ -192,6 +219,10 @@ def test_index_near(self): self.assertEqual(d.index_near(tuple(times), 9.6), 19) self.assertEqual(d.index_near(list(times), 9.6), 19) + # Input should remain unchanged + self.assertUnchanged(d.index_near, np.arange(0, 10), 4) + self.assertUnchanged(d.index_near, np.arange(0, 10), 9) + def test_index_on(self): t = np.arange(0, 10) self.assertEqual(d.index_on(t, 2, 4), (2, 4)) @@ -258,6 +289,11 @@ def test_index_on(self): self.assertEqual(d.index_on(tuple(t), 3), (2, 10)) self.assertEqual(d.index_on(list(t), 3), (2, 10)) + # Input should stay unchanged + self.assertUnchanged(d.index_on, np.arange(0, 10), 2, 4) + self.assertUnchanged(d.index_on, np.arange(0, 10), -5, 4) + self.assertUnchanged(d.index_on, np.arange(0, 10), 12, 14) + def test_max_on(self): t = np.linspace(0, 2, 101) v = np.cos(t * np.pi) @@ -267,6 +303,10 @@ def test_max_on(self): self.assertEqual(d.max_on(t, v, 1.5, 2), (t[99], v[99])) self.assertEqual(d.max_on(t, v, 1.5, 2, False, True), (t[100], v[100])) + t = np.linspace(0, 1, 11) + v = np.sin(3 * t * np.pi) + self.assertUnchanged(d.max_on, t, v) + def test_mean_on(self): t = np.arange(1, 11) self.assertEqual(d.mean_on(t, t, 1, 11), 5.5) @@ -279,6 +319,10 @@ def test_mean_on(self): self.assertEqual(d.mean_on(t, v, 4, 8, False), 37) self.assertEqual(d.mean_on(t, v, 4, 8, True, True), 37) + t = np.arange(1, 11) + v = -3 + 8 * t[::-1] + self.assertUnchanged(d.mean_on, t, v, 2, 7) + def test_min_on(self): t = np.linspace(0, 2, 101) v = np.cos(t * np.pi) @@ -288,6 +332,10 @@ def test_min_on(self): self.assertEqual(d.min_on(t, v, 1.5, 2), (t[75], v[75])) self.assertEqual(d.min_on(t, v, 1.5, 2, False), (t[76], v[76])) + t = np.linspace(0, 2, 101) + v = np.cos(t * np.pi) + self.assertUnchanged(d.min_on, t, v, 1.5, 2, False) + def test_time_crossing(self): t = np.linspace(1, 5, 100) v = np.sin(t) + 1 @@ -302,6 +350,10 @@ def test_time_crossing(self): self.assertEqual(d.time_crossing(t, v, 25), 6.5) self.assertEqual(d.time_crossing(t, v, 31), 5.9) + t = np.linspace(1, 5, 100) + v = np.sin(t) + 1 + self.assertUnchanged(d.time_crossing, t, v, 0.1) + def test_value_at(self): t = np.arange(0, 10) self.assertEqual(d.value_at(t, t, 0), 0) @@ -311,6 +363,10 @@ def test_value_at(self): self.assertEqual(d.value_at(t, v, 0), 20) self.assertEqual(d.value_at(t, v, 5), 30) + t = np.arange(0, 10) + v = 10 + t + self.assertUnchanged(d.value_at, t, v, 3) + def test_value_interpolated(self): t, v = [2, 3, 4, 5, 6, 7], [5, 0, 3, -1, 4, 8] self.assertEqual(d.value_interpolated(t, v, 2), 5) @@ -329,6 +385,9 @@ def test_value_interpolated(self): self.assertEqual(d.value_interpolated(t, v, 1), 6) self.assertEqual(d.value_interpolated(t, v, 2), 6) + t, v = [2, 3, 4, 5, 6, 7], [5, 0, 3, -1, 4, 8] + self.assertUnchanged(d.value_interpolated, t, v, 3) + def test_value_near(self): t = np.arange(0, 10) self.assertEqual(d.value_near(t, t, 0), 0) @@ -341,6 +400,13 @@ def test_value_near(self): self.assertEqual(d.value_at(t, v, 0), 20) self.assertEqual(d.value_at(t, v, 5), 30) + t = np.arange(0, 10) + v = 30 + 2 * t + self.assertUnchanged(d.value_at, t, v, 0) + self.assertUnchanged(d.value_at, t, v, 5) + if __name__ == '__main__': + import unittest unittest.main() + diff --git a/datkit/tests/test_smoothing.py b/datkit/tests/test_smoothing.py index 9aa4458..01c0af7 100755 --- a/datkit/tests/test_smoothing.py +++ b/datkit/tests/test_smoothing.py @@ -5,11 +5,10 @@ # This file is part of Datkit. # For copyright, sharing, and licensing, see https://github.com/myokit/datkit/ # -import unittest - import numpy as np import datkit as d +import datkit.tests # Extra output @@ -258,7 +257,7 @@ def test_window_size(self): if __name__ == '__main__': print('Add -v for more debug output') - import sys + import sys, unittest if '-v' in sys.argv: debug = True unittest.main() diff --git a/datkit/tests/test_spectral.py b/datkit/tests/test_spectral.py index 4a80995..cfbae95 100755 --- a/datkit/tests/test_spectral.py +++ b/datkit/tests/test_spectral.py @@ -5,11 +5,10 @@ # This file is part of Datkit. # For copyright, sharing, and licensing, see https://github.com/myokit/datkit/ # -import unittest - import numpy as np import datkit as d +import datkit.tests class SpectralTest(unittest.TestCase): @@ -41,6 +40,11 @@ def test_amplitude_spectrum(self): self.assertAlmostEqual(a[50], 4, 0) self.assertLess(a[51], 0.2) + # Test if input is unchanged + t = np.linspace(0, 10, 123) + v = 6 * np.sin(t * (2 * np.pi * 2)) + self.assertTrue(au(d.amplitude_spectrum, t, v)) + def test_power_spectral_density(self): t = np.linspace(0, 10, 1000) @@ -56,6 +60,12 @@ def test_power_spectral_density(self): self.assertAlmostEqual(psd[30], 245, 0) self.assertLess(psd[31], 0.3) + # Test if input is unchanged + t = np.linspace(0, 10, 123) + v = 6 * np.sin(t * (2 * np.pi * 2)) + self.assertTrue(au(d.power_spectral_density, t, v)) + if __name__ == '__main__': + import unittest unittest.main() diff --git a/datkit/tests/test_tests.py b/datkit/tests/test_tests.py new file mode 100755 index 0000000..dcc06ff --- /dev/null +++ b/datkit/tests/test_tests.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# +# Tests testing methods. +# +# This file is part of Datkit. +# For copyright, sharing, and licensing, see https://github.com/myokit/datkit/ +# +import numpy as np + +import datkit as d +import datkit.tests + + +def mixed(a, b, c, d, e): + return True + +def change3(a, b, c, d, e): + c[3] += 1 + + +class ArgsUnchangedTest(datkit.tests.TestCase): + """ Tests methods from ``datkit.tests``. """ + + def test_args_unchanged(self): + self.assertTrue(self.array_args_unchanged( + mixed, 1, 2, 3, 4, np.array([1, 2, 3]))) + self.assertUnchanged(mixed, 1, 4, np.array([1, 2, 3]), 2, 'hi') + self.assertFalse(self.array_args_unchanged( + change3, 1, 2, np.array([1, 2, 3, 4]), 4, 5)) + self.assertFalse(self.array_args_unchanged( + change3, 1, 2, [1, 2, 3, 4], 4, 5)) + self.assertFalse(self.array_args_unchanged( + change3, 'hello', 2, (1, 2, 3, 4), 4, 5)) + + +if __name__ == '__main__': + import unittest + unittest.main() From c2ca882cac4ca1465f8282d0a545ca6516a3fbe8 Mon Sep 17 00:00:00 2001 From: Michael Clerx Date: Thu, 3 Jul 2025 18:09:25 +0100 Subject: [PATCH 2/3] Added tests to check that input arguments stay unchanged. --- datkit/tests/test_smoothing.py | 15 ++++++++++++++- datkit/tests/test_spectral.py | 6 +++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/datkit/tests/test_smoothing.py b/datkit/tests/test_smoothing.py index 01c0af7..79d3d9c 100755 --- a/datkit/tests/test_smoothing.py +++ b/datkit/tests/test_smoothing.py @@ -15,7 +15,7 @@ debug = False -class SmoothingTest(unittest.TestCase): +class SmoothingTest(datkit.tests.TestCase): """ Tests methods from the hidden _smoothing module. """ def test_gaussian_smoothing(self): @@ -92,6 +92,10 @@ def test_gaussian_smoothing(self): self.assertRaisesRegex( ValueError, 'same size', d.gaussian_smoothing, t, t[:-1], 3) + # Input is unchanged + self.assertUnchanged( + d.gaussian_smoothing, np.arange(11), np.ones(11), 3) + def test_haar_downsample(self): # Numerical tests with ones: should return all ones @@ -154,6 +158,9 @@ def test_haar_downsample(self): self.assertRaisesRegex( ValueError, 'same size', d.haar_downsample, t, t[:-1], 3) + # Input is unchanged + self.assertUnchanged(d.haar_downsample, np.arange(10), np.ones(10)) + def test_moving_average(self): # Numerical tests with ones: should return all ones @@ -214,6 +221,9 @@ def test_moving_average(self): self.assertRaisesRegex( ValueError, 'same size', d.moving_average, t, v[:-1], 3) + # Input is unchanged + self.assertUnchanged(d.moving_average, np.arange(9), np.ones(9), 3) + def test_window_size(self): # Window size checking @@ -254,6 +264,9 @@ def test_window_size(self): self.assertRaisesRegex( ValueError, 'Two window sizes', d.window_size, t, 3, 0.3) + # Input is unchanged + self.assertUnchanged(d.window_size, np.linspace(0, 5, 51), 3) + if __name__ == '__main__': print('Add -v for more debug output') diff --git a/datkit/tests/test_spectral.py b/datkit/tests/test_spectral.py index cfbae95..38bd7a5 100755 --- a/datkit/tests/test_spectral.py +++ b/datkit/tests/test_spectral.py @@ -11,7 +11,7 @@ import datkit.tests -class SpectralTest(unittest.TestCase): +class SpectralTest(datkit.tests.TestCase): """ Tests methods from the hidden _spectral module. """ def test_amplitude_spectrum(self): @@ -43,7 +43,7 @@ def test_amplitude_spectrum(self): # Test if input is unchanged t = np.linspace(0, 10, 123) v = 6 * np.sin(t * (2 * np.pi * 2)) - self.assertTrue(au(d.amplitude_spectrum, t, v)) + self.assertUnchanged(d.amplitude_spectrum, t, v) def test_power_spectral_density(self): @@ -63,7 +63,7 @@ def test_power_spectral_density(self): # Test if input is unchanged t = np.linspace(0, 10, 123) v = 6 * np.sin(t * (2 * np.pi * 2)) - self.assertTrue(au(d.power_spectral_density, t, v)) + self.assertUnchanged(d.power_spectral_density, t, v) if __name__ == '__main__': From 3c343ee2e2897ed06523e350ab0356926799713d Mon Sep 17 00:00:00 2001 From: Michael Clerx Date: Thu, 3 Jul 2025 18:10:50 +0100 Subject: [PATCH 3/3] Style fixes --- datkit/tests/test_smoothing.py | 3 ++- datkit/tests/test_tests.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/datkit/tests/test_smoothing.py b/datkit/tests/test_smoothing.py index 79d3d9c..d4c2b5a 100755 --- a/datkit/tests/test_smoothing.py +++ b/datkit/tests/test_smoothing.py @@ -270,7 +270,8 @@ def test_window_size(self): if __name__ == '__main__': print('Add -v for more debug output') - import sys, unittest + import sys + import unittest if '-v' in sys.argv: debug = True unittest.main() diff --git a/datkit/tests/test_tests.py b/datkit/tests/test_tests.py index dcc06ff..aa357a4 100755 --- a/datkit/tests/test_tests.py +++ b/datkit/tests/test_tests.py @@ -7,13 +7,13 @@ # import numpy as np -import datkit as d import datkit.tests def mixed(a, b, c, d, e): return True + def change3(a, b, c, d, e): c[3] += 1