diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0c12a21cc6e40f8f77cfe4986e55a1da6ba8f767..15b69a405625d367bee895ae1ff039fc2dd585ce 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,13 +1,19 @@ image: python:3 +before_script: + - pip install doxec numpy + - python3 setup.py install + stages: - test +unittest: + stage: test + script: + - python3 setup.py test README: stage: test script: - - pip install doxec numpy - - python3 setup.py install - doxec README.md diff --git a/setup.py b/setup.py index c9e35cb987db74638dcc5e873612976b0b26c1dc..0a9ca87b2dc206922100d7c636c5015d2c5c2e08 100644 --- a/setup.py +++ b/setup.py @@ -11,5 +11,6 @@ module1 = Extension('sortednp', language='c++', setup (name = 'sortednp', version = '0.0.0', install_requires=['numpy'], + test_suite='tests', description = 'Merge and intersect sorted numpy arrays.', ext_modules = [module1]) diff --git a/sortednpmodule.c b/sortednpmodule.c index 13401a490668063d5c07159bf96b152976ed9eec..7dac6f1b8e03f6850e33402b5dab952d5fd16f3f 100644 --- a/sortednpmodule.c +++ b/sortednpmodule.c @@ -5,8 +5,9 @@ static PyObject * sortednp_intersect(PyObject *self, PyObject *args) { PyObject *a, *b; - if (!PyArg_ParseTuple(args, "OO", &a, &b)) - return NULL; + + if (!PyArg_ParseTuple(args, "O!O!", &PyArray_Type, &a, &PyArray_Type, &b)) + return NULL; a = PyArray_FROM_OF(a, NPY_ARRAY_CARRAY_RO); b = PyArray_FROM_OF(b, NPY_ARRAY_CARRAY_RO); @@ -19,6 +20,7 @@ static PyObject * sortednp_intersect(PyObject *self, PyObject *args) { int nd_b = PyArray_NDIM(b); if (PyArray_NDIM(a) != 1 || PyArray_NDIM(b) != 1) { + PyErr_SetString(PyExc_ValueError, "Arguments can not be multi-dimensional."); return NULL; } @@ -70,8 +72,8 @@ static PyObject * sortednp_intersect(PyObject *self, PyObject *args) { static PyObject * sortednp_merge(PyObject *self, PyObject *args) { PyObject *a, *b; - if (!PyArg_ParseTuple(args, "OO", &a, &b)) - return NULL; + if (!PyArg_ParseTuple(args, "O!O!", &PyArray_Type, &a, &PyArray_Type, &b)) + return NULL; a = PyArray_FROM_OF(a, NPY_ARRAY_CARRAY_RO); b = PyArray_FROM_OF(b, NPY_ARRAY_CARRAY_RO); @@ -84,6 +86,7 @@ static PyObject * sortednp_merge(PyObject *self, PyObject *args) { int nd_b = PyArray_NDIM(b); if (PyArray_NDIM(a) != 1 || PyArray_NDIM(b) != 1) { + PyErr_SetString(PyExc_ValueError, "Arguments can not be multi-dimensional."); return NULL; } diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/test_intersect.py b/tests/test_intersect.py new file mode 100644 index 0000000000000000000000000000000000000000..ae704899d1b6fb3b5f5623c30d1d01812ac83013 --- /dev/null +++ b/tests/test_intersect.py @@ -0,0 +1,237 @@ + +from abc import ABCMeta, abstractmethod +import unittest +import numpy as np + +import sortednp as snp + +class IntersectBase(metaclass=ABCMeta): + """ + Define general test cases for the intersect method. Sub-classes need to + implement have to overwrite the dtype method. + """ + + @abstractmethod + def get_dtype(self): + """ + Returns the numpy data type, which should be used for all tests. + """ + pass + + def test_simple_middle(self): + """ + Check that intersect returns only element, which are present in both + non-empty input arrays. The common elements are neither at the + beginning nor at the end of the arrays. + """ + a = np.array([2, 3, 6, 7, 9], dtype=self.get_dtype()) + b = np.array([1, 3, 7, 8, 10], dtype=self.get_dtype()) + + i = snp.intersect(a, b) + + self.assertEqual(list(i), [3, 7]) + self.assertEqual(i.dtype, self.get_dtype()) + + def test_simple_end_single(self): + """ + Check that intersect returns only element, which are present in both + non-empty input arrays. One common element is at the end of one array. + """ + a = np.array([2, 3, 6, 7, 9], dtype=self.get_dtype()) + b = np.array([1, 3, 7, 9, 10], dtype=self.get_dtype()) + + i = snp.intersect(a, b) + + self.assertEqual(list(i), [3, 7, 9]) + self.assertEqual(i.dtype, self.get_dtype()) + + + def test_simple_end_both(self): + """ + Check that intersect returns only element, which are present in both + non-empty input arrays. One common element is at the end of both + arrays. + """ + a = np.array([2, 3, 6, 7, 9], dtype=self.get_dtype()) + b = np.array([1, 3, 7, 9], dtype=self.get_dtype()) + + i = snp.intersect(a, b) + + self.assertEqual(list(i), [3, 7, 9]) + self.assertEqual(i.dtype, self.get_dtype()) + + def test_simple_begining_single(self): + """ + Check that intersect returns only element, which are present in both + non-empty input arrays. One common element is at the begining of one array. + """ + a = np.array([2, 3, 6, 7, 8], dtype=self.get_dtype()) + b = np.array([1, 2, 3, 7, 9, 10], dtype=self.get_dtype()) + + i = snp.intersect(a, b) + + self.assertEqual(list(i), [2, 3, 7]) + self.assertEqual(i.dtype, self.get_dtype()) + + def test_simple_begining_both(self): + """ + Check that intersect returns only element, which are present in both + non-empty input arrays. One common element is at the end of both + arrays. + """ + a = np.array([2, 3, 6, 7, 8], dtype=self.get_dtype()) + b = np.array([2, 3, 7, 9, 10], dtype=self.get_dtype()) + + i = snp.intersect(a, b) + + self.assertEqual(list(i), [2, 3, 7]) + self.assertEqual(i.dtype, self.get_dtype()) + + def test_empty_intersect(self): + """ + Check that intersect returns an empty array, if the non-empty inputs + do not have any elements in common. + """ + a = np.array([1, 3, 5, 10], dtype=self.get_dtype()) + b = np.array([2, 4, 7, 8, 20], dtype=self.get_dtype()) + + i = snp.intersect(a, b) + + self.assertEqual(list(i), []) + self.assertEqual(len(i), 0) + self.assertEqual(i.dtype, self.get_dtype()) + + def test_empty_input_single(self): + """ + Check that intersect returns an empty array, if one of the input arrays + is empty. + """ + a = np.array([], dtype=self.get_dtype()) + b = np.array([2, 4, 7, 8, 20], dtype=self.get_dtype()) + + i = snp.intersect(a, b) + self.assertEqual(list(i), []) + self.assertEqual(len(i), 0) + self.assertEqual(i.dtype, self.get_dtype()) + + i = snp.intersect(b, a) + self.assertEqual(list(i), []) + self.assertEqual(len(i), 0) + self.assertEqual(i.dtype, self.get_dtype()) + + def test_empty_input_both(self): + """ + Check that intersect returns an empty array, both input arrays are + empty. + """ + a = np.array([], dtype=self.get_dtype()) + b = np.array([], dtype=self.get_dtype()) + + i = snp.intersect(a, b) + self.assertEqual(list(i), []) + self.assertEqual(len(i), 0) + self.assertEqual(i.dtype, self.get_dtype()) + + def test_contained(self): + """ + Check that intersect returns the common elements, if one array is + contained in the other. + """ + a = np.array([1, 2, 3, 4, 5, 6, 7, 8], dtype=self.get_dtype()) + b = np.array([4, 5, 7], dtype=self.get_dtype()) + + i = snp.intersect(a, b) + self.assertEqual(list(i), [4, 5, 7]) + self.assertEqual(i.dtype, self.get_dtype()) + + i = snp.intersect(b, a) + self.assertEqual(list(i), [4, 5, 7]) + self.assertEqual(i.dtype, self.get_dtype()) + + def test_identical(self): + """ + Check that intersect returns a copy, if the same array is passed in + twice. + """ + a = np.array([3, 4, 6, 8], dtype=self.get_dtype()) + + i = snp.intersect(a, a) + self.assertEqual(list(i), [3, 4, 6, 8]) + self.assertEqual(list(a), [3, 4, 6, 8]) + self.assertEqual(i.dtype, self.get_dtype()) + + i[0] = 1 + self.assertEqual(list(a), [3, 4, 6, 8]) + + def test_separated(self): + """ + Check that intersect returns an empty array, if all elements are + greater than all elements in the other. + """ + a = np.array([1, 2, 3], dtype=self.get_dtype()) + b = np.array([4, 6, 8], dtype=self.get_dtype()) + + i = snp.intersect(a, b) + self.assertEqual(list(i), []) + self.assertEqual(len(i), 0) + self.assertEqual(i.dtype, self.get_dtype()) + + def test_duplicates_same(self): + """ + Check that duplicates in the same array are dropped if not present in + the other array. + """ + a = np.array([1, 2, 2, 3], dtype=self.get_dtype()) + b = np.array([1, 6, 8], dtype=self.get_dtype()) + + i = snp.intersect(a, b) + self.assertEqual(list(i), [1]) + self.assertEqual(i.dtype, self.get_dtype()) + + a = np.array([1, 2, 2, 3], dtype=self.get_dtype()) + b = np.array([1, 2, 4, 6, 8], dtype=self.get_dtype()) + + i = snp.intersect(a, b) + self.assertEqual(list(i), [1, 2]) + self.assertEqual(i.dtype, self.get_dtype()) + + + def test_duplicates_both(self): + """ + Check that duplicates in the same array are kept if they are also + duplicated in the other array. + """ + a = np.array([1, 2, 2, 3], dtype=self.get_dtype()) + b = np.array([2, 2, 4, 6, 8], dtype=self.get_dtype()) + + i = snp.intersect(a, b) + self.assertEqual(list(i), [2, 2]) + self.assertEqual(i.dtype, self.get_dtype()) + + def test_raise_multi_dim(self): + """ + Check that passing in a multi dimensional array raises an exception. + """ + a = np.zeros((10, 2), dtype=self.get_dtype()) + b = np.array([2, 3, 5, 6], dtype=self.get_dtype()) + + self.assertRaises(ValueError, snp.intersect, a, b) + self.assertRaises(ValueError, snp.intersect, b, a) + self.assertRaises(ValueError, snp.intersect, a, a) + + def test_raise_non_array(self): + """ + Check that passing in a non-numpy-array raises an exception. + """ + b = np.array([2, 3, 5, 6], dtype=self.get_dtype()) + + self.assertRaises(TypeError, snp.intersect, 3, b) + self.assertRaises(TypeError, snp.intersect, b, 2) + self.assertRaises(TypeError, snp.intersect, 3, "a") + + +class IntersectTestCase_Double(IntersectBase, unittest.TestCase): + def get_dtype(self): + return 'float' + + diff --git a/tests/test_merge.py b/tests/test_merge.py new file mode 100644 index 0000000000000000000000000000000000000000..83725dabf255fdc6d78e54901797e86fd0fb3406 --- /dev/null +++ b/tests/test_merge.py @@ -0,0 +1,156 @@ + +from abc import ABCMeta, abstractmethod +import unittest +import numpy as np + +import sortednp as snp + +class MergeBase(metaclass=ABCMeta): + """ + Define general test cases for the merge method. Sub-classes need to + implement have to overwrite the dtype method. + """ + + @abstractmethod + def get_dtype(self): + """ + Returns the numpy data type, which should be used for all tests. + """ + pass + + + def test_simple(self): + """ + Check that merging two non-empty arrays returns the union of the two + arrays. + """ + a = np.array([1, 3, 7], dtype=self.get_dtype()) + b = np.array([2, 5, 6], dtype=self.get_dtype()) + + m = snp.merge(a, b) + self.assertEqual(list(m), [1, 2, 3, 5, 6, 7]) + self.assertEqual(m.dtype, self.get_dtype()) + + def test_separated(self): + """ + Check that merging two non-empty arrays returns the union of the two + arrays if all element in on array are greater than all elements in the + other. This tests the copy parts of the implementation. + """ + a = np.array([1, 3, 7], dtype=self.get_dtype()) + b = np.array([9, 10, 16], dtype=self.get_dtype()) + + m = snp.merge(a, b) + self.assertEqual(list(m), [1, 3, 7, 9, 10, 16]) + self.assertEqual(m.dtype, self.get_dtype()) + + def test_empty_single(self): + """ + Check that merging two arrays returns a copy of the first one if + the other is empty. + """ + a = np.array([1, 3, 7], dtype=self.get_dtype()) + b = np.array([], dtype=self.get_dtype()) + + m = snp.merge(a, b) + self.assertEqual(list(m), [1, 3, 7]) + self.assertEqual(list(a), [1, 3, 7]) + self.assertEqual(m.dtype, self.get_dtype()) + m[0] = 0 + self.assertEqual(list(a), [1, 3, 7]) + + m = snp.merge(b, a) + self.assertEqual(list(m), [1, 3, 7]) + self.assertEqual(list(a), [1, 3, 7]) + self.assertEqual(m.dtype, self.get_dtype()) + m[0] = 0 + self.assertEqual(list(a), [1, 3, 7]) + + + def test_empty_both(self): + """ + Check that merging two empty arrays returns an empty array. + """ + a = np.array([], dtype=self.get_dtype()) + b = np.array([], dtype=self.get_dtype()) + + m = snp.merge(a, b) + self.assertEqual(list(m), []) + self.assertEqual(len(m), 0) + self.assertEqual(m.dtype, self.get_dtype()) + + + def test_identical(self): + """ + Check that merging two identical arrays returns each element twice. + """ + a = np.array([1, 3, 7], dtype=self.get_dtype()) + + m = snp.merge(a, a) + self.assertEqual(list(m), [1, 1, 3, 3, 7, 7]) + self.assertEqual(m.dtype, self.get_dtype()) + + + def test_duplicates_same(self): + """ + Check that duplications in a single array are passed to the result. + """ + a = np.array([1, 3, 3, 7], dtype=self.get_dtype()) + b = np.array([2, 5, 6], dtype=self.get_dtype()) + + m = snp.merge(a, b) + self.assertEqual(list(m), [1, 2, 3, 3, 5, 6, 7]) + self.assertEqual(m.dtype, self.get_dtype()) + + def test_duplicates_other(self): + """ + Check that duplications in the other array are passed to the result. + """ + a = np.array([1, 3, 7], dtype=self.get_dtype()) + b = np.array([2, 3, 5, 6], dtype=self.get_dtype()) + + m = snp.merge(a, b) + self.assertEqual(list(m), [1, 2, 3, 3, 5, 6, 7]) + self.assertEqual(m.dtype, self.get_dtype()) + + def test_duplicates_both(self): + """ + Check that duplications in a single and the other array are both passed to + the result. + """ + a = np.array([1, 3, 3, 7], dtype=self.get_dtype()) + b = np.array([2, 3, 5, 6], dtype=self.get_dtype()) + + m = snp.merge(a, b) + self.assertEqual(list(m), [1, 2, 3, 3, 3, 5, 6, 7]) + self.assertEqual(m.dtype, self.get_dtype()) + + + def test_raise_multi_dim(self): + """ + Check that passing in a multi dimensional array raises an exception. + """ + a = np.zeros((10, 2), dtype=self.get_dtype()) + b = np.array([2, 3, 5, 6], dtype=self.get_dtype()) + + self.assertRaises(ValueError, snp.merge, a, b) + self.assertRaises(ValueError, snp.merge, b, a) + self.assertRaises(ValueError, snp.merge, a, a) + + def test_raise_non_array(self): + """ + Check that passing in a non-numpy-array raises an exception. + """ + b = np.array([2, 3, 5, 6], dtype=self.get_dtype()) + + self.assertRaises(TypeError, snp.merge, 3, b) + self.assertRaises(TypeError, snp.merge, b, 2) + self.assertRaises(TypeError, snp.merge, 3, "a") + + +class MergeTestCase_Double(MergeBase, unittest.TestCase): + def get_dtype(self): + return 'float' + + +