From 71bf94c96ccedb0163c79c46600cd41f589580ac Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 11 Jun 2019 16:37:22 -0700 Subject: [PATCH 01/28] Add packaging provisions --- requirements-dev.txt | 7 ++++++ setup.py | 53 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 requirements-dev.txt create mode 100644 setup.py diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..2edbdad --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,7 @@ +pip >= 19.1.1 + +# Packaging and Publishing +# Minimum versions for Markdown support +setuptools >= 38.6.0 +twine >= 1.12.0 # For twine check +wheel >= 0.31.0 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e8166e4 --- /dev/null +++ b/setup.py @@ -0,0 +1,53 @@ +import setuptools +from pathlib import Path + +readme_path = Path(__file__).with_name("README.md") +with open(readme_path, encoding="utf-8") as f: + long_description = f.read() + +setuptools.setup( + name="algorithm-visualizer", + version="0.1.0", + license="MIT", + + author="Example Author", + author_email="author@example.com", + + description="A visualization library for Python.", + long_description=long_description, + long_description_content_type="text/markdown", + + keywords="algorithm data-structure visualization animation", + classifiers=[ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Environment :: Web Environment", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent" + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7" + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Programming Language :: Python :: Implementation :: Stackless", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Scientific/Engineering :: Visualization" + ], + + url="https://algorithm-visualizer.org", + project_urls={ + "Documentation": "https://github.com/algorithm-visualizer/algorithm-visualizer/wiki", + "Issue Tracker": "https://github.com/algorithm-visualizer/tracers.py/issues", + "Source": "https://github.com/algorithm-visualizer/tracers.py" + }, + + packages=setuptools.find_packages(), + python_requires=">=3.5", + extras_require={ + "requests": ["requests >= 2.22.0"] + } +) From 200f91eb986c18220588c442b0bfaa96d6aebadc Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 22 Jul 2019 16:51:29 -0700 Subject: [PATCH 02/28] Fix circular import --- algorithm_visualizer/tracers/array1d.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/algorithm_visualizer/tracers/array1d.py b/algorithm_visualizer/tracers/array1d.py index b0c1cee..4b9c14c 100644 --- a/algorithm_visualizer/tracers/array1d.py +++ b/algorithm_visualizer/tracers/array1d.py @@ -1,7 +1,11 @@ -from . import chart +from typing import TYPE_CHECKING + from .array2d import Array2DTracer from algorithm_visualizer.types import Serializable, SerializableSequence, UNDEFINED +if TYPE_CHECKING: + from .chart import ChartTracer + class Array1DTracer(Array2DTracer): def set(self, array1d: SerializableSequence[Serializable] = UNDEFINED): @@ -19,5 +23,5 @@ def select(self, sx: int, ex: int = UNDEFINED): def deselect(self, sx: int, ex: int = UNDEFINED): self.command("deselect", sx, ex) - def chart(self, chartTracer: "chart.ChartTracer"): + def chart(self, chartTracer: "ChartTracer"): self.command("chart", chartTracer.key) From 3ebe55d68d16ff5ed514766fa1bad761bcdcb190 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 22 Jul 2019 16:52:38 -0700 Subject: [PATCH 03/28] Make SerializableSequence a generic type --- algorithm_visualizer/types.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/algorithm_visualizer/types.py b/algorithm_visualizer/types.py index 0308b6c..5fcbc26 100644 --- a/algorithm_visualizer/types.py +++ b/algorithm_visualizer/types.py @@ -1,10 +1,17 @@ -from typing import Union +from typing import Any, Dict, List, Tuple, TypeVar, Union + -# Types which are serializable by the default JSONEncoder -Serializable = Union[dict, list, tuple, str, int, float, bool, None] -SerializableSequence = Union[list, tuple] Number = Union[int, float] +# Types which are serializable by the default JSONEncoder +# Recursive types aren't supported yet. See https://github.com/python/mypy/issues/731 +_Keys = Union[str, int, float, bool, None] +_Collections = Union[Dict[_Keys, Any], List[Any], Tuple[Any]] +Serializable = Union[_Collections, _Keys] + +_T = TypeVar("_T", _Collections, _Keys) +SerializableSequence = Union[List[_T], Tuple[_T]] + class Undefined: pass From 7dad08dd65763ed4fa3be3ab260f54f66038c3de Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 23 Jul 2019 00:20:10 -0700 Subject: [PATCH 04/28] Add mypy and visualization.json to gitignore --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 4398feb..3d2f3ea 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,9 @@ ENV/ env.bak/ venv.bak/ +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +visualization.json From c799cabcb44805209cd3fdd10be3ce8002a842ae Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 23 Jul 2019 14:02:12 -0700 Subject: [PATCH 05/28] Fix super()-related TypeError in Array2D.create() --- algorithm_visualizer/randomize.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/algorithm_visualizer/randomize.py b/algorithm_visualizer/randomize.py index b56ca08..3c0474b 100644 --- a/algorithm_visualizer/randomize.py +++ b/algorithm_visualizer/randomize.py @@ -68,7 +68,8 @@ def sorted(self, sorted: bool = True) -> "Array2D": return self def create(self) -> List[List]: - return [super().create() for _ in range(self._N)] + # Explicitly pass args to super() to avoid a TypeError (BPO 26495). + return [super(Array2D, self).create() for _ in range(self._N)] class Graph(_Randomizer): From 83590a7b01e8acca09927b2007164221911547bc Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 23 Jul 2019 14:22:41 -0700 Subject: [PATCH 06/28] Fix using same length variable for both dimensions of Array2D --- algorithm_visualizer/randomize.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/algorithm_visualizer/randomize.py b/algorithm_visualizer/randomize.py index 3c0474b..361260d 100644 --- a/algorithm_visualizer/randomize.py +++ b/algorithm_visualizer/randomize.py @@ -60,8 +60,8 @@ def create(self) -> List: class Array2D(Array1D): def __init__(self, N: int = 10, M: int = 10, randomizer: _Randomizer = Integer()): - super().__init__(N, randomizer) - self._M = M + super().__init__(M, randomizer) + self._M = N def sorted(self, sorted: bool = True) -> "Array2D": self._sorted = sorted @@ -69,7 +69,7 @@ def sorted(self, sorted: bool = True) -> "Array2D": def create(self) -> List[List]: # Explicitly pass args to super() to avoid a TypeError (BPO 26495). - return [super(Array2D, self).create() for _ in range(self._N)] + return [super(Array2D, self).create() for _ in range(self._M)] class Graph(_Randomizer): From 31e9b53661bc68ad7f3e1659cb9ddb28ccc9baa2 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 23 Jul 2019 17:17:50 -0700 Subject: [PATCH 07/28] Add randomizer tests * Add comments to the Graph randomizer code --- algorithm_visualizer/randomize.py | 4 + tests/__init__.py | 0 tests/test_randomize.py | 193 ++++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/test_randomize.py diff --git a/algorithm_visualizer/randomize.py b/algorithm_visualizer/randomize.py index 361260d..e311120 100644 --- a/algorithm_visualizer/randomize.py +++ b/algorithm_visualizer/randomize.py @@ -93,15 +93,19 @@ def create(self) -> List[List]: for i in range(self._N): for j in range(self._N): if i == j: + # Vertex can't have an edge to itself (no loops) graph[i][j] = 0 elif self._directed or i < j: if random.random() >= self._ratio: + # Don't create an edge if the ratio is exceeded graph[i][j] = 0 elif self._weighted: graph[i][j] = self._randomizer.create() else: graph[i][j] = 1 else: + # Edge is the same for both its vertices if it is not directed + # In such case the initial weight for the edge is set above when i < j graph[i][j] = graph[j][i] return graph diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_randomize.py b/tests/test_randomize.py new file mode 100644 index 0000000..431d626 --- /dev/null +++ b/tests/test_randomize.py @@ -0,0 +1,193 @@ +import unittest + +from algorithm_visualizer import Randomize + +ITERATIONS = 100 + + +class IntegerTests(unittest.TestCase): + def test_integer_type(self): + rand = Randomize.Integer() + value = rand.create() + + self.assertIs(type(value), int) + + def test_integer_range(self): + rand_range = (1, 2) + rand = Randomize.Integer(*rand_range) + + for _ in range(ITERATIONS): + value = rand.create() + + self.assertIn(value, rand_range) + + +class DoubleTests(unittest.TestCase): + def test_double_type(self): + rand = Randomize.Double() + value = rand.create() + + self.assertIs(type(value), float) + + def test_double_range(self): + rand_range = (1.0, 2.0) + rand = Randomize.Double(*rand_range) + + for _ in range(ITERATIONS): + value = rand.create() + + self.assertGreaterEqual(value, 1.0) + self.assertLessEqual(value, 2.0) + + +class StringTests(unittest.TestCase): + def test_string_type(self): + rand = Randomize.String() + value = rand.create() + + self.assertIs(type(value), str) + + def test_string_length(self): + rand = Randomize.String(10) + value = rand.create() + + self.assertEqual(len(value), 10) + + def test_string_letters(self): + letters = ('a', 'b', 'c') + rand = Randomize.String(letters=letters) + + for _ in range(ITERATIONS): + for char in rand.create(): + self.assertIn(char, letters) + + +class Array1DTests(unittest.TestCase): + def test_array1d_length(self): + rand = Randomize.Array1D(5) + array = rand.create() + + self.assertEqual(len(array), 5) + + def test_array1d_randomizer(self): + rand = Randomize.Array1D(ITERATIONS, randomizer=Randomize.Double(2, 4)) + array = rand.create() + + for value in array: + self.assertIs(type(value), float) + self.assertGreaterEqual(value, 2) + self.assertLessEqual(value, 4) + + def test_array1d_sorted(self): + rand = Randomize.Array1D(ITERATIONS) + + sort_ret = rand.sorted(True) + self.assertIs(sort_ret, rand) + self.assertTrue(rand._sorted) + + array = rand.create() + + for i in range(len(array) - 1): + self.assertLessEqual(array[i], array[i + 1]) + + +class Array2DTests(unittest.TestCase): + def test_array2d_length(self): + rand = Randomize.Array2D(5, 3) + arrays = rand.create() + + self.assertEqual(len(arrays), 5) + + for array in arrays: + self.assertEqual(len(array), 3) + + def test_array2d_randomizer(self): + rand = Randomize.Array2D(ITERATIONS, ITERATIONS, randomizer=Randomize.Double(2, 4)) + arrays = rand.create() + + for array in arrays: + for value in array: + self.assertIs(type(value), float) + self.assertGreaterEqual(value, 2) + self.assertLessEqual(value, 4) + + def test_array2d_sorted(self): + rand = Randomize.Array2D(ITERATIONS, ITERATIONS) + + sort_ret = rand.sorted(True) + self.assertIs(sort_ret, rand) + self.assertTrue(rand._sorted) + + arrays = rand.create() + + for array in arrays: + for i in range(len(array) - 1): + self.assertLessEqual(array[i], array[i + 1]) + + +class GraphTests(unittest.TestCase): + def test_graph_length(self): + rand = Randomize.Graph(3) + graph = rand.create() + + self.assertEqual(len(graph), 3) + + for edges in graph: + self.assertEqual(len(edges), 3) + + def test_graph_no_loops(self): + rand = Randomize.Graph(ITERATIONS, ratio=1) + graph = rand.create() + + for i, edges in enumerate(graph): + for j, edge in enumerate(edges): + if i == j: + self.assertEqual(edge, 0) + + def test_graph_ratio_1(self): + rand = Randomize.Graph(ITERATIONS, ratio=1) + graph = rand.create() + + for i, edges in enumerate(graph): + for j, edge in enumerate(edges): + if i != j: + self.assertEqual(edge, 1) + + def test_graph_ratio_0(self): + rand = Randomize.Graph(ITERATIONS, ratio=0) + graph = rand.create() + + for i, edges in enumerate(graph): + for j, edge in enumerate(edges): + if i != j: + self.assertEqual(edge, 0) + + def test_graph_undirected(self): + rand = Randomize.Graph(ITERATIONS) + + directed_ret = rand.directed(False) + self.assertIs(directed_ret, rand) + self.assertFalse(rand._directed) + + graph = rand.create() + + for i, edges in enumerate(graph): + for j, edge in enumerate(edges): + if i != j: + self.assertEqual(edge, graph[j][i]) + + def test_graph_weighted(self): + rand = Randomize.Graph(ITERATIONS, ratio=1, randomizer=Randomize.Double(2, 4)) + + weighted_ret = rand.weighted() + self.assertIs(weighted_ret, rand) + self.assertTrue(rand._weighted) + + graph = rand.create() + + for i, edges in enumerate(graph): + for j, edge in enumerate(edges): + if i != j: + self.assertIs(type(edge), float) + self.assertGreaterEqual(edge, 2) + self.assertLessEqual(edge, 4) From 60f8b5d5e46905f6b61b4455ce56b3adbe5de0c2 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 25 Jul 2019 00:14:34 -0700 Subject: [PATCH 08/28] Fix Commander object count tracking Modify the class attribute instead of the instance attribute. --- algorithm_visualizer/commander.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/algorithm_visualizer/commander.py b/algorithm_visualizer/commander.py index c1e47ce..59c756a 100644 --- a/algorithm_visualizer/commander.py +++ b/algorithm_visualizer/commander.py @@ -15,7 +15,7 @@ class Commander: commands: List[Dict[str, Serializable]] = [] def __init__(self, *args: Serializable): - self._objectCount += 1 + Commander._objectCount += 1 self.key = self._keyRandomizer.create() self.command(self.__class__.__name__, *args) @@ -38,5 +38,5 @@ def command(self, method: str, *args): self._command(self.key, method, *args) def destroy(self): - self._objectCount -= 1 + Commander._objectCount -= 1 self.command("destroy") From 2c3316ffdc751a8d9298b1d9f64321f454e10e2b Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 25 Jul 2019 19:43:11 -0700 Subject: [PATCH 09/28] Add Commander tests --- tests/test_commander.py | 53 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/test_commander.py diff --git a/tests/test_commander.py b/tests/test_commander.py new file mode 100644 index 0000000..f2a1592 --- /dev/null +++ b/tests/test_commander.py @@ -0,0 +1,53 @@ +import unittest + +from algorithm_visualizer import commander +from algorithm_visualizer import Commander + + +class CommanderTests(unittest.TestCase): + def test_commander_max_commands(self): + Commander.commands = [None for _ in range(commander._MAX_COMMANDS)] + + with self.assertRaisesRegex(RuntimeError, "Too Many Commands"): + Commander() + + Commander.commands = [] + + def test_commander_max_objects(self): + Commander._objectCount = 200 + + with self.assertRaisesRegex(RuntimeError, "Too Many Objects"): + Commander() + + Commander._objectCount = 0 + + def test_commander_command(self): + cmder = Commander() + args = [["bar", "baz"], 12] + cmder.command("foo", *args) + cmd = Commander.commands[-1] + + self.assertEqual(cmd["method"], "foo") + self.assertEqual(cmd["key"], cmder.key) + self.assertEqual(cmd["args"], args) + + def test_commander_create(self): + old_count = Commander._objectCount + + args = [1, 2, 3] + cmder = Commander(*args) + cmd = Commander.commands[-1] + + self.assertEqual(old_count + 1, Commander._objectCount) + self.assertEqual(cmd["method"], cmder.__class__.__name__) + self.assertEqual(cmd["args"], args) + + def test_commander_destroy(self): + old_count = Commander._objectCount + + cmder = Commander() + cmder.destroy() + cmd = Commander.commands[-1] + + self.assertEqual(old_count, Commander._objectCount) + self.assertEqual(cmd["method"], "destroy") From b505a7d19fbcece7555adcfe4c523bb4d06ec18d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 26 Jul 2019 14:13:30 -0700 Subject: [PATCH 10/28] Minor refactor to Commander tests * Use a dictionary for command assertion instead of one assertion per kv pair * Explicitly check for the name "Commander" --- tests/test_commander.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/test_commander.py b/tests/test_commander.py index f2a1592..2397072 100644 --- a/tests/test_commander.py +++ b/tests/test_commander.py @@ -27,19 +27,23 @@ def test_commander_command(self): cmder.command("foo", *args) cmd = Commander.commands[-1] - self.assertEqual(cmd["method"], "foo") - self.assertEqual(cmd["key"], cmder.key) - self.assertEqual(cmd["args"], args) + expected_cmd = { + "key": cmder.key, + "method": "foo", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) def test_commander_create(self): old_count = Commander._objectCount args = [1, 2, 3] - cmder = Commander(*args) + Commander(*args) cmd = Commander.commands[-1] self.assertEqual(old_count + 1, Commander._objectCount) - self.assertEqual(cmd["method"], cmder.__class__.__name__) + self.assertEqual(cmd["method"], "Commander") self.assertEqual(cmd["args"], args) def test_commander_destroy(self): From c2728e5f688daa9e01e991118af67fb4d29bffdf Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 26 Jul 2019 14:13:46 -0700 Subject: [PATCH 11/28] Add Layout tests --- tests/test_layouts.py | 68 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 tests/test_layouts.py diff --git a/tests/test_layouts.py b/tests/test_layouts.py new file mode 100644 index 0000000..e72dc14 --- /dev/null +++ b/tests/test_layouts.py @@ -0,0 +1,68 @@ +import unittest + +from algorithm_visualizer import Commander +from algorithm_visualizer import Layout + + +class LayoutsTests(unittest.TestCase): + def setUp(self): + self.child = Commander() + self.layout = Layout([self.child]) + self.create_cmd = Commander.commands[-1] + + def test_layout_create(self): + expected_cmd = { + "key": self.layout.key, + "method": "Layout", + "args": [[self.child.key]], + } + + self.assertEqual(self.create_cmd, expected_cmd) + + def test_layout_setRoot(self): + self.layout.setRoot(self.child) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": None, + "method": "setRoot", + "args": [self.child.key], + } + + self.assertEqual(cmd, expected_cmd) + + def test_layout_add(self): + self.layout.add(self.child, 1) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.layout.key, + "method": "add", + "args": [self.child.key, 1], + } + + self.assertEqual(cmd, expected_cmd) + + def test_layout_remove(self): + self.layout.remove(self.child) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.layout.key, + "method": "remove", + "args": [self.child.key], + } + + self.assertEqual(cmd, expected_cmd) + + def test_layout_removeAll(self): + self.layout.removeAll() + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.layout.key, + "method": "removeAll", + "args": [], + } + + self.assertEqual(cmd, expected_cmd) From dcf95990ce54df861da04be7568dc8486014ba40 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 26 Jul 2019 14:39:59 -0700 Subject: [PATCH 12/28] Test for undefined arguments for Commander.command() --- tests/test_commander.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_commander.py b/tests/test_commander.py index 2397072..020a1b1 100644 --- a/tests/test_commander.py +++ b/tests/test_commander.py @@ -2,6 +2,7 @@ from algorithm_visualizer import commander from algorithm_visualizer import Commander +from algorithm_visualizer.types import UNDEFINED class CommanderTests(unittest.TestCase): @@ -24,7 +25,7 @@ def test_commander_max_objects(self): def test_commander_command(self): cmder = Commander() args = [["bar", "baz"], 12] - cmder.command("foo", *args) + cmder.command("foo", *args, UNDEFINED) cmd = Commander.commands[-1] expected_cmd = { From 7a43d3d90eccb66a101f64d900a685dad4f5577d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 27 Jul 2019 17:18:22 -0700 Subject: [PATCH 13/28] Add Tracer tests --- tests/tracers/__init__.py | 0 tests/tracers/test_tracer.py | 59 ++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 tests/tracers/__init__.py create mode 100644 tests/tracers/test_tracer.py diff --git a/tests/tracers/__init__.py b/tests/tracers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tracers/test_tracer.py b/tests/tracers/test_tracer.py new file mode 100644 index 0000000..7c35fb2 --- /dev/null +++ b/tests/tracers/test_tracer.py @@ -0,0 +1,59 @@ +import unittest + +from algorithm_visualizer import Commander +from algorithm_visualizer import Tracer + + +class TracerTests(unittest.TestCase): + def setUp(self): + self.tracer = Tracer() + + def test_tracer_create(self): + title = "foo" + tracer = Tracer(title) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": tracer.key, + "method": "Tracer", + "args": [title], + } + + self.assertEqual(cmd, expected_cmd) + + def test_tracer_delay(self): + delay = 5 + self.tracer.delay(delay) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": None, + "method": "delay", + "args": [delay], + } + + self.assertEqual(cmd, expected_cmd) + + def test_tracer_set(self): + self.tracer.set() + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "set", + "args": [], + } + + self.assertEqual(cmd, expected_cmd) + + def test_tracer_reset(self): + self.tracer.reset() + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "reset", + "args": [], + } + + self.assertEqual(cmd, expected_cmd) From 1c37e4b15419f154b03a095d37cf87c6b40ea3fa Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 27 Jul 2019 18:13:02 -0700 Subject: [PATCH 14/28] Refactor Commander tests * Reset object count and commands list to previous values * Use dictionaries for all assertions rather than checking single keys * Create a Commander object in setUp() for use with most tests --- tests/test_commander.py | 56 +++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/tests/test_commander.py b/tests/test_commander.py index 020a1b1..4ac9e3c 100644 --- a/tests/test_commander.py +++ b/tests/test_commander.py @@ -6,53 +6,67 @@ class CommanderTests(unittest.TestCase): + def setUp(self): + self.commander = Commander() + + def test_commander_create(self): + old_count = Commander._objectCount + + args = [1, 2, 3] + cmder = Commander(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": cmder.key, + "method": "Commander", + "args": args, + } + + self.assertEqual(old_count + 1, Commander._objectCount) + self.assertEqual(cmd, expected_cmd) + def test_commander_max_commands(self): + old_cmds = Commander.commands Commander.commands = [None for _ in range(commander._MAX_COMMANDS)] with self.assertRaisesRegex(RuntimeError, "Too Many Commands"): - Commander() + self.commander.command("foo") - Commander.commands = [] + Commander.commands = old_cmds def test_commander_max_objects(self): + old_count = Commander._objectCount Commander._objectCount = 200 with self.assertRaisesRegex(RuntimeError, "Too Many Objects"): Commander() - Commander._objectCount = 0 + Commander._objectCount = old_count def test_commander_command(self): - cmder = Commander() args = [["bar", "baz"], 12] - cmder.command("foo", *args, UNDEFINED) + self.commander.command("foo", *args, UNDEFINED) cmd = Commander.commands[-1] expected_cmd = { - "key": cmder.key, + "key": self.commander.key, "method": "foo", "args": args, } self.assertEqual(cmd, expected_cmd) - def test_commander_create(self): - old_count = Commander._objectCount - - args = [1, 2, 3] - Commander(*args) - cmd = Commander.commands[-1] - - self.assertEqual(old_count + 1, Commander._objectCount) - self.assertEqual(cmd["method"], "Commander") - self.assertEqual(cmd["args"], args) - def test_commander_destroy(self): old_count = Commander._objectCount - cmder = Commander() - cmder.destroy() + self.commander.destroy() cmd = Commander.commands[-1] - self.assertEqual(old_count, Commander._objectCount) - self.assertEqual(cmd["method"], "destroy") + expected_cmd = { + "key": self.commander.key, + "method": "destroy", + "args": [], + } + + self.assertEqual(old_count - 1, Commander._objectCount) + self.assertEqual(cmd, expected_cmd) From 818fea1ddd95c65fa6156cce7d20f9b6010cd36b Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 27 Jul 2019 18:34:21 -0700 Subject: [PATCH 15/28] Add LogTracer tests --- tests/tracers/test_log.py | 61 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/tracers/test_log.py diff --git a/tests/tracers/test_log.py b/tests/tracers/test_log.py new file mode 100644 index 0000000..15f0f79 --- /dev/null +++ b/tests/tracers/test_log.py @@ -0,0 +1,61 @@ +import unittest + +from algorithm_visualizer import Commander +from algorithm_visualizer import LogTracer + + +class LogTests(unittest.TestCase): + def setUp(self): + self.logger = LogTracer() + + def test_log_set(self): + args = [1] + self.logger.set(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.logger.key, + "method": "set", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_log_print(self): + args = ["hello"] + self.logger.print(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.logger.key, + "method": "print", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_log_println(self): + args = ["hello"] + self.logger.println(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.logger.key, + "method": "println", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_log_printf(self): + args = ["%s", "hello"] + self.logger.printf(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.logger.key, + "method": "printf", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) From 47081036763e81bc38912d7f8f14f73e8859ce96 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 27 Jul 2019 18:54:50 -0700 Subject: [PATCH 16/28] Add Array2DTracer tests --- tests/tracers/test_array2d.py | 132 ++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 tests/tracers/test_array2d.py diff --git a/tests/tracers/test_array2d.py b/tests/tracers/test_array2d.py new file mode 100644 index 0000000..d544fc4 --- /dev/null +++ b/tests/tracers/test_array2d.py @@ -0,0 +1,132 @@ +import unittest + +from algorithm_visualizer import Commander +from algorithm_visualizer import Array2DTracer + + +class Array2DTests(unittest.TestCase): + def setUp(self): + self.tracer = Array2DTracer() + + def test_array2d_set(self): + args = [ + [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ], + ] + self.tracer.set(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "set", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_array2d_patch(self): + args = [1, 2, "foo"] + self.tracer.patch(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "patch", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_array2d_depatch(self): + args = [1, 2] + self.tracer.depatch(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "depatch", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_array2d_select(self): + args = [1, 2, 3, 4] + self.tracer.select(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "select", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_array2d_selectRow(self): + args = [1, 2, 3] + self.tracer.selectRow(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "selectRow", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_array2d_selectCol(self): + args = [1, 2, 3] + self.tracer.selectCol(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "selectCol", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_array2d_deselect(self): + args = [1, 2, 3, 4] + self.tracer.deselect(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "deselect", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_array2d_deselectRow(self): + args = [1, 2, 3] + self.tracer.deselectRow(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "deselectRow", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_array2d_deselectCol(self): + args = [1, 2, 3] + self.tracer.deselectCol(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "deselectCol", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) From 845f70bdc98d33652041b573e7c032413fa54944 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 27 Jul 2019 19:03:15 -0700 Subject: [PATCH 17/28] Add Array1DTracer tests --- tests/tracers/test_array1d.py | 88 +++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tests/tracers/test_array1d.py diff --git a/tests/tracers/test_array1d.py b/tests/tracers/test_array1d.py new file mode 100644 index 0000000..f674931 --- /dev/null +++ b/tests/tracers/test_array1d.py @@ -0,0 +1,88 @@ +import unittest + +from algorithm_visualizer import Commander +from algorithm_visualizer import Array1DTracer +from algorithm_visualizer import ChartTracer + + +class Array1DTests(unittest.TestCase): + def setUp(self): + self.tracer = Array1DTracer() + + def test_array1d_set(self): + args = [[1, 2, 3]] + self.tracer.set(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "set", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_array1d_patch(self): + args = [1, "foo"] + self.tracer.patch(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "patch", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_array1d_depatch(self): + args = [1] + self.tracer.depatch(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "depatch", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_array1d_select(self): + args = [1, 2] + self.tracer.select(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "select", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_array1d_deselect(self): + args = [1, 2] + self.tracer.deselect(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "deselect", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_array1d_chart(self): + chart = ChartTracer() + self.tracer.chart(chart) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "chart", + "args": [chart.key], + } + + self.assertEqual(cmd, expected_cmd) From 6ea7a2d4d840af3db293aa4e8562b226da8dab9d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 27 Jul 2019 19:20:11 -0700 Subject: [PATCH 18/28] Return self from GraphTracer.directed() --- algorithm_visualizer/tracers/graph.py | 1 + 1 file changed, 1 insertion(+) diff --git a/algorithm_visualizer/tracers/graph.py b/algorithm_visualizer/tracers/graph.py index 522f002..318498e 100644 --- a/algorithm_visualizer/tracers/graph.py +++ b/algorithm_visualizer/tracers/graph.py @@ -8,6 +8,7 @@ def set(self, array2d: SerializableSequence[SerializableSequence[Serializable]] def directed(self, isDirected: bool = UNDEFINED): self.command("directed", isDirected) + return self def weighted(self, isWeighted: bool = UNDEFINED) -> "GraphTracer": self.command("weighted", isWeighted) From ae2502750e282493a4437dedaf1204c5cbd91a44 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 27 Jul 2019 19:40:32 -0700 Subject: [PATCH 19/28] Add GraphTracer tests --- tests/tracers/test_graph.py | 211 ++++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 tests/tracers/test_graph.py diff --git a/tests/tracers/test_graph.py b/tests/tracers/test_graph.py new file mode 100644 index 0000000..f2928f9 --- /dev/null +++ b/tests/tracers/test_graph.py @@ -0,0 +1,211 @@ +import unittest + +from algorithm_visualizer import Commander +from algorithm_visualizer import GraphTracer +from algorithm_visualizer import LogTracer + + +class GraphTests(unittest.TestCase): + def setUp(self): + self.tracer = GraphTracer() + + def test_graph_set(self): + args = [ + [ + [0, 1, 0], + [1, 0, 1], + [0, 1, 0], + ], + ] + self.tracer.set(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "set", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_graph_directed(self): + args = [True] + ret = self.tracer.directed(*args) + + self.assertIs(ret, self.tracer) + + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "directed", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_graph_weighted(self): + args = [True] + ret = self.tracer.weighted(*args) + + self.assertIs(ret, self.tracer) + + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "weighted", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_graph_layoutCircle(self): + ret = self.tracer.layoutCircle() + + self.assertIs(ret, self.tracer) + + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "layoutCircle", + "args": [], + } + + self.assertEqual(cmd, expected_cmd) + + def test_graph_layoutTree(self): + args = [True] + ret = self.tracer.layoutTree(*args) + + self.assertIs(ret, self.tracer) + + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "layoutTree", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_graph_layoutRandom(self): + ret = self.tracer.layoutRandom() + + self.assertIs(ret, self.tracer) + + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "layoutRandom", + "args": [], + } + + self.assertEqual(cmd, expected_cmd) + + def test_graph_addNode(self): + args = ["foo", 12.34, 1, 2, 3, 4] + self.tracer.addNode(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "addNode", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_graph_updateNode(self): + args = ["foo", 12.34, 1, 2, 3, 4] + self.tracer.updateNode(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "updateNode", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_graph_removeNode(self): + args = ["foo"] + self.tracer.removeNode(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "removeNode", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_graph_visit(self): + args = ["foo", "bar", 12.34] + self.tracer.visit(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "visit", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_graph_leave(self): + args = ["foo", "bar", 12.34] + self.tracer.leave(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "leave", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_graph_select(self): + args = ["foo", "bar"] + self.tracer.select(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "select", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_graph_deselect(self): + args = ["foo", "bar"] + self.tracer.deselect(*args) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "deselect", + "args": args, + } + + self.assertEqual(cmd, expected_cmd) + + def test_graph_log(self): + log = LogTracer() + self.tracer.log(log) + cmd = Commander.commands[-1] + + expected_cmd = { + "key": self.tracer.key, + "method": "log", + "args": [log.key], + } + + self.assertEqual(cmd, expected_cmd) From 060fca014939a0cf582339ed665589269df079a4 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 1 Aug 2019 22:39:27 -0700 Subject: [PATCH 20/28] Add helper function for tests It reduces code redundancy by performing the common operations of fetching the last command and constructing the command dictionary from the given arguments. --- tests/__init__.py | 22 ++++++ tests/test_commander.py | 38 ++------- tests/test_layouts.py | 54 +++---------- tests/tracers/test_array1d.py | 61 +++------------ tests/tracers/test_array2d.py | 88 +++------------------ tests/tracers/test_graph.py | 143 ++++------------------------------ tests/tracers/test_log.py | 43 ++-------- tests/tracers/test_tracer.py | 51 +++--------- 8 files changed, 97 insertions(+), 403 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..80c0069 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,22 @@ +import unittest +from typing import Optional, Union + +from algorithm_visualizer import Commander +from algorithm_visualizer.types import Serializable, Undefined + + +class CommanderTestCase(unittest.TestCase): + def assertCommandEqual( + self, + method: str, + *args: Union[Serializable, Undefined], + key: Optional[str] = None + ): + cmd = Commander.commands[-1] + expected_cmd = { + "key": key, + "method": method, + "args": list(args), + } + + self.assertEqual(expected_cmd, cmd) diff --git a/tests/test_commander.py b/tests/test_commander.py index 4ac9e3c..5eeaca4 100644 --- a/tests/test_commander.py +++ b/tests/test_commander.py @@ -1,29 +1,21 @@ -import unittest - from algorithm_visualizer import commander from algorithm_visualizer import Commander from algorithm_visualizer.types import UNDEFINED +from tests import CommanderTestCase + -class CommanderTests(unittest.TestCase): +class CommanderTests(CommanderTestCase): def setUp(self): self.commander = Commander() def test_commander_create(self): old_count = Commander._objectCount - args = [1, 2, 3] cmder = Commander(*args) - cmd = Commander.commands[-1] - - expected_cmd = { - "key": cmder.key, - "method": "Commander", - "args": args, - } self.assertEqual(old_count + 1, Commander._objectCount) - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("Commander", *args, key=cmder.key) def test_commander_max_commands(self): old_cmds = Commander.commands @@ -44,29 +36,15 @@ def test_commander_max_objects(self): Commander._objectCount = old_count def test_commander_command(self): + method = "foo" args = [["bar", "baz"], 12] - self.commander.command("foo", *args, UNDEFINED) - cmd = Commander.commands[-1] + self.commander.command(method, *args, UNDEFINED) - expected_cmd = { - "key": self.commander.key, - "method": "foo", - "args": args, - } - - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual(method, *args, key=self.commander.key) def test_commander_destroy(self): old_count = Commander._objectCount - self.commander.destroy() - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.commander.key, - "method": "destroy", - "args": [], - } self.assertEqual(old_count - 1, Commander._objectCount) - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("destroy", key=self.commander.key) diff --git a/tests/test_layouts.py b/tests/test_layouts.py index e72dc14..0f99a1a 100644 --- a/tests/test_layouts.py +++ b/tests/test_layouts.py @@ -1,68 +1,36 @@ -import unittest - from algorithm_visualizer import Commander from algorithm_visualizer import Layout +from tests import CommanderTestCase + -class LayoutsTests(unittest.TestCase): +class LayoutsTests(CommanderTestCase): def setUp(self): self.child = Commander() self.layout = Layout([self.child]) - self.create_cmd = Commander.commands[-1] def test_layout_create(self): - expected_cmd = { - "key": self.layout.key, - "method": "Layout", - "args": [[self.child.key]], - } + layout = Layout([self.child]) - self.assertEqual(self.create_cmd, expected_cmd) + self.assertCommandEqual("Layout", [self.child.key], key=layout.key) def test_layout_setRoot(self): self.layout.setRoot(self.child) - cmd = Commander.commands[-1] - - expected_cmd = { - "key": None, - "method": "setRoot", - "args": [self.child.key], - } - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("setRoot", self.child.key) def test_layout_add(self): - self.layout.add(self.child, 1) - cmd = Commander.commands[-1] + index = 1 + self.layout.add(self.child, index) - expected_cmd = { - "key": self.layout.key, - "method": "add", - "args": [self.child.key, 1], - } - - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("add", self.child.key, index, key=self.layout.key) def test_layout_remove(self): self.layout.remove(self.child) - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.layout.key, - "method": "remove", - "args": [self.child.key], - } - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("remove", self.child.key, key=self.layout.key) def test_layout_removeAll(self): self.layout.removeAll() - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.layout.key, - "method": "removeAll", - "args": [], - } - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("removeAll", key=self.layout.key) diff --git a/tests/tracers/test_array1d.py b/tests/tracers/test_array1d.py index f674931..1d69bc9 100644 --- a/tests/tracers/test_array1d.py +++ b/tests/tracers/test_array1d.py @@ -1,88 +1,45 @@ -import unittest - -from algorithm_visualizer import Commander from algorithm_visualizer import Array1DTracer from algorithm_visualizer import ChartTracer +from tests import CommanderTestCase + -class Array1DTests(unittest.TestCase): +class Array1DTests(CommanderTestCase): def setUp(self): self.tracer = Array1DTracer() def test_array1d_set(self): args = [[1, 2, 3]] self.tracer.set(*args) - cmd = Commander.commands[-1] - expected_cmd = { - "key": self.tracer.key, - "method": "set", - "args": args, - } - - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("set", *args, key=self.tracer.key) def test_array1d_patch(self): args = [1, "foo"] self.tracer.patch(*args) - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.tracer.key, - "method": "patch", - "args": args, - } - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("patch", *args, key=self.tracer.key) def test_array1d_depatch(self): args = [1] self.tracer.depatch(*args) - cmd = Commander.commands[-1] - expected_cmd = { - "key": self.tracer.key, - "method": "depatch", - "args": args, - } - - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("depatch", *args, key=self.tracer.key) def test_array1d_select(self): args = [1, 2] self.tracer.select(*args) - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.tracer.key, - "method": "select", - "args": args, - } - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("select", *args, key=self.tracer.key) def test_array1d_deselect(self): args = [1, 2] self.tracer.deselect(*args) - cmd = Commander.commands[-1] - expected_cmd = { - "key": self.tracer.key, - "method": "deselect", - "args": args, - } - - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("deselect", *args, key=self.tracer.key) def test_array1d_chart(self): chart = ChartTracer() self.tracer.chart(chart) - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.tracer.key, - "method": "chart", - "args": [chart.key], - } - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("chart", chart.key, key=self.tracer.key) diff --git a/tests/tracers/test_array2d.py b/tests/tracers/test_array2d.py index d544fc4..5e97c91 100644 --- a/tests/tracers/test_array2d.py +++ b/tests/tracers/test_array2d.py @@ -1,10 +1,9 @@ -import unittest - -from algorithm_visualizer import Commander from algorithm_visualizer import Array2DTracer +from tests import CommanderTestCase + -class Array2DTests(unittest.TestCase): +class Array2DTests(CommanderTestCase): def setUp(self): self.tracer = Array2DTracer() @@ -17,116 +16,53 @@ def test_array2d_set(self): ], ] self.tracer.set(*args) - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.tracer.key, - "method": "set", - "args": args, - } - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("set", *args, key=self.tracer.key) def test_array2d_patch(self): args = [1, 2, "foo"] self.tracer.patch(*args) - cmd = Commander.commands[-1] - expected_cmd = { - "key": self.tracer.key, - "method": "patch", - "args": args, - } - - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("patch", *args, key=self.tracer.key) def test_array2d_depatch(self): args = [1, 2] self.tracer.depatch(*args) - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.tracer.key, - "method": "depatch", - "args": args, - } - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("depatch", *args, key=self.tracer.key) def test_array2d_select(self): args = [1, 2, 3, 4] self.tracer.select(*args) - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.tracer.key, - "method": "select", - "args": args, - } - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("select", *args, key=self.tracer.key) def test_array2d_selectRow(self): args = [1, 2, 3] self.tracer.selectRow(*args) - cmd = Commander.commands[-1] - expected_cmd = { - "key": self.tracer.key, - "method": "selectRow", - "args": args, - } - - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("selectRow", *args, key=self.tracer.key) def test_array2d_selectCol(self): args = [1, 2, 3] self.tracer.selectCol(*args) - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.tracer.key, - "method": "selectCol", - "args": args, - } - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("selectCol", *args, key=self.tracer.key) def test_array2d_deselect(self): args = [1, 2, 3, 4] self.tracer.deselect(*args) - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.tracer.key, - "method": "deselect", - "args": args, - } - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("deselect", *args, key=self.tracer.key) def test_array2d_deselectRow(self): args = [1, 2, 3] self.tracer.deselectRow(*args) - cmd = Commander.commands[-1] - expected_cmd = { - "key": self.tracer.key, - "method": "deselectRow", - "args": args, - } - - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("deselectRow", *args, key=self.tracer.key) def test_array2d_deselectCol(self): args = [1, 2, 3] self.tracer.deselectCol(*args) - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.tracer.key, - "method": "deselectCol", - "args": args, - } - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("deselectCol", *args, key=self.tracer.key) diff --git a/tests/tracers/test_graph.py b/tests/tracers/test_graph.py index f2928f9..5abbf39 100644 --- a/tests/tracers/test_graph.py +++ b/tests/tracers/test_graph.py @@ -1,11 +1,10 @@ -import unittest - -from algorithm_visualizer import Commander from algorithm_visualizer import GraphTracer from algorithm_visualizer import LogTracer +from tests import CommanderTestCase + -class GraphTests(unittest.TestCase): +class GraphTests(CommanderTestCase): def setUp(self): self.tracer = GraphTracer() @@ -18,194 +17,86 @@ def test_graph_set(self): ], ] self.tracer.set(*args) - cmd = Commander.commands[-1] - expected_cmd = { - "key": self.tracer.key, - "method": "set", - "args": args, - } - - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("set", *args, key=self.tracer.key) def test_graph_directed(self): args = [True] ret = self.tracer.directed(*args) self.assertIs(ret, self.tracer) - - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.tracer.key, - "method": "directed", - "args": args, - } - - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("directed", *args, key=self.tracer.key) def test_graph_weighted(self): args = [True] ret = self.tracer.weighted(*args) self.assertIs(ret, self.tracer) - - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.tracer.key, - "method": "weighted", - "args": args, - } - - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("weighted", *args, key=self.tracer.key) def test_graph_layoutCircle(self): ret = self.tracer.layoutCircle() self.assertIs(ret, self.tracer) - - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.tracer.key, - "method": "layoutCircle", - "args": [], - } - - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("layoutCircle", key=self.tracer.key) def test_graph_layoutTree(self): args = [True] ret = self.tracer.layoutTree(*args) self.assertIs(ret, self.tracer) - - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.tracer.key, - "method": "layoutTree", - "args": args, - } - - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("layoutTree", *args, key=self.tracer.key) def test_graph_layoutRandom(self): ret = self.tracer.layoutRandom() self.assertIs(ret, self.tracer) - - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.tracer.key, - "method": "layoutRandom", - "args": [], - } - - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("layoutRandom", key=self.tracer.key) def test_graph_addNode(self): args = ["foo", 12.34, 1, 2, 3, 4] self.tracer.addNode(*args) - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.tracer.key, - "method": "addNode", - "args": args, - } - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("addNode", *args, key=self.tracer.key) def test_graph_updateNode(self): args = ["foo", 12.34, 1, 2, 3, 4] self.tracer.updateNode(*args) - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.tracer.key, - "method": "updateNode", - "args": args, - } - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("updateNode", *args, key=self.tracer.key) def test_graph_removeNode(self): args = ["foo"] self.tracer.removeNode(*args) - cmd = Commander.commands[-1] - expected_cmd = { - "key": self.tracer.key, - "method": "removeNode", - "args": args, - } - - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("removeNode", *args, key=self.tracer.key) def test_graph_visit(self): args = ["foo", "bar", 12.34] self.tracer.visit(*args) - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.tracer.key, - "method": "visit", - "args": args, - } - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("visit", *args, key=self.tracer.key) def test_graph_leave(self): args = ["foo", "bar", 12.34] self.tracer.leave(*args) - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.tracer.key, - "method": "leave", - "args": args, - } - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("leave", *args, key=self.tracer.key) def test_graph_select(self): args = ["foo", "bar"] self.tracer.select(*args) - cmd = Commander.commands[-1] - expected_cmd = { - "key": self.tracer.key, - "method": "select", - "args": args, - } - - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("select", *args, key=self.tracer.key) def test_graph_deselect(self): args = ["foo", "bar"] self.tracer.deselect(*args) - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.tracer.key, - "method": "deselect", - "args": args, - } - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("deselect", *args, key=self.tracer.key) def test_graph_log(self): log = LogTracer() self.tracer.log(log) - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.tracer.key, - "method": "log", - "args": [log.key], - } - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("log", log.key, key=self.tracer.key) diff --git a/tests/tracers/test_log.py b/tests/tracers/test_log.py index 15f0f79..8631332 100644 --- a/tests/tracers/test_log.py +++ b/tests/tracers/test_log.py @@ -1,61 +1,32 @@ -import unittest - -from algorithm_visualizer import Commander from algorithm_visualizer import LogTracer +from tests import CommanderTestCase + -class LogTests(unittest.TestCase): +class LogTests(CommanderTestCase): def setUp(self): self.logger = LogTracer() def test_log_set(self): args = [1] self.logger.set(*args) - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.logger.key, - "method": "set", - "args": args, - } - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("set", *args, key=self.logger.key) def test_log_print(self): args = ["hello"] self.logger.print(*args) - cmd = Commander.commands[-1] - expected_cmd = { - "key": self.logger.key, - "method": "print", - "args": args, - } - - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("print", *args, key=self.logger.key) def test_log_println(self): args = ["hello"] self.logger.println(*args) - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.logger.key, - "method": "println", - "args": args, - } - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("println", *args, key=self.logger.key) def test_log_printf(self): args = ["%s", "hello"] self.logger.printf(*args) - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.logger.key, - "method": "printf", - "args": args, - } - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("printf", *args, key=self.logger.key) diff --git a/tests/tracers/test_tracer.py b/tests/tracers/test_tracer.py index 7c35fb2..0ee9db7 100644 --- a/tests/tracers/test_tracer.py +++ b/tests/tracers/test_tracer.py @@ -1,59 +1,30 @@ -import unittest - -from algorithm_visualizer import Commander from algorithm_visualizer import Tracer +from tests import CommanderTestCase + -class TracerTests(unittest.TestCase): +class TracerTests(CommanderTestCase): def setUp(self): self.tracer = Tracer() def test_tracer_create(self): - title = "foo" - tracer = Tracer(title) - cmd = Commander.commands[-1] - - expected_cmd = { - "key": tracer.key, - "method": "Tracer", - "args": [title], - } + args = ["foo"] + tracer = Tracer(*args) - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("Tracer", *args, key=tracer.key) def test_tracer_delay(self): - delay = 5 - self.tracer.delay(delay) - cmd = Commander.commands[-1] + args = [5] + self.tracer.delay(*args) - expected_cmd = { - "key": None, - "method": "delay", - "args": [delay], - } - - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("delay", *args) def test_tracer_set(self): self.tracer.set() - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.tracer.key, - "method": "set", - "args": [], - } - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("set", key=self.tracer.key) def test_tracer_reset(self): self.tracer.reset() - cmd = Commander.commands[-1] - - expected_cmd = { - "key": self.tracer.key, - "method": "reset", - "args": [], - } - self.assertEqual(cmd, expected_cmd) + self.assertCommandEqual("reset", key=self.tracer.key) From 0a69697173e85feb8b5048eaea55b8b87660cffa Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 1 Aug 2019 22:44:39 -0700 Subject: [PATCH 21/28] Add missing Graph edge tests --- tests/tracers/test_graph.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/tracers/test_graph.py b/tests/tracers/test_graph.py index 5abbf39..7f1e7f6 100644 --- a/tests/tracers/test_graph.py +++ b/tests/tracers/test_graph.py @@ -71,6 +71,24 @@ def test_graph_removeNode(self): self.assertCommandEqual("removeNode", *args, key=self.tracer.key) + def test_graph_addEdge(self): + args = ["source", "target", 12.34, 1, 2] + self.tracer.addEdge(*args) + + self.assertCommandEqual("addEdge", *args, key=self.tracer.key) + + def test_graph_updateEdge(self): + args = ["source", "target", 12.34, 1, 2] + self.tracer.updateEdge(*args) + + self.assertCommandEqual("updateEdge", *args, key=self.tracer.key) + + def test_graph_removeEdge(self): + args = ["source", "target"] + self.tracer.removeEdge(*args) + + self.assertCommandEqual("removeEdge", *args, key=self.tracer.key) + def test_graph_visit(self): args = ["foo", "bar", 12.34] self.tracer.visit(*args) From 190e5490bce18080a46cfdf1729844826b4c3a35 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 1 Aug 2019 23:05:23 -0700 Subject: [PATCH 22/28] Configure coverage.py --- .coveragerc | 8 ++++++++ algorithm_visualizer/randomize.py | 2 +- requirements-dev.txt | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..c9e0e02 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,8 @@ +[run] +branch = True +source = algorithm_visualizer + +[report] +exclude_lines = + pragma: no cover + if TYPE_CHECKING: diff --git a/algorithm_visualizer/randomize.py b/algorithm_visualizer/randomize.py index e311120..cb51511 100644 --- a/algorithm_visualizer/randomize.py +++ b/algorithm_visualizer/randomize.py @@ -9,7 +9,7 @@ class _Randomizer(metaclass=abc.ABCMeta): @abc.abstractmethod def create(self) -> NoReturn: - raise NotImplementedError + raise NotImplementedError # pragma: no cover class Integer(_Randomizer): diff --git a/requirements-dev.txt b/requirements-dev.txt index 2edbdad..719e477 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,3 +5,5 @@ pip >= 19.1.1 setuptools >= 38.6.0 twine >= 1.12.0 # For twine check wheel >= 0.31.0 + +coverage ~= 4.5 From 75e641bf3f4346bbda5a15ae91b50edb6f058623 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 26 Aug 2019 21:16:16 -0700 Subject: [PATCH 23/28] Refactor execute() to make code more testable * Add type alias equivalent to os.PathLike to support Python 3.5 --- algorithm_visualizer/__init__.py | 45 ++++++++++++++++++++------------ algorithm_visualizer/types.py | 2 ++ 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/algorithm_visualizer/__init__.py b/algorithm_visualizer/__init__.py index cbe3aa7..a959fe8 100644 --- a/algorithm_visualizer/__init__.py +++ b/algorithm_visualizer/__init__.py @@ -1,11 +1,14 @@ import atexit import json import os +import webbrowser +from pathlib import Path from . import randomize as Randomize from .commander import Commander from .layouts import * from .tracers import * +from .types import PathLike __all__ = ( "Randomize", "Commander", @@ -14,23 +17,33 @@ ) +def create_json_file(path: PathLike = "./visualization.json"): + commands = json.dumps(Commander.commands, separators=(",", ":")) + with Path(path).open("w", encoding="UTF-8") as file: + file.write(commands) + + +def get_url() -> str: + import requests + + commands = json.dumps(Commander.commands, separators=(",", ":")) + response = requests.post( + "https://algorithm-visualizer.org/api/visualizations", + headers={"Content-type": "application/json"}, + data=commands + ) + + if response.status_code == 200: + return response.text + else: + raise requests.HTTPError(response=response) + + @atexit.register def execute(): - commands = json.dumps(Commander.commands, separators=(",", ":")) if os.getenv("ALGORITHM_VISUALIZER"): - with open("visualization.json", "w", encoding="UTF-8") as file: - file.write(commands) + create_json_file() else: - import requests - import webbrowser - - response = requests.post( - "https://algorithm-visualizer.org/api/visualizations", - headers={"Content-type": "application/json"}, - data=commands - ) - - if response.status_code == 200: - webbrowser.open(response.text) - else: - raise requests.HTTPError(response=response) + url = get_url() + webbrowser.open(url) + diff --git a/algorithm_visualizer/types.py b/algorithm_visualizer/types.py index 5fcbc26..88a8a8d 100644 --- a/algorithm_visualizer/types.py +++ b/algorithm_visualizer/types.py @@ -1,6 +1,8 @@ +from pathlib import PurePath from typing import Any, Dict, List, Tuple, TypeVar, Union +PathLike = Union[str, bytes, PurePath] Number = Union[int, float] # Types which are serializable by the default JSONEncoder From 9a9168dab60b225bd8d758375cec740b99a1f1a6 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 26 Aug 2019 22:27:43 -0700 Subject: [PATCH 24/28] Add a test for create_json_file --- tests/test_execute.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/test_execute.py diff --git a/tests/test_execute.py b/tests/test_execute.py new file mode 100644 index 0000000..917973f --- /dev/null +++ b/tests/test_execute.py @@ -0,0 +1,31 @@ +import json +import unittest +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest import mock + +import algorithm_visualizer + + +class TestExecute(unittest.TestCase): + @mock.patch.object(algorithm_visualizer.Commander, "commands", new_callable=mock.PropertyMock) + def test_create_json_file(self, cmd_mock): + expected_cmds = [ + { + "key": "abc123", + "method": "foo.bar", + "args": [1, 2, 3], + } + ] + cmd_mock.return_value = expected_cmds + + with TemporaryDirectory() as temp_dir: + path = Path(temp_dir, "visualization.json") + algorithm_visualizer.create_json_file(path) + self.assertTrue(path.is_file()) + + with path.open("r", encoding="UTF-8") as f: + actual_cmds = json.load(f) + + self.assertEqual(expected_cmds, actual_cmds) + From d7fdacbfc72128566558bc92910ccdc3da94809b Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 27 Aug 2019 10:45:36 -0700 Subject: [PATCH 25/28] Replace requests with urllib --- algorithm_visualizer/__init__.py | 31 +++++++++++++++++++++---------- setup.py | 5 +---- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/algorithm_visualizer/__init__.py b/algorithm_visualizer/__init__.py index a959fe8..d18ca23 100644 --- a/algorithm_visualizer/__init__.py +++ b/algorithm_visualizer/__init__.py @@ -3,6 +3,7 @@ import os import webbrowser from pathlib import Path +from urllib.request import HTTPError, Request, urlopen from . import randomize as Randomize from .commander import Commander @@ -24,19 +25,29 @@ def create_json_file(path: PathLike = "./visualization.json"): def get_url() -> str: - import requests - - commands = json.dumps(Commander.commands, separators=(",", ":")) - response = requests.post( - "https://algorithm-visualizer.org/api/visualizations", - headers={"Content-type": "application/json"}, - data=commands + url = "https://algorithm-visualizer.org/api/visualizations" + commands = json.dumps(Commander.commands, separators=(",", ":")).encode('utf-8') + request = Request( + url, + method="POST", + data=commands, + headers={ + "Content-type": "application/json; charset=utf-8", + "Content-Length": len(commands) + } ) + response = urlopen(request) - if response.status_code == 200: - return response.text + if response.status == 200: + return response.read().decode('utf-8') else: - raise requests.HTTPError(response=response) + raise HTTPError( + url=url, + code=response.status, + msg="Failed to retrieve the scratch URL: non-200 response", + hdrs=dict(response.info()), + fp=None + ) @atexit.register diff --git a/setup.py b/setup.py index e8166e4..086071f 100644 --- a/setup.py +++ b/setup.py @@ -46,8 +46,5 @@ }, packages=setuptools.find_packages(), - python_requires=">=3.5", - extras_require={ - "requests": ["requests >= 2.22.0"] - } + python_requires=">=3.5" ) From 91723d7899b1848bd03a37a25aa1680f777f9c9c Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 27 Aug 2019 10:49:31 -0700 Subject: [PATCH 26/28] Add get_url tests --- tests/test_execute.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_execute.py b/tests/test_execute.py index 917973f..5a22043 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -3,6 +3,7 @@ from pathlib import Path from tempfile import TemporaryDirectory from unittest import mock +from urllib.error import HTTPError import algorithm_visualizer @@ -29,3 +30,17 @@ def test_create_json_file(self, cmd_mock): self.assertEqual(expected_cmds, actual_cmds) + def test_get_url(self): + url = algorithm_visualizer.get_url() + self.assertIsInstance(url, str) + + @mock.patch.object(algorithm_visualizer, "urlopen") + def test_get_url_non_200(self, mock_urlopen): + response = mock.Mock() + type(response).status = mock.PropertyMock(return_value=201) + response.info = mock.Mock(return_value={}) + + mock_urlopen.return_value = response + + self.assertRaises(HTTPError, algorithm_visualizer.get_url) + From cb30d80c255c015fe4b167714ec8450d6af49d00 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 27 Aug 2019 10:55:31 -0700 Subject: [PATCH 27/28] Mock urlopen for get_url test --- tests/test_execute.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/test_execute.py b/tests/test_execute.py index 5a22043..481ade4 100644 --- a/tests/test_execute.py +++ b/tests/test_execute.py @@ -30,9 +30,18 @@ def test_create_json_file(self, cmd_mock): self.assertEqual(expected_cmds, actual_cmds) - def test_get_url(self): + @mock.patch.object(algorithm_visualizer, "urlopen") + def test_get_url(self, mock_urlopen): + expected_url = "https://foo.bar" + + response = mock.Mock() + type(response).status = mock.PropertyMock(return_value=200) + response.read = mock.Mock(return_value=expected_url.encode("utf-8")) + + mock_urlopen.return_value = response + url = algorithm_visualizer.get_url() - self.assertIsInstance(url, str) + self.assertEqual(url, expected_url) @mock.patch.object(algorithm_visualizer, "urlopen") def test_get_url_non_200(self, mock_urlopen): From 34724a0445d788df7db7bea14721c71a2c033ae4 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 27 Aug 2019 10:59:23 -0700 Subject: [PATCH 28/28] Exclude atexit from coverage --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index c9e0e02..96fd5c0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,3 +6,4 @@ source = algorithm_visualizer exclude_lines = pragma: no cover if TYPE_CHECKING: + @atexit