From e9e38688fd4e182a06b617f6b4422f2790919d00 Mon Sep 17 00:00:00 2001 From: st170001 Date: Wed, 15 Jan 2025 16:35:22 +0100 Subject: [PATCH 01/11] Initial solution for exercise 1 - 3 with Pytest --- diffusion2d.py | 11 +++++- tests/unit/test_diffusion2d_functions.py | 48 +++++++++++++++++++++--- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/diffusion2d.py b/diffusion2d.py index 51a07f2d..25cede54 100644 --- a/diffusion2d.py +++ b/diffusion2d.py @@ -38,6 +38,11 @@ def __init__(self): self.dt = None def initialize_domain(self, w=10., h=10., dx=0.1, dy=0.1): + assert type(w) == float, "Width must be a float" + assert type(h) == float, "Height must be a float" + assert type(dx) == float, "dx must be a float" + assert type(dy) == float, "dy must be a float" + self.w = w self.h = h self.dx = dx @@ -45,7 +50,11 @@ def initialize_domain(self, w=10., h=10., dx=0.1, dy=0.1): self.nx = int(w / dx) self.ny = int(h / dy) - def initialize_physical_parameters(self, d=4., T_cold=300, T_hot=700): + def initialize_physical_parameters(self, d=4., T_cold=300., T_hot=700.): + assert type(d) == float, "Thermal diffusivity must be a float" + assert type(T_cold) == float, "Cold temperature must be a float" + assert type(T_hot) == float, "Hot temperature must be a float" + self.D = d self.T_cold = T_cold self.T_hot = T_hot diff --git a/tests/unit/test_diffusion2d_functions.py b/tests/unit/test_diffusion2d_functions.py index c4277ffd..2b76a19c 100644 --- a/tests/unit/test_diffusion2d_functions.py +++ b/tests/unit/test_diffusion2d_functions.py @@ -3,24 +3,62 @@ """ from diffusion2d import SolveDiffusion2D +import pytest +@pytest.fixture +def solver(): + return SolveDiffusion2D() -def test_initialize_domain(): + +def test_initialize_domain(solver): """ Check function SolveDiffusion2D.initialize_domain """ - solver = SolveDiffusion2D() + w = 20. + h = 30. + dx = 0.5 + dy = 0.5 + solver.initialize_domain(w, h, dx, dy) + assert solver.nx == pytest.approx(40, abs=0.01) + assert solver.ny == pytest.approx(60, abs=0.01) + assert solver.dx == dx + assert solver.dy == dy -def test_initialize_physical_parameters(): +def test_initialize_physical_parameters(solver): """ Checks function SolveDiffusion2D.initialize_domain """ + d = 4. + T_cold = 300. + T_hot = 700. + w = 20. + h = 30. + dx = 0.5 + dy = 0.5 + solver.initialize_domain(w, h, dx, dy) + solver.initialize_physical_parameters(d, T_cold, T_hot) + assert solver.D == pytest.approx(d, abs=0.01) + assert solver.T_cold == pytest.approx(T_cold, abs=0.01) + assert solver.T_hot == pytest.approx(T_hot, abs=0.01) solver = SolveDiffusion2D() -def test_set_initial_condition(): +def test_set_initial_condition(solver): """ Checks function SolveDiffusion2D.get_initial_function """ - solver = SolveDiffusion2D() + d = 4. + T_cold = 300. + T_hot = 700. + w = 20. + h = 30. + dx = 0.5 + dy = 0.5 + solver.initialize_domain(w, h, dx, dy) + solver.initialize_physical_parameters(d, T_cold, T_hot) + u = solver.set_initial_condition() + assert u.shape == (40, 60) + assert u[0, 0] == pytest.approx(300., abs=0.01) + assert u[20, 30] == pytest.approx(300., abs=0.01) + assert u[39, 49] == pytest.approx(300., abs=0.01) From f7c34a87f95fda2244a31050d156dd07591be48f Mon Sep 17 00:00:00 2001 From: st170001 Date: Wed, 15 Jan 2025 17:31:41 +0100 Subject: [PATCH 02/11] More elaborate unit testing and modified README --- README.md | 170 +++++++++++++++++++++++ diffusion2d.py | 4 +- tests/integration/test_diffusion2d.py | 38 ++++- tests/unit/test_diffusion2d_functions.py | 51 ++++--- 4 files changed, 240 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index da66993c..1c1afe01 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,176 @@ Please follow the instructions in [python_testing_exercise.md](https://github.co ### pytest log +``` +============================= test session starts ============================== +platform linux -- Python 3.12.8, pytest-8.3.3, pluggy-1.5.0 +rootdir: /home/julian/Documents/git/testing-python-exercise-wt2425 +collected 5 items + +tests/integration/test_diffusion2d.py .. [ 40%] +tests/unit/test_diffusion2d_functions.py F.F [100%] + +=================================== FAILURES =================================== +____________________________ test_initialize_domain ____________________________ + +solver = + + def test_initialize_domain(solver): + """ + Check function SolveDiffusion2D.initialize_domain + """ + w = 20. + h = 30. + dx = 0.5 + dy = 0.5 + solver.initialize_domain(w, h, dx, dy) +> assert solver.nx == pytest.approx(40, abs=0.01) +E assert 60 == 40 ± 1.0e-02 +E +E comparison failed +E Obtained: 60 +E Expected: 40 ± 1.0e-02 + +tests/unit/test_diffusion2d_functions.py:22: AssertionError +__________________________ test_set_initial_condition __________________________ + +solver = + + def test_set_initial_condition(solver): + """ + Checks function SolveDiffusion2D.get_initial_function + """ + d = 4. + T_cold = 300. + T_hot = 700. + w = 20. + h = 30. + dx = 0.5 + dy = 0.5 + solver.initialize_domain(w, h, dx, dy) + solver.initialize_physical_parameters(d, T_cold, T_hot) + u = solver.set_initial_condition() +> assert u.shape == (40, 60) +E assert (60, 60) == (40, 60) +E +E At index 0 diff: 60 != 40 +E Use -v to get more diff + +tests/unit/test_diffusion2d_functions.py:61: AssertionError +----------------------------- Captured stdout call ----------------------------- +dt = 0.015625 +=========================== short test summary info ============================ +FAILED tests/unit/test_diffusion2d_functions.py::test_initialize_domain - ass... +FAILED tests/unit/test_diffusion2d_functions.py::test_set_initial_condition +========================= 2 failed, 3 passed in 0.73s ========================== +``` + +Changed factor `2` to `4` in formula: `self.dt = dx2 * dy2 / (4 * self.D * (dx2 + dy2))` + +``` +============================= test session starts ============================== +platform linux -- Python 3.12.8, pytest-8.3.3, pluggy-1.5.0 +rootdir: /home/julian/Documents/git/testing-python-exercise-wt2425 +collected 5 items + +tests/integration/test_diffusion2d.py .. [ 40%] +tests/unit/test_diffusion2d_functions.py .F. [100%] + +=================================== FAILURES =================================== +_____________________ test_initialize_physical_parameters ______________________ + +solver = + + def test_initialize_physical_parameters(solver): + """ + Checks function SolveDiffusion2D.initialize_domain + """ + d = 4. + T_cold = 300. + T_hot = 700. + w = 20. + h = 30. + dx = 0.5 + dy = 0.5 + solver.initialize_domain(w, h, dx, dy) + solver.initialize_physical_parameters(d, T_cold, T_hot) + assert solver.D == pytest.approx(d, abs=0.01) + assert solver.T_cold == pytest.approx(T_cold, abs=0.01) + assert solver.T_hot == pytest.approx(T_hot, abs=0.01) +> assert solver.dt == pytest.approx(0.015, abs=0.001) +E assert 0.0078125 == 0.015 ± 1.0e-03 +E +E comparison failed +E Obtained: 0.0078125 +E Expected: 0.015 ± 1.0e-03 + +tests/unit/test_diffusion2d_functions.py:44: AssertionError +----------------------------- Captured stdout call ----------------------------- +dt = 0.0078125 +=========================== short test summary info ============================ +FAILED tests/unit/test_diffusion2d_functions.py::test_initialize_physical_parameters +========================= 1 failed, 4 passed in 0.75s ========================== +``` + +Initialization with T_hot instead of T_cold: + +``` +============================= test session starts ============================== +platform linux -- Python 3.12.8, pytest-8.3.3, pluggy-1.5.0 +rootdir: /home/julian/Documents/git/testing-python-exercise-wt2425 +collected 3 items + +tests/unit/test_diffusion2d_functions.py .dt = 0.015625 +.[[700. 700. 700. 700. 700. 700.] + [700. 700. 700. 700. 700. 700.] + [700. 700. 700. 700. 700. 700.] + [700. 700. 700. 700. 700. 700.]] +F + +=================================== FAILURES =================================== +__________________________ test_set_initial_condition __________________________ + +solver = + + def test_set_initial_condition(solver): + """ + Checks function SolveDiffusion2D.get_initial_function + """ + solver.D = 4. + solver.T_cold = 300. + solver.T_hot = 700. + solver.w = 2. + solver.h = 3. + solver.dx = 0.5 + solver.dy = 0.5 + solver.nx = 4 + solver.ny = 6 + solver.dt = 0.015625 + u = solver.set_initial_condition() + print(u) + assert u.shape == (4, 6) +> assert numpy.array_equal(u, + numpy.array( + [ + [300., 300., 300., 300., 300., 300.], + [300., 300., 300., 300., 300., 300.], + [300., 300., 300., 300., 300., 300.], + [300., 300., 300., 300., 300., 300.] + ] + ) + ) +E assert False +E + where False = (array([[700., 700., 700., 700., 700., 700.],\n [700., 700., 700., 700., 700., 700.],\n [700., 700., 700., 700., 700., 700.],\n [700., 700., 700., 700., 700., 700.]]), array([[300., 300., 300., 300., 300., 300.],\n [300., 300., 300., 300., 300., 300.],\n [300., 300., 300., 300., 300., 300.],\n [300., 300., 300., 300., 300., 300.]])) +E + where = numpy.array_equal +E + and array([[300., 300., 300., 300., 300., 300.],\n [300., 300., 300., 300., 300., 300.],\n [300., 300., 300., 300., 300., 300.],\n [300., 300., 300., 300., 300., 300.]]) = ([[300.0, 300.0, 300.0, 300.0, 300.0, 300.0], [300.0, 300.0, 300.0, 300.0, 300.0, 300.0], [300.0, 300.0, 300.0, 300.0, 300.0, 300.0], [300.0, 300.0, 300.0, 300.0, 300.0, 300.0]]) +E + where = numpy.array + +tests/unit/test_diffusion2d_functions.py:70: AssertionError +=========================== short test summary info ============================ +FAILED tests/unit/test_diffusion2d_functions.py::test_set_initial_condition +========================= 1 failed, 2 passed in 0.70s ========================== +``` + ### unittest log ## Citing diff --git a/diffusion2d.py b/diffusion2d.py index 25cede54..4ca27488 100644 --- a/diffusion2d.py +++ b/diffusion2d.py @@ -50,7 +50,7 @@ def initialize_domain(self, w=10., h=10., dx=0.1, dy=0.1): self.nx = int(w / dx) self.ny = int(h / dy) - def initialize_physical_parameters(self, d=4., T_cold=300., T_hot=700.): + def initialize_physical_parameters(self, d=4., T_cold=300., T_hot=700.0): assert type(d) == float, "Thermal diffusivity must be a float" assert type(T_cold) == float, "Cold temperature must be a float" assert type(T_hot) == float, "Hot temperature must be a float" @@ -66,7 +66,7 @@ def initialize_physical_parameters(self, d=4., T_cold=300., T_hot=700.): print("dt = {}".format(self.dt)) def set_initial_condition(self): - u = self.T_cold * np.ones((self.nx, self.ny)) + u = self.T_hot * np.ones((self.nx, self.ny)) # Initial conditions - circle of radius r centred at (cx,cy) (mm) r, cx, cy = 2, 5, 5 diff --git a/tests/integration/test_diffusion2d.py b/tests/integration/test_diffusion2d.py index fd026b40..7df1f422 100644 --- a/tests/integration/test_diffusion2d.py +++ b/tests/integration/test_diffusion2d.py @@ -3,17 +3,49 @@ """ from diffusion2d import SolveDiffusion2D +import pytest +@pytest.fixture +def solver(): + return SolveDiffusion2D() -def test_initialize_physical_parameters(): +def test_initialize_physical_parameters(solver): """ Checks function SolveDiffusion2D.initialize_domain """ + d = 4. + T_cold = 300. + T_hot = 700. + w = 20. + h = 30. + dx = 0.5 + dy = 0.5 + solver.initialize_domain(w, h, dx, dy) + solver.initialize_physical_parameters(d, T_cold, T_hot) + assert solver.D == pytest.approx(d, abs=0.01) + assert solver.T_cold == pytest.approx(T_cold, abs=0.01) + assert solver.T_hot == pytest.approx(T_hot, abs=0.01) + assert solver.dt == pytest.approx(0.015, abs=0.001) + solver.dx * solver.dy / (4 * solver.D) solver = SolveDiffusion2D() -def test_set_initial_condition(): +def test_set_initial_condition(solver): """ Checks function SolveDiffusion2D.get_initial_function """ - solver = SolveDiffusion2D() + d = 4. + T_cold = 300. + T_hot = 700. + w = 20. + h = 30. + dx = 0.5 + dy = 0.5 + solver.initialize_domain(w, h, dx, dy) + solver.initialize_physical_parameters(d, T_cold, T_hot) + u = solver.set_initial_condition() + assert u.shape == (40, 60) + for x in range(40): + for y in range(60): + print(x, y) + assert u[x, y] == pytest.approx(300., abs=0.01) diff --git a/tests/unit/test_diffusion2d_functions.py b/tests/unit/test_diffusion2d_functions.py index 2b76a19c..a4eaacca 100644 --- a/tests/unit/test_diffusion2d_functions.py +++ b/tests/unit/test_diffusion2d_functions.py @@ -4,6 +4,9 @@ from diffusion2d import SolveDiffusion2D import pytest +import sys +import numpy +numpy.set_printoptions(threshold=sys.maxsize) @pytest.fixture def solver(): @@ -32,15 +35,18 @@ def test_initialize_physical_parameters(solver): d = 4. T_cold = 300. T_hot = 700. - w = 20. - h = 30. - dx = 0.5 - dy = 0.5 - solver.initialize_domain(w, h, dx, dy) + solver.w = 20. + solver.h = 30. + solver.dx = 0.5 + solver.dy = 0.5 + solver.nx = 40. + solver.ny = 60. solver.initialize_physical_parameters(d, T_cold, T_hot) assert solver.D == pytest.approx(d, abs=0.01) assert solver.T_cold == pytest.approx(T_cold, abs=0.01) assert solver.T_hot == pytest.approx(T_hot, abs=0.01) + assert solver.dt == pytest.approx(0.015, abs=0.001) + solver.dx * solver.dy / (4 * solver.D) solver = SolveDiffusion2D() @@ -48,17 +54,26 @@ def test_set_initial_condition(solver): """ Checks function SolveDiffusion2D.get_initial_function """ - d = 4. - T_cold = 300. - T_hot = 700. - w = 20. - h = 30. - dx = 0.5 - dy = 0.5 - solver.initialize_domain(w, h, dx, dy) - solver.initialize_physical_parameters(d, T_cold, T_hot) + solver.D = 4. + solver.T_cold = 300. + solver.T_hot = 700. + solver.w = 2. + solver.h = 3. + solver.dx = 0.5 + solver.dy = 0.5 + solver.nx = 4 + solver.ny = 6 + solver.dt = 0.015625 u = solver.set_initial_condition() - assert u.shape == (40, 60) - assert u[0, 0] == pytest.approx(300., abs=0.01) - assert u[20, 30] == pytest.approx(300., abs=0.01) - assert u[39, 49] == pytest.approx(300., abs=0.01) + print(u) + assert u.shape == (4, 6) + assert numpy.array_equal(u, + numpy.array( + [ + [300., 300., 300., 300., 300., 300.], + [300., 300., 300., 300., 300., 300.], + [300., 300., 300., 300., 300., 300.], + [300., 300., 300., 300., 300., 300.] + ] + ) + ) From c539cf99b495e6230b895b5a28ad23ec614c985b Mon Sep 17 00:00:00 2001 From: st170001 Date: Fri, 17 Jan 2025 14:15:05 +0100 Subject: [PATCH 03/11] Fixed intentionally introduced error --- diffusion2d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diffusion2d.py b/diffusion2d.py index 4ca27488..4c42f95e 100644 --- a/diffusion2d.py +++ b/diffusion2d.py @@ -66,7 +66,7 @@ def initialize_physical_parameters(self, d=4., T_cold=300., T_hot=700.0): print("dt = {}".format(self.dt)) def set_initial_condition(self): - u = self.T_hot * np.ones((self.nx, self.ny)) + u = self.T_cold * np.ones((self.nx, self.ny)) # Initial conditions - circle of radius r centred at (cx,cy) (mm) r, cx, cy = 2, 5, 5 From 774a3641e2924015053bc75a0a5be6ed2b0e9ff9 Mon Sep 17 00:00:00 2001 From: st170001 Date: Fri, 17 Jan 2025 15:35:08 +0100 Subject: [PATCH 04/11] Use unittest framework instead of pytest --- tests/integration/test_diffusion2d.py | 1 - tests/unit/test_diffusion2d_functions.py | 136 ++++++++++++----------- 2 files changed, 69 insertions(+), 68 deletions(-) diff --git a/tests/integration/test_diffusion2d.py b/tests/integration/test_diffusion2d.py index 7df1f422..f869ca7e 100644 --- a/tests/integration/test_diffusion2d.py +++ b/tests/integration/test_diffusion2d.py @@ -47,5 +47,4 @@ def test_set_initial_condition(solver): assert u.shape == (40, 60) for x in range(40): for y in range(60): - print(x, y) assert u[x, y] == pytest.approx(300., abs=0.01) diff --git a/tests/unit/test_diffusion2d_functions.py b/tests/unit/test_diffusion2d_functions.py index a4eaacca..3fe21d59 100644 --- a/tests/unit/test_diffusion2d_functions.py +++ b/tests/unit/test_diffusion2d_functions.py @@ -1,79 +1,81 @@ """ -Tests for functions in class SolveDiffusion2D +Tests for functions in class SolveDiffusion2D using unittest """ +import unittest from diffusion2d import SolveDiffusion2D -import pytest -import sys import numpy -numpy.set_printoptions(threshold=sys.maxsize) +import sys -@pytest.fixture -def solver(): - return SolveDiffusion2D() +# Append parent directory path to allow SolveDiffusion2D import. +sys.path.append('../..') + +numpy.set_printoptions(threshold=sys.maxsize) +class TestDiffusion2D(unittest.TestCase): -def test_initialize_domain(solver): - """ - Check function SolveDiffusion2D.initialize_domain - """ - w = 20. - h = 30. - dx = 0.5 - dy = 0.5 - solver.initialize_domain(w, h, dx, dy) - assert solver.nx == pytest.approx(40, abs=0.01) - assert solver.ny == pytest.approx(60, abs=0.01) - assert solver.dx == dx - assert solver.dy == dy + def setUp(self): + self.solver = SolveDiffusion2D() + def test_initialize_domain(self): + """ + Check function SolveDiffusion2D.initialize_domain + """ + w = 20. + h = 30. + dx = 0.5 + dy = 0.5 + self.solver.initialize_domain(w, h, dx, dy) + self.assertAlmostEqual(self.solver.nx, 40, places=2) + self.assertAlmostEqual(self.solver.ny, 60, places=2) + self.assertEqual(self.solver.dx, dx) + self.assertEqual(self.solver.dy, dy) -def test_initialize_physical_parameters(solver): - """ - Checks function SolveDiffusion2D.initialize_domain - """ - d = 4. - T_cold = 300. - T_hot = 700. - solver.w = 20. - solver.h = 30. - solver.dx = 0.5 - solver.dy = 0.5 - solver.nx = 40. - solver.ny = 60. - solver.initialize_physical_parameters(d, T_cold, T_hot) - assert solver.D == pytest.approx(d, abs=0.01) - assert solver.T_cold == pytest.approx(T_cold, abs=0.01) - assert solver.T_hot == pytest.approx(T_hot, abs=0.01) - assert solver.dt == pytest.approx(0.015, abs=0.001) - solver.dx * solver.dy / (4 * solver.D) - solver = SolveDiffusion2D() + def test_initialize_physical_parameters(self): + """ + Checks function SolveDiffusion2D.initialize_physical_parameters + """ + d = 4. + T_cold = 300. + T_hot = 700. + self.solver.w = 20. + self.solver.h = 30. + self.solver.dx = 0.5 + self.solver.dy = 0.5 + self.solver.nx = 40. + self.solver.ny = 60. + self.solver.initialize_physical_parameters(d, T_cold, T_hot) + self.assertAlmostEqual(self.solver.D, d, places=2) + self.assertAlmostEqual(self.solver.T_cold, T_cold, places=2) + self.assertAlmostEqual(self.solver.T_hot, T_hot, places=2) + self.assertAlmostEqual(self.solver.dt, 0.015, places=2) + def test_set_initial_condition(self): + """ + Checks function SolveDiffusion2D.set_initial_condition + """ + self.solver.D = 4. + self.solver.T_cold = 300. + self.solver.T_hot = 700. + self.solver.w = 2. + self.solver.h = 3. + self.solver.dx = 0.5 + self.solver.dy = 0.5 + self.solver.nx = 4 + self.solver.ny = 6 + self.solver.dt = 0.015625 + u = self.solver.set_initial_condition() + self.assertEqual(u.shape, (4, 6)) + self.assertTrue(numpy.array_equal(u, + numpy.array( + [ + [300., 300., 300., 300., 300., 300.], + [300., 300., 300., 300., 300., 300.], + [300., 300., 300., 300., 300., 300.], + [300., 300., 300., 300., 300., 300.] + ] + ) + )) -def test_set_initial_condition(solver): - """ - Checks function SolveDiffusion2D.get_initial_function - """ - solver.D = 4. - solver.T_cold = 300. - solver.T_hot = 700. - solver.w = 2. - solver.h = 3. - solver.dx = 0.5 - solver.dy = 0.5 - solver.nx = 4 - solver.ny = 6 - solver.dt = 0.015625 - u = solver.set_initial_condition() - print(u) - assert u.shape == (4, 6) - assert numpy.array_equal(u, - numpy.array( - [ - [300., 300., 300., 300., 300., 300.], - [300., 300., 300., 300., 300., 300.], - [300., 300., 300., 300., 300., 300.], - [300., 300., 300., 300., 300., 300.] - ] - ) - ) +if __name__ == '__main__': + unittest.main() From 64d1b2cf90c1de25620288752fe630cbf59aff26 Mon Sep 17 00:00:00 2001 From: st170001 Date: Fri, 17 Jan 2025 19:05:33 +0100 Subject: [PATCH 05/11] Refined unittests with tolerance for array comparisons --- .gitignore | 103 +++++++++++++++++++++++ __init__.py | 0 tests/__init__.py | 0 tests/unit/__init__.py | 0 tests/unit/test_diffusion2d_functions.py | 23 ++--- 5 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 .gitignore create mode 100644 __init__.py create mode 100644 tests/__init__.py create mode 100644 tests/unit/__init__.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..9237a10f --- /dev/null +++ b/.gitignore @@ -0,0 +1,103 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/test_diffusion2d_functions.py b/tests/unit/test_diffusion2d_functions.py index 3fe21d59..9a9f93ad 100644 --- a/tests/unit/test_diffusion2d_functions.py +++ b/tests/unit/test_diffusion2d_functions.py @@ -7,7 +7,7 @@ import numpy import sys -# Append parent directory path to allow SolveDiffusion2D import. +# Append parent directory path to system paths to allow SolveDiffusion2D import. sys.path.append('../..') numpy.set_printoptions(threshold=sys.maxsize) @@ -26,8 +26,8 @@ def test_initialize_domain(self): dx = 0.5 dy = 0.5 self.solver.initialize_domain(w, h, dx, dy) - self.assertAlmostEqual(self.solver.nx, 40, places=2) - self.assertAlmostEqual(self.solver.ny, 60, places=2) + self.assertAlmostEqual(self.solver.nx, 40, places=3) + self.assertAlmostEqual(self.solver.ny, 60, places=3) self.assertEqual(self.solver.dx, dx) self.assertEqual(self.solver.dy, dy) @@ -45,10 +45,10 @@ def test_initialize_physical_parameters(self): self.solver.nx = 40. self.solver.ny = 60. self.solver.initialize_physical_parameters(d, T_cold, T_hot) - self.assertAlmostEqual(self.solver.D, d, places=2) - self.assertAlmostEqual(self.solver.T_cold, T_cold, places=2) - self.assertAlmostEqual(self.solver.T_hot, T_hot, places=2) - self.assertAlmostEqual(self.solver.dt, 0.015, places=2) + self.assertAlmostEqual(self.solver.D, d, places=3) + self.assertAlmostEqual(self.solver.T_cold, T_cold, places=3) + self.assertAlmostEqual(self.solver.T_hot, T_hot, places=3) + self.assertAlmostEqual(self.solver.dt, 0.016, places=3) def test_set_initial_condition(self): """ @@ -66,15 +66,18 @@ def test_set_initial_condition(self): self.solver.dt = 0.015625 u = self.solver.set_initial_condition() self.assertEqual(u.shape, (4, 6)) - self.assertTrue(numpy.array_equal(u, - numpy.array( + # numpy.allclose allows an absolute tolerance of 1e-03, 3 places in this configuration. + self.assertTrue(numpy.allclose(a=u, + b=numpy.array( [ [300., 300., 300., 300., 300., 300.], [300., 300., 300., 300., 300., 300.], [300., 300., 300., 300., 300., 300.], [300., 300., 300., 300., 300., 300.] ] - ) + ), + atol=1e-03, + rtol=0 )) if __name__ == '__main__': From 2a5023a23e457be7ab10e52c9c7d94561b12be1b Mon Sep 17 00:00:00 2001 From: st170001 Date: Fri, 17 Jan 2025 19:10:17 +0100 Subject: [PATCH 06/11] Added failures on modifications for unittests --- README.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++ diffusion2d.py | 2 +- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1c1afe01..651dbdd7 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,60 @@ FAILED tests/unit/test_diffusion2d_functions.py::test_set_initial_condition ### unittest log +Modifications similar to pytest logs. + +``` +====================================================================== +FAIL: test_initialize_domain (tests.unit.test_diffusion2d_functions.TestDiffusion2D.test_initialize_domain) +Check function SolveDiffusion2D.initialize_domain +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/julian/Documents/git/testing-python-exercise-wt2425/tests/unit/test_diffusion2d_functions.py", line 29, in test_initialize_domain + self.assertAlmostEqual(self.solver.nx, 40, places=3) +AssertionError: 60 != 40 within 3 places (20 difference) + +---------------------------------------------------------------------- +Ran 3 tests in 0.001s + +FAILED (failures=1) +``` + +``` +====================================================================== +FAIL: test_initialize_physical_parameters (tests.unit.test_diffusion2d_functions.TestDiffusion2D.test_initialize_physical_parameters) +Checks function SolveDiffusion2D.initialize_physical_parameters +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/julian/Documents/git/testing-python-exercise-wt2425/tests/unit/test_diffusion2d_functions.py", line 51, in test_initialize_physical_parameters + self.assertAlmostEqual(self.solver.dt, 0.016, places=3) +AssertionError: 0.0078125 != 0.016 within 3 places (0.0081875 difference) + +---------------------------------------------------------------------- +Ran 3 tests in 0.001s + +FAILED (failures=1) +``` + +``` +====================================================================== +FAIL: test_set_initial_condition (tests.unit.test_diffusion2d_functions.TestDiffusion2D.test_set_initial_condition) +Checks function SolveDiffusion2D.set_initial_condition +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/home/julian/Documents/git/testing-python-exercise-wt2425/tests/unit/test_diffusion2d_functions.py", line 70, in test_set_initial_condition + self.assertTrue(numpy.allclose(a=u, +AssertionError: False is not true + +---------------------------------------------------------------------- +Ran 3 tests in 0.001s + +FAILED (failures=1) +``` + ## Citing The code used in this exercise is based on [Chapter 7 of the book "Learning Scientific Programming with Python"](https://scipython.com/book/chapter-7-matplotlib/examples/the-two-dimensional-diffusion-equation/). + +``` + +``` diff --git a/diffusion2d.py b/diffusion2d.py index 4c42f95e..4ca27488 100644 --- a/diffusion2d.py +++ b/diffusion2d.py @@ -66,7 +66,7 @@ def initialize_physical_parameters(self, d=4., T_cold=300., T_hot=700.0): print("dt = {}".format(self.dt)) def set_initial_condition(self): - u = self.T_cold * np.ones((self.nx, self.ny)) + u = self.T_hot * np.ones((self.nx, self.ny)) # Initial conditions - circle of radius r centred at (cx,cy) (mm) r, cx, cy = 2, 5, 5 From edbf28eb078c954ac41fbe9690cbb1a7a73a46e9 Mon Sep 17 00:00:00 2001 From: st170001 Date: Fri, 17 Jan 2025 19:13:35 +0100 Subject: [PATCH 07/11] Adapted unitest log to numpy.allclose --- README.md | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 651dbdd7..e11c18ce 100644 --- a/README.md +++ b/README.md @@ -120,22 +120,17 @@ FAILED tests/unit/test_diffusion2d_functions.py::test_initialize_physical_parame Initialization with T_hot instead of T_cold: ``` -============================= test session starts ============================== +=================== test session starts =================== platform linux -- Python 3.12.8, pytest-8.3.3, pluggy-1.5.0 rootdir: /home/julian/Documents/git/testing-python-exercise-wt2425 collected 3 items -tests/unit/test_diffusion2d_functions.py .dt = 0.015625 -.[[700. 700. 700. 700. 700. 700.] - [700. 700. 700. 700. 700. 700.] - [700. 700. 700. 700. 700. 700.] - [700. 700. 700. 700. 700. 700.]] -F +tests/unit/test_diffusion2d_functions.py ..F [100%] -=================================== FAILURES =================================== -__________________________ test_set_initial_condition __________________________ +======================== FAILURES ========================= +_______________ test_set_initial_condition ________________ -solver = +solver = def test_set_initial_condition(solver): """ @@ -154,26 +149,33 @@ solver = u = solver.set_initial_condition() print(u) assert u.shape == (4, 6) -> assert numpy.array_equal(u, - numpy.array( +> assert numpy.allclose(a=u, + b=numpy.array( [ [300., 300., 300., 300., 300., 300.], [300., 300., 300., 300., 300., 300.], [300., 300., 300., 300., 300., 300.], [300., 300., 300., 300., 300., 300.] ] - ) + ), + atol=1e-3, + rtol=0, ) E assert False -E + where False = (array([[700., 700., 700., 700., 700., 700.],\n [700., 700., 700., 700., 700., 700.],\n [700., 700., 700., 700., 700., 700.],\n [700., 700., 700., 700., 700., 700.]]), array([[300., 300., 300., 300., 300., 300.],\n [300., 300., 300., 300., 300., 300.],\n [300., 300., 300., 300., 300., 300.],\n [300., 300., 300., 300., 300., 300.]])) -E + where = numpy.array_equal +E + where False = (a=array([[700., 700., 700., 700., 700., 700.],\n [700., 700., 700., 700., 700., 700.],\n [700., 700., 700., 700., 700., 700.],\n [700., 700., 700., 700., 700., 700.]]), b=array([[300., 300., 300., 300., 300., 300.],\n [300., 300., 300., 300., 300., 300.],\n [300., 300., 300., 300., 300., 300.],\n [300., 300., 300., 300., 300., 300.]]), atol=0.001, rtol=0) +E + where = numpy.allclose E + and array([[300., 300., 300., 300., 300., 300.],\n [300., 300., 300., 300., 300., 300.],\n [300., 300., 300., 300., 300., 300.],\n [300., 300., 300., 300., 300., 300.]]) = ([[300.0, 300.0, 300.0, 300.0, 300.0, 300.0], [300.0, 300.0, 300.0, 300.0, 300.0, 300.0], [300.0, 300.0, 300.0, 300.0, 300.0, 300.0], [300.0, 300.0, 300.0, 300.0, 300.0, 300.0]]) E + where = numpy.array tests/unit/test_diffusion2d_functions.py:70: AssertionError -=========================== short test summary info ============================ -FAILED tests/unit/test_diffusion2d_functions.py::test_set_initial_condition -========================= 1 failed, 2 passed in 0.70s ========================== +------------------ Captured stdout call ------------------- +[[700. 700. 700. 700. 700. 700.] + [700. 700. 700. 700. 700. 700.] + [700. 700. 700. 700. 700. 700.] + [700. 700. 700. 700. 700. 700.]] +================= short test summary info ================= +FAILED tests/unit/test_diffusion2d_functions.py::test_set_initial_condition - assert False +=============== 1 failed, 2 passed in 0.41s =============== ``` ### unittest log From f857cf1c033589d5614fcf553e60aeee09476a51 Mon Sep 17 00:00:00 2001 From: st170001 Date: Fri, 17 Jan 2025 19:24:12 +0100 Subject: [PATCH 08/11] Integration test logs --- README.md | 109 ++++++++++++++++++++++- diffusion2d.py | 25 +++--- tests/integration/test_diffusion2d.py | 48 ++++++---- tests/unit/test_diffusion2d_functions.py | 61 +++++++------ 4 files changed, 185 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index e11c18ce..c4b9e8bc 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,8 @@ Please follow the instructions in [python_testing_exercise.md](https://github.co ============================= test session starts ============================== platform linux -- Python 3.12.8, pytest-8.3.3, pluggy-1.5.0 rootdir: /home/julian/Documents/git/testing-python-exercise-wt2425 -collected 5 items +collected 3 items -tests/integration/test_diffusion2d.py .. [ 40%] tests/unit/test_diffusion2d_functions.py F.F [100%] =================================== FAILURES =================================== @@ -178,6 +177,112 @@ FAILED tests/unit/test_diffusion2d_functions.py::test_set_initial_condition - as =============== 1 failed, 2 passed in 0.41s =============== ``` +#### Integration tests + +Changed factor `2` to `4` in formula: `self.dt = dx2 * dy2 / (4 * self.D * (dx2 + dy2))` + +``` +======================= test session starts ======================= +platform linux -- Python 3.12.8, pytest-8.3.3, pluggy-1.5.0 +rootdir: /home/julian/Documents/git/testing-python-exercise-wt2425 +collected 2 items + +tests/integration/test_diffusion2d.py F. [100%] + +============================ FAILURES ============================= +_______________ test_initialize_physical_parameters _______________ + +solver = + + def test_initialize_physical_parameters(solver): + """ + Checks function SolveDiffusion2D.initialize_domain + """ + d = 4.0 + T_cold = 300.0 + T_hot = 700.0 + w = 20.0 + h = 30.0 + dx = 0.5 + dy = 0.5 + solver.initialize_domain(w, h, dx, dy) + solver.initialize_physical_parameters(d, T_cold, T_hot) + assert solver.D == pytest.approx(d, abs=0.001) + assert solver.T_cold == pytest.approx(T_cold, abs=0.001) + assert solver.T_hot == pytest.approx(T_hot, abs=0.001) +> assert solver.dt == pytest.approx(0.015, abs=0.001) +E assert 0.0078125 == 0.015 ± 1.0e-03 +E +E comparison failed +E Obtained: 0.0078125 +E Expected: 0.015 ± 1.0e-03 + +tests/integration/test_diffusion2d.py:31: AssertionError +---------------------- Captured stdout call ----------------------- +dt = 0.0078125 +===================== short test summary info ===================== +FAILED tests/integration/test_diffusion2d.py::test_initialize_physical_parameters - assert 0.0078125 == 0.015 ± 1.0e-03 +=================== 1 failed, 1 passed in 0.42s =================== +``` + +Initialization with T_hot instead of T_cold: + +``` +======================= test session starts ======================= +platform linux -- Python 3.12.8, pytest-8.3.3, pluggy-1.5.0 +rootdir: /home/julian/Documents/git/testing-python-exercise-wt2425 +collected 2 items + +tests/integration/test_diffusion2d.py .F [100%] + +============================ FAILURES ============================= +___________________ test_set_initial_condition ____________________ + +solver = + + def test_set_initial_condition(solver): + """ + Checks function SolveDiffusion2D.get_initial_function + """ + d = 4.0 + T_cold = 300.0 + T_hot = 700.0 + w = 2.0 + h = 3.0 + dx = 0.5 + dy = 0.5 + solver.initialize_domain(w, h, dx, dy) + solver.initialize_physical_parameters(d, T_cold, T_hot) + u = solver.set_initial_condition() + assert u.shape == (4, 6) + # numpy.allclose allows an absolute tolerance of 1e-03, 3 places in this configuration. +> assert numpy.allclose( + a=u, + b=numpy.array( + [ + [300.0, 300.0, 300.0, 300.0, 300.0, 300.0], + [300.0, 300.0, 300.0, 300.0, 300.0, 300.0], + [300.0, 300.0, 300.0, 300.0, 300.0, 300.0], + [300.0, 300.0, 300.0, 300.0, 300.0, 300.0], + ] + ), + atol=1e-03, + rtol=0, + ) +E assert False +E + where False = (a=array([[700., 700., 700., 700., 700., 700.],\n [700., 700., 700., 700., 700., 700.],\n [700., 700., 700., 700., 700., 700.],\n [700., 700., 700., 700., 700., 700.]]), b=array([[300., 300., 300., 300., 300., 300.],\n [300., 300., 300., 300., 300., 300.],\n [300., 300., 300., 300., 300., 300.],\n [300., 300., 300., 300., 300., 300.]]), atol=0.001, rtol=0) +E + where = numpy.allclose +E + and array([[300., 300., 300., 300., 300., 300.],\n [300., 300., 300., 300., 300., 300.],\n [300., 300., 300., 300., 300., 300.],\n [300., 300., 300., 300., 300., 300.]]) = ([[300.0, 300.0, 300.0, 300.0, 300.0, 300.0], [300.0, 300.0, 300.0, 300.0, 300.0, 300.0], [300.0, 300.0, 300.0, 300.0, 300.0, 300.0], [300.0, 300.0, 300.0, 300.0, 300.0, 300.0]]) +E + where = numpy.array + +tests/integration/test_diffusion2d.py:52: AssertionError +---------------------- Captured stdout call ----------------------- +dt = 0.015625 +===================== short test summary info ===================== +FAILED tests/integration/test_diffusion2d.py::test_set_initial_condition - assert False +=================== 1 failed, 1 passed in 0.41s =================== +``` + ### unittest log Modifications similar to pytest logs. diff --git a/diffusion2d.py b/diffusion2d.py index 4ca27488..814e0f4d 100644 --- a/diffusion2d.py +++ b/diffusion2d.py @@ -37,12 +37,12 @@ def __init__(self): # Timestep self.dt = None - def initialize_domain(self, w=10., h=10., dx=0.1, dy=0.1): + def initialize_domain(self, w=10.0, h=10.0, dx=0.1, dy=0.1): assert type(w) == float, "Width must be a float" assert type(h) == float, "Height must be a float" assert type(dx) == float, "dx must be a float" assert type(dy) == float, "dy must be a float" - + self.w = w self.h = h self.dx = dx @@ -50,11 +50,11 @@ def initialize_domain(self, w=10., h=10., dx=0.1, dy=0.1): self.nx = int(w / dx) self.ny = int(h / dy) - def initialize_physical_parameters(self, d=4., T_cold=300., T_hot=700.0): + def initialize_physical_parameters(self, d=4.0, T_cold=300.0, T_hot=700.0): assert type(d) == float, "Thermal diffusivity must be a float" assert type(T_cold) == float, "Cold temperature must be a float" assert type(T_hot) == float, "Hot temperature must be a float" - + self.D = d self.T_cold = T_cold self.T_hot = T_hot @@ -66,11 +66,11 @@ def initialize_physical_parameters(self, d=4., T_cold=300., T_hot=700.0): print("dt = {}".format(self.dt)) def set_initial_condition(self): - u = self.T_hot * np.ones((self.nx, self.ny)) + u = self.T_cold * np.ones((self.nx, self.ny)) # Initial conditions - circle of radius r centred at (cx,cy) (mm) r, cx, cy = 2, 5, 5 - r2 = r ** 2 + r2 = r**2 for i in range(self.nx): for j in range(self.ny): p2 = (i * self.dx - cx) ** 2 + (j * self.dy - cy) ** 2 @@ -87,17 +87,20 @@ def do_timestep(self, u_nm1): # Propagate with forward-difference in time, central-difference in space u[1:-1, 1:-1] = u_nm1[1:-1, 1:-1] + self.D * self.dt * ( - (u_nm1[2:, 1:-1] - 2 * u_nm1[1:-1, 1:-1] + u_nm1[:-2, 1:-1]) / dx2 - + (u_nm1[1:-1, 2:] - 2 * u_nm1[1:-1, 1:-1] + u_nm1[1:-1, :-2]) / dy2) + (u_nm1[2:, 1:-1] - 2 * u_nm1[1:-1, 1:-1] + u_nm1[:-2, 1:-1]) / dx2 + + (u_nm1[1:-1, 2:] - 2 * u_nm1[1:-1, 1:-1] + u_nm1[1:-1, :-2]) / dy2 + ) return u.copy() def create_figure(self, fig, u, n, fignum): fignum += 1 ax = fig.add_subplot(220 + fignum) - im = ax.imshow(u.copy(), cmap=plt.get_cmap('hot'), vmin=self.T_cold, vmax=self.T_hot) + im = ax.imshow( + u.copy(), cmap=plt.get_cmap("hot"), vmin=self.T_cold, vmax=self.T_hot + ) ax.set_axis_off() - ax.set_title('{:.1f} ms'.format(n * self.dt * 1000)) + ax.set_title("{:.1f} ms".format(n * self.dt * 1000)) return fignum, im @@ -105,7 +108,7 @@ def create_figure(self, fig, u, n, fignum): def output_figure(fig, im): fig.subplots_adjust(right=0.85) cbar_ax = fig.add_axes([0.9, 0.15, 0.03, 0.7]) - cbar_ax.set_xlabel('$T$ / K', labelpad=20) + cbar_ax.set_xlabel("$T$ / K", labelpad=20) fig.colorbar(im, cax=cbar_ax) plt.show() diff --git a/tests/integration/test_diffusion2d.py b/tests/integration/test_diffusion2d.py index f869ca7e..e6933812 100644 --- a/tests/integration/test_diffusion2d.py +++ b/tests/integration/test_diffusion2d.py @@ -3,28 +3,31 @@ """ from diffusion2d import SolveDiffusion2D +import numpy import pytest + @pytest.fixture def solver(): return SolveDiffusion2D() + def test_initialize_physical_parameters(solver): """ Checks function SolveDiffusion2D.initialize_domain """ - d = 4. - T_cold = 300. - T_hot = 700. - w = 20. - h = 30. + d = 4.0 + T_cold = 300.0 + T_hot = 700.0 + w = 20.0 + h = 30.0 dx = 0.5 dy = 0.5 solver.initialize_domain(w, h, dx, dy) solver.initialize_physical_parameters(d, T_cold, T_hot) - assert solver.D == pytest.approx(d, abs=0.01) - assert solver.T_cold == pytest.approx(T_cold, abs=0.01) - assert solver.T_hot == pytest.approx(T_hot, abs=0.01) + assert solver.D == pytest.approx(d, abs=0.001) + assert solver.T_cold == pytest.approx(T_cold, abs=0.001) + assert solver.T_hot == pytest.approx(T_hot, abs=0.001) assert solver.dt == pytest.approx(0.015, abs=0.001) solver.dx * solver.dy / (4 * solver.D) solver = SolveDiffusion2D() @@ -34,17 +37,28 @@ def test_set_initial_condition(solver): """ Checks function SolveDiffusion2D.get_initial_function """ - d = 4. - T_cold = 300. - T_hot = 700. - w = 20. - h = 30. + d = 4.0 + T_cold = 300.0 + T_hot = 700.0 + w = 2.0 + h = 3.0 dx = 0.5 dy = 0.5 solver.initialize_domain(w, h, dx, dy) solver.initialize_physical_parameters(d, T_cold, T_hot) u = solver.set_initial_condition() - assert u.shape == (40, 60) - for x in range(40): - for y in range(60): - assert u[x, y] == pytest.approx(300., abs=0.01) + assert u.shape == (4, 6) + # numpy.allclose allows an absolute tolerance of 1e-03, 3 places in this configuration. + assert numpy.allclose( + a=u, + b=numpy.array( + [ + [300.0, 300.0, 300.0, 300.0, 300.0, 300.0], + [300.0, 300.0, 300.0, 300.0, 300.0, 300.0], + [300.0, 300.0, 300.0, 300.0, 300.0, 300.0], + [300.0, 300.0, 300.0, 300.0, 300.0, 300.0], + ] + ), + atol=1e-03, + rtol=0, + ) diff --git a/tests/unit/test_diffusion2d_functions.py b/tests/unit/test_diffusion2d_functions.py index 9a9f93ad..8e1ee62e 100644 --- a/tests/unit/test_diffusion2d_functions.py +++ b/tests/unit/test_diffusion2d_functions.py @@ -8,10 +8,11 @@ import sys # Append parent directory path to system paths to allow SolveDiffusion2D import. -sys.path.append('../..') +sys.path.append("../..") numpy.set_printoptions(threshold=sys.maxsize) + class TestDiffusion2D(unittest.TestCase): def setUp(self): @@ -21,8 +22,8 @@ def test_initialize_domain(self): """ Check function SolveDiffusion2D.initialize_domain """ - w = 20. - h = 30. + w = 20.0 + h = 30.0 dx = 0.5 dy = 0.5 self.solver.initialize_domain(w, h, dx, dy) @@ -35,15 +36,15 @@ def test_initialize_physical_parameters(self): """ Checks function SolveDiffusion2D.initialize_physical_parameters """ - d = 4. - T_cold = 300. - T_hot = 700. - self.solver.w = 20. - self.solver.h = 30. + d = 4.0 + T_cold = 300.0 + T_hot = 700.0 + self.solver.w = 20.0 + self.solver.h = 30.0 self.solver.dx = 0.5 self.solver.dy = 0.5 - self.solver.nx = 40. - self.solver.ny = 60. + self.solver.nx = 40.0 + self.solver.ny = 60.0 self.solver.initialize_physical_parameters(d, T_cold, T_hot) self.assertAlmostEqual(self.solver.D, d, places=3) self.assertAlmostEqual(self.solver.T_cold, T_cold, places=3) @@ -54,11 +55,11 @@ def test_set_initial_condition(self): """ Checks function SolveDiffusion2D.set_initial_condition """ - self.solver.D = 4. - self.solver.T_cold = 300. - self.solver.T_hot = 700. - self.solver.w = 2. - self.solver.h = 3. + self.solver.D = 4.0 + self.solver.T_cold = 300.0 + self.solver.T_hot = 700.0 + self.solver.w = 2.0 + self.solver.h = 3.0 self.solver.dx = 0.5 self.solver.dy = 0.5 self.solver.nx = 4 @@ -67,18 +68,22 @@ def test_set_initial_condition(self): u = self.solver.set_initial_condition() self.assertEqual(u.shape, (4, 6)) # numpy.allclose allows an absolute tolerance of 1e-03, 3 places in this configuration. - self.assertTrue(numpy.allclose(a=u, - b=numpy.array( - [ - [300., 300., 300., 300., 300., 300.], - [300., 300., 300., 300., 300., 300.], - [300., 300., 300., 300., 300., 300.], - [300., 300., 300., 300., 300., 300.] - ] - ), - atol=1e-03, - rtol=0 - )) + self.assertTrue( + numpy.allclose( + a=u, + b=numpy.array( + [ + [300.0, 300.0, 300.0, 300.0, 300.0, 300.0], + [300.0, 300.0, 300.0, 300.0, 300.0, 300.0], + [300.0, 300.0, 300.0, 300.0, 300.0, 300.0], + [300.0, 300.0, 300.0, 300.0, 300.0, 300.0], + ] + ), + atol=1e-03, + rtol=0, + ) + ) + -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() From 7567bebcf20250cfc2f07ac2fdd86a2c94a25159 Mon Sep 17 00:00:00 2001 From: st170001 Date: Fri, 17 Jan 2025 19:26:45 +0100 Subject: [PATCH 09/11] Fixed pytest logs --- README.md | 70 +++++++++++++++++-------------------------------------- 1 file changed, 21 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index c4b9e8bc..51136415 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,17 @@ Please follow the instructions in [python_testing_exercise.md](https://github.co ### pytest log ``` -============================= test session starts ============================== +======================= test session starts ======================= platform linux -- Python 3.12.8, pytest-8.3.3, pluggy-1.5.0 rootdir: /home/julian/Documents/git/testing-python-exercise-wt2425 collected 3 items -tests/unit/test_diffusion2d_functions.py F.F [100%] +tests/unit/test_diffusion2d_functions.py F.. [100%] -=================================== FAILURES =================================== -____________________________ test_initialize_domain ____________________________ +============================ FAILURES ============================= +_____________________ test_initialize_domain ______________________ -solver = +solver = def test_initialize_domain(solver): """ @@ -35,55 +35,26 @@ E comparison failed E Obtained: 60 E Expected: 40 ± 1.0e-02 -tests/unit/test_diffusion2d_functions.py:22: AssertionError -__________________________ test_set_initial_condition __________________________ - -solver = - - def test_set_initial_condition(solver): - """ - Checks function SolveDiffusion2D.get_initial_function - """ - d = 4. - T_cold = 300. - T_hot = 700. - w = 20. - h = 30. - dx = 0.5 - dy = 0.5 - solver.initialize_domain(w, h, dx, dy) - solver.initialize_physical_parameters(d, T_cold, T_hot) - u = solver.set_initial_condition() -> assert u.shape == (40, 60) -E assert (60, 60) == (40, 60) -E -E At index 0 diff: 60 != 40 -E Use -v to get more diff - -tests/unit/test_diffusion2d_functions.py:61: AssertionError ------------------------------ Captured stdout call ----------------------------- -dt = 0.015625 -=========================== short test summary info ============================ -FAILED tests/unit/test_diffusion2d_functions.py::test_initialize_domain - ass... -FAILED tests/unit/test_diffusion2d_functions.py::test_set_initial_condition -========================= 2 failed, 3 passed in 0.73s ========================== +tests/unit/test_diffusion2d_functions.py:25: AssertionError +===================== short test summary info ===================== +FAILED tests/unit/test_diffusion2d_functions.py::test_initialize_domain - assert 60 == 40 ± 1.0e-02 +=================== 1 failed, 2 passed in 0.41s =================== ``` -Changed factor `2` to `4` in formula: `self.dt = dx2 * dy2 / (4 * self.D * (dx2 + dy2))` +Changed factor `2` to `4` in formula: `self.dt = dx2 * dy2 / (4 * self.D * (dx2 + dy2))`: ``` ============================= test session starts ============================== platform linux -- Python 3.12.8, pytest-8.3.3, pluggy-1.5.0 rootdir: /home/julian/Documents/git/testing-python-exercise-wt2425 -collected 5 items +collected 3 items -tests/integration/test_diffusion2d.py .. [ 40%] tests/unit/test_diffusion2d_functions.py .F. [100%] =================================== FAILURES =================================== _____________________ test_initialize_physical_parameters ______________________ -solver = +solver = def test_initialize_physical_parameters(solver): """ @@ -92,11 +63,12 @@ solver = d = 4. T_cold = 300. T_hot = 700. - w = 20. - h = 30. - dx = 0.5 - dy = 0.5 - solver.initialize_domain(w, h, dx, dy) + solver.w = 20. + solver.h = 30. + solver.dx = 0.5 + solver.dy = 0.5 + solver.nx = 40. + solver.ny = 60. solver.initialize_physical_parameters(d, T_cold, T_hot) assert solver.D == pytest.approx(d, abs=0.01) assert solver.T_cold == pytest.approx(T_cold, abs=0.01) @@ -108,12 +80,12 @@ E comparison failed E Obtained: 0.0078125 E Expected: 0.015 ± 1.0e-03 -tests/unit/test_diffusion2d_functions.py:44: AssertionError +tests/unit/test_diffusion2d_functions.py:48: AssertionError ----------------------------- Captured stdout call ----------------------------- dt = 0.0078125 =========================== short test summary info ============================ -FAILED tests/unit/test_diffusion2d_functions.py::test_initialize_physical_parameters -========================= 1 failed, 4 passed in 0.75s ========================== +FAILED tests/unit/test_diffusion2d_functions.py::test_initialize_physical_parameters - assert 0.0078125 == 0.015 ± 1.0e-03 +========================= 1 failed, 2 passed in 0.41s ========================== ``` Initialization with T_hot instead of T_cold: From c393d591b5a0c9996c5c49d9e6abc289de16ebd9 Mon Sep 17 00:00:00 2001 From: st170001 Date: Fri, 17 Jan 2025 20:07:26 +0100 Subject: [PATCH 10/11] Coverage report and tox configuration --- README.md | 4 ++++ coverage-report.pdf | Bin 0 -> 36504 bytes requirements.txt | 2 ++ tox.toml | 13 +++++++++++++ 4 files changed, 19 insertions(+) create mode 100644 coverage-report.pdf create mode 100644 requirements.txt create mode 100644 tox.toml diff --git a/README.md b/README.md index 51136415..12e8616a 100644 --- a/README.md +++ b/README.md @@ -307,6 +307,10 @@ Ran 3 tests in 0.001s FAILED (failures=1) ``` +## Coverage + +I have used the flag: `coverage run --omit="*/site-packages/*"` to exclude dependencies like numpy in the coverage report. + ## Citing The code used in this exercise is based on [Chapter 7 of the book "Learning Scientific Programming with Python"](https://scipython.com/book/chapter-7-matplotlib/examples/the-two-dimensional-diffusion-equation/). diff --git a/coverage-report.pdf b/coverage-report.pdf new file mode 100644 index 0000000000000000000000000000000000000000..745d4315364e1adff284e613fbb75a62be82f2bb GIT binary patch literal 36504 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"] +env_list = ["pytest", "unittest"] + +[env.pytest] +description = "pytest" +deps = ["pytest", "-r requirements.txt"] +commands = [["python -m pytest tests/unit/test_diffusion2d_functions.py" + ]] + +[env.unittest] +description = "unittest" +deps = ["unittest", "-r requirements.txt"] +commands = [["python -m unittest tests/integration/test_diffusion2d.py"]] \ No newline at end of file From ea3d3c0aea472b032b8fd331af70b0eae5ee4072 Mon Sep 17 00:00:00 2001 From: st170001 Date: Fri, 17 Jan 2025 20:09:58 +0100 Subject: [PATCH 11/11] Small fixes in tox.toml --- tox.toml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tox.toml b/tox.toml index e2d6465e..3ecf3a13 100644 --- a/tox.toml +++ b/tox.toml @@ -4,10 +4,9 @@ env_list = ["pytest", "unittest"] [env.pytest] description = "pytest" deps = ["pytest", "-r requirements.txt"] -commands = [["python -m pytest tests/unit/test_diffusion2d_functions.py" - ]] +commands = [["python", "-m", "pytest", "tests/unit/test_diffusion2d_functions.py"]] [env.unittest] description = "unittest" -deps = ["unittest", "-r requirements.txt"] -commands = [["python -m unittest tests/integration/test_diffusion2d.py"]] \ No newline at end of file +deps = ["-r requirements.txt"] +commands = [["python", "-m", "unittest", "tests/integration/test_diffusion2d.py"]] \ No newline at end of file