diff --git a/.github/workflows/pylint.yaml b/.github/workflows/pylint.yaml index e28c3a0..772da58 100644 --- a/.github/workflows/pylint.yaml +++ b/.github/workflows/pylint.yaml @@ -21,4 +21,4 @@ jobs: pip install numpy - name: Analysing the code with pylint run: | - pylint $(git ls-files '*.py') \ No newline at end of file + pylint $(git ls-files 'spyrograph/*.py') \ No newline at end of file diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml new file mode 100644 index 0000000..e77ffd0 --- /dev/null +++ b/.github/workflows/pytest.yaml @@ -0,0 +1,23 @@ +name: Pytest + +on: [pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest pandas numpy + - name: Unit testing the code with pytest + run: | + pytest tests \ No newline at end of file diff --git a/.pylintrc b/.pylintrc index 35d3330..06dc518 100644 --- a/.pylintrc +++ b/.pylintrc @@ -57,7 +57,7 @@ ignore-paths= # Files or directories matching the regular expression patterns are skipped. # The regex matches against base names, not paths. The default value ignores # Emacs file locks -ignore-patterns=^\.# +ignore-patterns=^\.#,tests # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/_roulette.py b/tests/_roulette.py new file mode 100644 index 0000000..f840c14 --- /dev/null +++ b/tests/_roulette.py @@ -0,0 +1,123 @@ +import pytest + +import numpy as np +import pandas as pd + +class _TestGeneral: + # Define this class attr in subclasses + class_name = None + @pytest.fixture() + def thetas(self) -> "np.array": + """Return a numpy array of theta values""" + return np.arange(0, np.pi*2, .1) + + @pytest.fixture() + def instance(self, thetas): + """Return an instance of the shape""" + return self.class_name( + R = 300, + r = 200, + d = 100, + thetas=thetas + ) + + def test_theta_range(self) -> None: + """Test that passing theta start, stop, step creates an expected list of theta values""" + obj = self.class_name( + R = 300, + r = 200, + d = 100, + theta_start=1, + theta_stop=10, + theta_step=1 + ) + assert all(obj.thetas == np.arange(1,10,1)) + + def test_theta_range_default_step(self) -> None: + """Test that passing theta start, stop, step creates an expected list of theta values""" + obj = self.class_name( + R = 300, + r = 200, + d = 100, + theta_start=0, + theta_stop=1 + ) + + def test_multiple_thetas_exception(self, thetas: "np.array") -> None: + """Test that passing multiple definitions of setting theta raises a ValueError""" + with pytest.raises(ValueError): + obj = self.class_name( + R = 300, + r = 200, + d = 100, + thetas=thetas, + theta_start=1, + theta_stop=10, + theta_step=1 + ) + + def test_coords(self, instance) -> None: + """Test that coordinates attribute matches x, y, and theta values""" + assert instance.coords[0] == (instance.x[0], instance.y[0], instance.thetas[0]) + assert instance.coords[-1] == (instance.x[-1], instance.y[-1], instance.thetas[-1]) + + def test_dataframe_property(self, instance) -> None: + """Test that DataFrame property is working as expected""" + assert isinstance(instance.df, pd.DataFrame) + assert list(instance.df.columns) == ["x", "y", "theta"] + assert all(instance.df["x"].to_numpy() == instance.x) + assert all(instance.df["y"].to_numpy() == instance.y) + assert all(instance.df["theta"].to_numpy() == instance.thetas) + +class _TestSpecial: + # Define this class attr in subclasses + class_name = None + @pytest.fixture() + def thetas(self) -> "np.array": + """Return a numpy array of theta values""" + return np.arange(0, np.pi*2, .1) + + @pytest.fixture() + def instance(self, thetas): + """Return an instance of the shape""" + return self.class_name( + R = 300, + r = 200, + thetas=thetas + ) + + def test_theta_range(self) -> None: + """Test that passing theta start, stop, step creates an expected list of theta values""" + obj = self.class_name( + R = 300, + r = 200, + theta_start=1, + theta_stop=10, + theta_step=1 + ) + assert all(obj.thetas == np.arange(1,10,1)) + + def test_theta_range_default_step(self) -> None: + """Test that passing theta start, stop, step creates an expected list of theta values""" + obj = self.class_name( + R = 300, + r = 200, + theta_start=0, + theta_stop=1 + ) + + def test_multiple_thetas_exception(self, thetas: "np.array") -> None: + """Test that passing multiple definitions of setting theta raises a ValueError""" + with pytest.raises(ValueError): + obj = self.class_name( + R = 300, + r = 200, + thetas=thetas, + theta_start=1, + theta_stop=10, + theta_step=1 + ) + + def test_rolling_radius_equals_distance(self, instance): + """Test radius of rolling circle is equal to distance""" + assert instance.r == instance.d diff --git a/tests/test_epitrochoid.py b/tests/test_epitrochoid.py new file mode 100644 index 0000000..3434e55 --- /dev/null +++ b/tests/test_epitrochoid.py @@ -0,0 +1,14 @@ +import pytest + +from tests._roulette import _TestGeneral, _TestSpecial +from spyrograph.epitrochoid.epitrochoid import Epitrochoid +from spyrograph.epitrochoid.epicycloid import Epicycloid + +class TestEpitrochoid(_TestGeneral): + class_name = Epitrochoid + + def test_circle_offset(self, instance): + assert instance._circle_offset() == 500 + +class TestEpicycloid(_TestSpecial, TestEpitrochoid): + class_name = Epicycloid diff --git a/tests/test_hypotrochoid.py b/tests/test_hypotrochoid.py new file mode 100644 index 0000000..5133270 --- /dev/null +++ b/tests/test_hypotrochoid.py @@ -0,0 +1,15 @@ +import numpy as np +import pytest + +from tests._roulette import _TestGeneral, _TestSpecial +from spyrograph.hypotrochoid.hypotrochoid import Hypotrochoid +from spyrograph.hypotrochoid.hypocycloid import Hypocycloid + +class TestHypotrochoid(_TestGeneral): + class_name = Hypotrochoid + + def test_circle_offset(self, instance): + assert instance._circle_offset() == 100 + +class TestHypocycloid(_TestSpecial, TestHypotrochoid): + class_name = Hypocycloid