mapFolding 0.6.0__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. mapFolding/__init__.py +6 -104
  2. mapFolding/basecamp.py +12 -8
  3. mapFolding/beDRY.py +96 -286
  4. mapFolding/filesystem.py +87 -0
  5. mapFolding/noHomeYet.py +20 -0
  6. mapFolding/oeis.py +46 -39
  7. mapFolding/reference/flattened.py +377 -0
  8. mapFolding/reference/hunterNumba.py +132 -0
  9. mapFolding/reference/irvineJavaPort.py +120 -0
  10. mapFolding/reference/jax.py +208 -0
  11. mapFolding/reference/lunnan.py +153 -0
  12. mapFolding/reference/lunnanNumpy.py +123 -0
  13. mapFolding/reference/lunnanWhile.py +121 -0
  14. mapFolding/reference/rotatedEntryPoint.py +240 -0
  15. mapFolding/reference/total_countPlus1vsPlusN.py +211 -0
  16. mapFolding/someAssemblyRequired/Z0Z_workbench.py +34 -0
  17. mapFolding/someAssemblyRequired/__init__.py +16 -0
  18. mapFolding/someAssemblyRequired/getLLVMforNoReason.py +21 -0
  19. mapFolding/someAssemblyRequired/ingredientsNumba.py +100 -0
  20. mapFolding/someAssemblyRequired/synthesizeCountingFunctions.py +7 -0
  21. mapFolding/someAssemblyRequired/synthesizeDataConverters.py +135 -0
  22. mapFolding/someAssemblyRequired/synthesizeNumba.py +91 -0
  23. mapFolding/someAssemblyRequired/synthesizeNumbaJob.py +417 -0
  24. mapFolding/someAssemblyRequired/synthesizeNumbaModules.py +91 -0
  25. mapFolding/someAssemblyRequired/transformationTools.py +425 -0
  26. mapFolding/someAssemblyRequired/whatWillBe.py +311 -0
  27. mapFolding/syntheticModules/__init__.py +0 -0
  28. mapFolding/syntheticModules/dataNamespaceFlattened.py +30 -0
  29. mapFolding/syntheticModules/numbaCount.py +90 -0
  30. mapFolding/syntheticModules/numbaCountExample.py +158 -0
  31. mapFolding/syntheticModules/numbaCountSequential.py +110 -0
  32. mapFolding/syntheticModules/numbaCount_doTheNeedful.py +13 -0
  33. mapFolding/syntheticModules/numba_doTheNeedful.py +12 -0
  34. mapFolding/syntheticModules/numba_doTheNeedfulExample.py +13 -0
  35. mapFolding/theDao.py +203 -227
  36. mapFolding/theSSOT.py +255 -102
  37. {mapfolding-0.6.0.dist-info → mapfolding-0.7.0.dist-info}/METADATA +7 -6
  38. mapfolding-0.7.0.dist-info/RECORD +50 -0
  39. {mapfolding-0.6.0.dist-info → mapfolding-0.7.0.dist-info}/WHEEL +1 -1
  40. {mapfolding-0.6.0.dist-info → mapfolding-0.7.0.dist-info}/top_level.txt +1 -0
  41. tests/__init__.py +0 -0
  42. tests/conftest.py +278 -0
  43. tests/test_computations.py +49 -0
  44. tests/test_filesystem.py +52 -0
  45. tests/test_oeis.py +128 -0
  46. tests/test_other.py +84 -0
  47. tests/test_tasks.py +50 -0
  48. mapFolding/theConfiguration.py +0 -58
  49. mapFolding/theSSOTdatatypes.py +0 -155
  50. mapFolding/theWrongWay.py +0 -7
  51. mapfolding-0.6.0.dist-info/RECORD +0 -16
  52. {mapfolding-0.6.0.dist-info → mapfolding-0.7.0.dist-info}/LICENSE +0 -0
  53. {mapfolding-0.6.0.dist-info → mapfolding-0.7.0.dist-info}/entry_points.txt +0 -0
tests/conftest.py ADDED
@@ -0,0 +1,278 @@
1
+ from collections.abc import Callable, Generator, Sequence
2
+ from mapFolding.theSSOT import getAlgorithmDispatcher, getSourceAlgorithm, getPackageDispatcher, theModuleOfSyntheticModules, FREAKOUT
3
+ from mapFolding.beDRY import getLeavesTotal, validateListDimensions, makeDataContainer
4
+ from mapFolding.oeis import oeisIDsImplemented, settingsOEIS
5
+ from pathlib import Path
6
+ from typing import Any, ContextManager
7
+ import importlib.util
8
+ import pytest
9
+ import random
10
+ import shutil
11
+ import unittest.mock
12
+ import uuid
13
+ # TODO learn how to run tests and coverage analysis without `env = ["NUMBA_DISABLE_JIT=1"]`
14
+
15
+ # SSOT for test data paths and filenames
16
+ pathDataSamples = Path("tests/dataSamples")
17
+ # NOTE `tmp` is not a diminutive form of temporary: it signals a technical term. And "temp" is strongly disfavored.
18
+ pathTmpRoot: Path = pathDataSamples / "tmp"
19
+
20
+ # The registrar maintains the register of temp files
21
+ registerOfTemporaryFilesystemObjects: set[Path] = set()
22
+
23
+ def registrarRecordsTmpObject(path: Path) -> None:
24
+ """The registrar adds a tmp file to the register."""
25
+ registerOfTemporaryFilesystemObjects.add(path)
26
+
27
+ def registrarDeletesTmpObjects() -> None:
28
+ """The registrar cleans up tmp files in the register."""
29
+ for pathTmp in sorted(registerOfTemporaryFilesystemObjects, reverse=True):
30
+ try:
31
+ if pathTmp.is_file():
32
+ pathTmp.unlink(missing_ok=True)
33
+ elif pathTmp.is_dir():
34
+ shutil.rmtree(pathTmp, ignore_errors=True)
35
+ except Exception as ERRORmessage:
36
+ print(f"Warning: Failed to clean up {pathTmp}: {ERRORmessage}")
37
+ registerOfTemporaryFilesystemObjects.clear()
38
+
39
+ @pytest.fixture(scope="session", autouse=True)
40
+ def setupTeardownTmpObjects() -> Generator[None, None, None]:
41
+ """Auto-fixture to setup test data directories and cleanup after."""
42
+ pathDataSamples.mkdir(exist_ok=True)
43
+ pathTmpRoot.mkdir(exist_ok=True)
44
+ yield
45
+ registrarDeletesTmpObjects()
46
+
47
+ @pytest.fixture
48
+ def pathTmpTesting(request: pytest.FixtureRequest) -> Path:
49
+ # "Z0Z_" ensures the directory name does not start with a number, which would make it an invalid Python identifier
50
+ pathTmp = pathTmpRoot / ("Z0Z_" + str(uuid.uuid4().hex))
51
+ pathTmp.mkdir(parents=True, exist_ok=False)
52
+
53
+ registrarRecordsTmpObject(pathTmp)
54
+ return pathTmp
55
+
56
+ @pytest.fixture
57
+ def pathFilenameTmpTesting(request: pytest.FixtureRequest) -> Path:
58
+ try:
59
+ extension = request.param
60
+ except AttributeError:
61
+ extension = ".txt"
62
+
63
+ # "Z0Z_" ensures the name does not start with a number, which would make it an invalid Python identifier
64
+ uuidHex = uuid.uuid4().hex
65
+ subpath = "Z0Z_" + uuidHex[0:-8]
66
+ filenameStem = "Z0Z_" + uuidHex[-8:None]
67
+
68
+ pathFilenameTmp = Path(pathTmpRoot, subpath, filenameStem + extension)
69
+ pathFilenameTmp.parent.mkdir(parents=True, exist_ok=False)
70
+
71
+ registrarRecordsTmpObject(pathFilenameTmp)
72
+ return pathFilenameTmp
73
+
74
+ @pytest.fixture
75
+ def pathCacheTesting(pathTmpTesting: Path) -> Generator[Path, Any, None]:
76
+ """Temporarily replace the OEIS cache directory with a test directory."""
77
+ import mapFolding.oeis as oeis
78
+ pathCacheOriginal = oeis.pathCache
79
+ oeis.pathCache = pathTmpTesting
80
+ yield pathTmpTesting
81
+ oeis.pathCache = pathCacheOriginal
82
+
83
+ @pytest.fixture
84
+ def pathFilenameFoldsTotalTesting(pathTmpTesting: Path) -> Path:
85
+ return pathTmpTesting.joinpath("foldsTotalTest.txt")
86
+
87
+ """
88
+ Section: Fixtures"""
89
+
90
+ @pytest.fixture(autouse=True)
91
+ def setupWarningsAsErrors() -> Generator[None, Any, None]:
92
+ """Convert all warnings to errors for all tests."""
93
+ import warnings
94
+ warnings.filterwarnings("error")
95
+ yield
96
+ warnings.resetwarnings()
97
+
98
+ @pytest.fixture
99
+ def listDimensionsTestCountFolds(oeisID: str):
100
+ """For each `oeisID` from the `pytest.fixture`, returns `listDimensions` from `valuesTestValidation`
101
+ if `validateListDimensions` approves. Each `listDimensions` is suitable for testing counts."""
102
+ while True:
103
+ n = random.choice(settingsOEIS[oeisID]['valuesTestValidation'])
104
+ if n < 2:
105
+ continue
106
+ listDimensionsCandidate = list(settingsOEIS[oeisID]['getMapShape'](n))
107
+
108
+ try:
109
+ return validateListDimensions(listDimensionsCandidate)
110
+ except (ValueError, NotImplementedError):
111
+ pass
112
+
113
+ @pytest.fixture
114
+ def mapShapeTestFunctionality(oeisID_1random: str):
115
+ """To test functionality, get one `listDimensions` from `valuesTestValidation` if
116
+ `validateListDimensions` approves. The algorithm can count the folds of the returned
117
+ `listDimensions` in a short enough time suitable for testing."""
118
+ while True:
119
+ n = random.choice(settingsOEIS[oeisID_1random]['valuesTestValidation'])
120
+ if n < 2:
121
+ continue
122
+ listDimensionsCandidate = list(settingsOEIS[oeisID_1random]['getMapShape'](n))
123
+
124
+ try:
125
+ return validateListDimensions(listDimensionsCandidate)
126
+ except (ValueError, NotImplementedError):
127
+ pass
128
+
129
+ @pytest.fixture
130
+ def listDimensionsTestParallelization(oeisID: str) -> list[int]:
131
+ """For each `oeisID` from the `pytest.fixture`, returns `listDimensions` from `valuesTestParallelization`"""
132
+ n = random.choice(settingsOEIS[oeisID]['valuesTestParallelization'])
133
+ return list(settingsOEIS[oeisID]['getMapShape'](n))
134
+
135
+ @pytest.fixture
136
+ def mockBenchmarkTimer() -> Generator[unittest.mock.MagicMock | unittest.mock.AsyncMock, Any, None]:
137
+ """Mock time.perf_counter_ns for consistent benchmark timing."""
138
+ with unittest.mock.patch('time.perf_counter_ns') as mockTimer:
139
+ mockTimer.side_effect = [0, 1e9] # Start and end times for 1 second
140
+ yield mockTimer
141
+
142
+ @pytest.fixture
143
+ def mockFoldingFunction() -> Callable[..., Callable[..., None]]:
144
+ """Creates a mock function that simulates _countFolds behavior."""
145
+ def make_mock(foldsValue: int, listDimensions: list[int]) -> Callable[..., None]:
146
+ mock_array = makeDataContainer(2)
147
+ mock_array[0] = foldsValue
148
+ mapShape = validateListDimensions(listDimensions)
149
+ mock_array[-1] = getLeavesTotal(mapShape)
150
+
151
+ def mock_countFolds(**keywordArguments: Any) -> None:
152
+ keywordArguments['foldGroups'][:] = mock_array
153
+ return None
154
+
155
+ return mock_countFolds
156
+ return make_mock
157
+
158
+ @pytest.fixture
159
+ def mockDispatcher() -> Callable[[Any], ContextManager[Any]]:
160
+ """Context manager for mocking dispatcher callable."""
161
+ def wrapper(mockFunction: Any) -> ContextManager[Any]:
162
+ dispatcherCallable = getPackageDispatcher()
163
+ return unittest.mock.patch(
164
+ f"{dispatcherCallable.__module__}.{dispatcherCallable.__name__}",
165
+ side_effect=mockFunction
166
+ )
167
+ return wrapper
168
+
169
+ @pytest.fixture(params=oeisIDsImplemented)
170
+ def oeisID(request: pytest.FixtureRequest) -> Any:
171
+ return request.param
172
+
173
+ @pytest.fixture
174
+ def oeisID_1random() -> str:
175
+ """Return one random valid OEIS ID."""
176
+ return random.choice(oeisIDsImplemented)
177
+
178
+ @pytest.fixture
179
+ def useThisDispatcher() -> Generator[Callable[..., None], Any, None]:
180
+ """A fixture providing a context manager for temporarily replacing the dispatcher.
181
+
182
+ Returns
183
+ A context manager for patching the dispatcher
184
+ """
185
+ import mapFolding.basecamp as basecamp
186
+ dispatcherOriginal = basecamp.getPackageDispatcher
187
+
188
+ def patchDispatcher(callableTarget: Callable[..., Any]) -> None:
189
+ def callableParameterized(*arguments: Any, **keywordArguments: Any) -> Callable[..., Any]:
190
+ return callableTarget
191
+ basecamp.getPackageDispatcher = callableParameterized
192
+
193
+ yield patchDispatcher
194
+ basecamp.getPackageDispatcher = dispatcherOriginal
195
+
196
+ @pytest.fixture
197
+ def useAlgorithmSourceDispatcher(useThisDispatcher: Callable[..., Any]) -> Generator[None, None, None]:
198
+ """Temporarily patches getDispatcherCallable to return the algorithm dispatcher."""
199
+ useThisDispatcher(getAlgorithmDispatcher())
200
+ yield
201
+
202
+ @pytest.fixture
203
+ def syntheticDispatcherFixture(useThisDispatcher: Callable[..., Any]) -> Callable[..., Any]:
204
+ listCallablesInline = listNumbaCallableDispatchees
205
+ callableDispatcher = True
206
+ algorithmSource = getSourceAlgorithm()
207
+ relativePathWrite = theModuleOfSyntheticModules
208
+ filenameModuleWrite = 'pytestCount.py'
209
+ formatFilenameWrite = "pytest_{callableTarget}.py"
210
+ listSynthesizedModules: list[YouOughtaKnow] = makeFlowNumbaOptimized(listCallablesInline, callableDispatcher, algorithmSource, relativePathWrite, filenameModuleWrite, formatFilenameWrite)
211
+ dispatcherSynthetic: YouOughtaKnow | None = None
212
+ for stuff in listSynthesizedModules:
213
+ registrarRecordsTmpObject(stuff.pathFilenameForMe)
214
+ if stuff.callableSynthesized not in listCallablesInline:
215
+ dispatcherSynthetic = stuff
216
+
217
+ if dispatcherSynthetic is None:
218
+ raise FREAKOUT
219
+
220
+ dispatcherSpec = importlib.util.spec_from_file_location(dispatcherSynthetic.callableSynthesized, dispatcherSynthetic.pathFilenameForMe)
221
+ if dispatcherSpec is None:
222
+ raise ImportError(f"{dispatcherSynthetic.pathFilenameForMe=}")
223
+ if dispatcherSpec.loader is None:
224
+ raise ImportError(f"Failed to get loader for module {dispatcherSynthetic.pathFilenameForMe}")
225
+
226
+ dispatcherModule = importlib.util.module_from_spec(dispatcherSpec)
227
+ dispatcherSpec.loader.exec_module(dispatcherModule)
228
+ callableDispatcherSynthetic = getattr(dispatcherModule, dispatcherSynthetic.callableSynthesized)
229
+
230
+ useThisDispatcher(callableDispatcherSynthetic)
231
+ return callableDispatcherSynthetic
232
+
233
+ def uniformTestMessage(expected: Any, actual: Any, functionName: str, *arguments: Any) -> str:
234
+ """Format assertion message for any test comparison."""
235
+ return (f"\nTesting: `{functionName}({', '.join(str(parameter) for parameter in arguments)})`\n"
236
+ f"Expected: {expected}\n"
237
+ f"Got: {actual}")
238
+
239
+ def standardizedEqualToCallableReturn(expected: Any, functionTarget: Callable[..., Any], *arguments: Any) -> None:
240
+ """Use with callables that produce a return or an error."""
241
+ if type(expected) is type[Exception]:
242
+ messageExpected = expected.__name__
243
+ else:
244
+ messageExpected = expected
245
+
246
+ try:
247
+ messageActual = actual = functionTarget(*arguments)
248
+ except Exception as actualError:
249
+ messageActual = type(actualError).__name__
250
+ actual = type(actualError)
251
+
252
+ assert actual == expected, uniformTestMessage(messageExpected, messageActual, functionTarget.__name__, *arguments)
253
+
254
+ def standardizedSystemExit(expected: str | int | Sequence[int], functionTarget: Callable[..., Any], *arguments: Any) -> None:
255
+ """Template for tests expecting SystemExit.
256
+
257
+ Parameters
258
+ expected: Exit code expectation:
259
+ - "error": any non-zero exit code
260
+ - "nonError": specifically zero exit code
261
+ - int: exact exit code match
262
+ - Sequence[int]: exit code must be one of these values
263
+ functionTarget: The function to test
264
+ arguments: Arguments to pass to the function
265
+ """
266
+ with pytest.raises(SystemExit) as exitInfo:
267
+ functionTarget(*arguments)
268
+
269
+ exitCode = exitInfo.value.code
270
+
271
+ if expected == "error":
272
+ assert exitCode != 0, f"Expected error exit (non-zero) but got code {exitCode}"
273
+ elif expected == "nonError":
274
+ assert exitCode == 0, f"Expected non-error exit (0) but got code {exitCode}"
275
+ elif isinstance(expected, (list, tuple)):
276
+ assert exitCode in expected, f"Expected exit code to be one of {expected} but got {exitCode}"
277
+ else:
278
+ assert exitCode == expected, f"Expected exit code {expected} but got {exitCode}"
@@ -0,0 +1,49 @@
1
+ from mapFolding.basecamp import countFolds
2
+ from mapFolding.filesystem import getPathFilenameFoldsTotal
3
+ from mapFolding.noHomeYet import getFoldsTotalKnown
4
+ from mapFolding.oeis import settingsOEIS, oeisIDfor_n
5
+ # from mapFolding.someAssemblyRequired import writeJobNumba
6
+ from tests.conftest import standardizedEqualToCallableReturn, registrarRecordsTmpObject
7
+ import importlib.util
8
+ import pytest
9
+ from pathlib import Path
10
+ from types import ModuleType
11
+
12
+ def test_algorithmSourceParallel(listDimensionsTestParallelization, useAlgorithmSourceDispatcher: None) -> None:
13
+ standardizedEqualToCallableReturn(getFoldsTotalKnown(tuple(listDimensionsTestParallelization)), countFolds, listDimensionsTestParallelization, None, 'maximum')
14
+
15
+ def test_algorithmSourceSequential(listDimensionsTestCountFolds, useAlgorithmSourceDispatcher: None) -> None:
16
+ standardizedEqualToCallableReturn(getFoldsTotalKnown(tuple(listDimensionsTestCountFolds)), countFolds, listDimensionsTestCountFolds)
17
+
18
+ def test_aOFn_calculate_value(oeisID: str) -> None:
19
+ for n in settingsOEIS[oeisID]['valuesTestValidation']:
20
+ standardizedEqualToCallableReturn(settingsOEIS[oeisID]['valuesKnown'][n], oeisIDfor_n, oeisID, n)
21
+
22
+ # @pytest.mark.parametrize('pathFilenameTmpTesting', ['.py'], indirect=True)
23
+ # def test_writeJobNumba(listDimensionsTestCountFolds: list[int], pathFilenameTmpTesting: Path) -> None:
24
+ # from mapFolding.syntheticModules import numbaCount
25
+ # algorithmSourceHARDCODED: ModuleType = numbaCount
26
+ # algorithmSource = algorithmSourceHARDCODED
27
+ # callableTargetHARDCODED = 'countSequential'
28
+ # callableTarget = callableTargetHARDCODED
29
+ # pathFilenameModule = writeJobNumba(listDimensionsTestCountFolds, algorithmSource, callableTarget, pathFilenameWriteJob=pathFilenameTmpTesting.absolute())
30
+
31
+ # Don_Lapre_Road_to_Self_Improvement = importlib.util.spec_from_file_location("__main__", pathFilenameModule)
32
+ # if Don_Lapre_Road_to_Self_Improvement is None:
33
+ # raise ImportError(f"Failed to create module specification from {pathFilenameModule}")
34
+ # if Don_Lapre_Road_to_Self_Improvement.loader is None:
35
+ # raise ImportError(f"Failed to get loader for module {pathFilenameModule}")
36
+ # module = importlib.util.module_from_spec(Don_Lapre_Road_to_Self_Improvement)
37
+
38
+ # module.__name__ = "__main__"
39
+ # Don_Lapre_Road_to_Self_Improvement.loader.exec_module(module)
40
+
41
+ # pathFilenameFoldsTotal = getPathFilenameFoldsTotal(listDimensionsTestCountFolds)
42
+ # registrarRecordsTmpObject(pathFilenameFoldsTotal)
43
+ # standardizedEqualTo(str(foldsTotalKnown[tuple(listDimensionsTestCountFolds)]), pathFilenameFoldsTotal.read_text().strip)
44
+
45
+ # def test_syntheticParallel(syntheticDispatcherFixture: None, listDimensionsTestParallelization: list[int], foldsTotalKnown: dict[tuple[int, ...], int]):
46
+ # standardizedEqualTo(foldsTotalKnown[tuple(listDimensionsTestParallelization)], countFolds, listDimensionsTestParallelization, None, 'maximum')
47
+
48
+ # def test_syntheticSequential(syntheticDispatcherFixture: None, listDimensionsTestCountFolds: list[int], foldsTotalKnown: dict[tuple[int, ...], int]):
49
+ # standardizedEqualTo(foldsTotalKnown[tuple(listDimensionsTestCountFolds)], countFolds, listDimensionsTestCountFolds)
@@ -0,0 +1,52 @@
1
+ from contextlib import redirect_stdout
2
+ from mapFolding.filesystem import getFilenameFoldsTotal, getPathFilenameFoldsTotal, saveFoldsTotal
3
+ from mapFolding.beDRY import validateListDimensions
4
+ from mapFolding.theSSOT import getPathJobRootDEFAULT
5
+ from pathlib import Path
6
+ from typing import Any
7
+ import io
8
+ import numpy
9
+ import pytest
10
+ import unittest.mock
11
+
12
+ def test_saveFoldsTotal_fallback(pathTmpTesting: Path) -> None:
13
+ foldsTotal = 123
14
+ pathFilename = pathTmpTesting / "foldsTotal.txt"
15
+ with unittest.mock.patch("pathlib.Path.write_text", side_effect=OSError("Simulated write failure")):
16
+ with unittest.mock.patch("os.getcwd", return_value=str(pathTmpTesting)):
17
+ capturedOutput = io.StringIO()
18
+ with redirect_stdout(capturedOutput):
19
+ saveFoldsTotal(pathFilename, foldsTotal)
20
+ fallbackFiles = list(pathTmpTesting.glob("foldsTotalYO_*.txt"))
21
+ assert len(fallbackFiles) == 1, "Fallback file was not created upon write failure."
22
+
23
+ @pytest.mark.parametrize("listDimensions, expectedFilename", [
24
+ ([11, 13], "p11x13.foldsTotal"),
25
+ ([17, 13, 11], "p11x13x17.foldsTotal"),
26
+ ])
27
+ def test_getFilenameFoldsTotal(listDimensions: list[int], expectedFilename: str) -> None:
28
+ """Test that getFilenameFoldsTotal generates correct filenames with dimensions sorted."""
29
+ mapShape = validateListDimensions(listDimensions)
30
+ filenameActual = getFilenameFoldsTotal(mapShape)
31
+ assert filenameActual == expectedFilename, f"Expected filename {expectedFilename} but got {filenameActual}"
32
+
33
+ def test_getPathFilenameFoldsTotal_defaultPath(mapShapeTestFunctionality: tuple[int, ...]) -> None:
34
+ """Test getPathFilenameFoldsTotal with default path."""
35
+ pathFilenameFoldsTotal = getPathFilenameFoldsTotal(mapShapeTestFunctionality)
36
+ assert pathFilenameFoldsTotal.is_absolute(), "Path should be absolute"
37
+ assert pathFilenameFoldsTotal.name == getFilenameFoldsTotal(mapShapeTestFunctionality), "Filename should match getFilenameFoldsTotal output"
38
+ assert pathFilenameFoldsTotal.parent == getPathJobRootDEFAULT(), "Parent directory should match default job root"
39
+
40
+ def test_getPathFilenameFoldsTotal_relativeFilename(mapShapeTestFunctionality: tuple[int, ...]) -> None:
41
+ """Test getPathFilenameFoldsTotal with relative filename."""
42
+ relativeFilename = Path("custom/path/test.foldsTotal")
43
+ pathFilenameFoldsTotal = getPathFilenameFoldsTotal(mapShapeTestFunctionality, relativeFilename)
44
+ assert pathFilenameFoldsTotal.is_absolute(), "Path should be absolute"
45
+ assert pathFilenameFoldsTotal == getPathJobRootDEFAULT() / relativeFilename, "Relative path should be appended to default job root"
46
+
47
+ def test_getPathFilenameFoldsTotal_createsDirs(pathTmpTesting: Path, mapShapeTestFunctionality: tuple[int, ...]) -> None:
48
+ """Test that getPathFilenameFoldsTotal creates necessary directories."""
49
+ nestedPath = pathTmpTesting / "deep/nested/structure"
50
+ pathFilenameFoldsTotal = getPathFilenameFoldsTotal(mapShapeTestFunctionality, nestedPath)
51
+ assert pathFilenameFoldsTotal.parent.exists(), "Parent directories should be created"
52
+ assert pathFilenameFoldsTotal.parent.is_dir(), "Created path should be a directory"
tests/test_oeis.py ADDED
@@ -0,0 +1,128 @@
1
+ from contextlib import redirect_stdout
2
+ from mapFolding.oeis import oeisIDfor_n, getOEISids, clearOEIScache, getOEISidValues, OEIS_for_n, oeisIDsImplemented, settingsOEIS, validateOEISid
3
+ from pathlib import Path
4
+ from tests.conftest import standardizedEqualToCallableReturn, standardizedSystemExit
5
+ from typing import Any, NoReturn
6
+ from urllib.error import URLError
7
+ import io
8
+ import pytest
9
+ import random
10
+ import re as regex
11
+ import unittest.mock
12
+ import urllib.request
13
+
14
+ @pytest.mark.parametrize("badID", ["A999999", " A999999 ", "A999999extra"])
15
+ def test__validateOEISid_invalid_id(badID: str) -> None:
16
+ standardizedEqualToCallableReturn(KeyError, validateOEISid, badID)
17
+
18
+ def test__validateOEISid_partially_valid(oeisID_1random: str) -> None:
19
+ standardizedEqualToCallableReturn(KeyError, validateOEISid, f"{oeisID_1random}extra")
20
+
21
+ def test__validateOEISid_valid_id(oeisID: str) -> None:
22
+ standardizedEqualToCallableReturn(oeisID, validateOEISid, oeisID)
23
+
24
+ def test__validateOEISid_valid_id_case_insensitive(oeisID: str) -> None:
25
+ standardizedEqualToCallableReturn(oeisID.upper(), validateOEISid, oeisID.lower())
26
+ standardizedEqualToCallableReturn(oeisID.upper(), validateOEISid, oeisID.upper())
27
+ standardizedEqualToCallableReturn(oeisID.upper(), validateOEISid, oeisID.swapcase())
28
+
29
+ parameters_test_aOFn_invalid_n = [
30
+ (-random.randint(1, 100), "randomNegative"),
31
+ ("foo", "string"),
32
+ (1.5, "float")
33
+ ]
34
+ badValues, badValuesIDs = zip(*parameters_test_aOFn_invalid_n)
35
+ @pytest.mark.parametrize("badN", badValues, ids=badValuesIDs)
36
+ def test_aOFn_invalid_n(oeisID_1random: str, badN: Any) -> None:
37
+ """Check that negative or non-integer n raises ValueError."""
38
+ standardizedEqualToCallableReturn(ValueError, oeisIDfor_n, oeisID_1random, badN)
39
+
40
+ def test_aOFn_zeroDim_A001418() -> None:
41
+ standardizedEqualToCallableReturn(ArithmeticError, oeisIDfor_n, 'A001418', 0)
42
+
43
+ # ===== OEIS Cache Tests =====
44
+ @pytest.mark.parametrize("cacheExists", [True, False])
45
+ @unittest.mock.patch('pathlib.Path.exists')
46
+ @unittest.mock.patch('pathlib.Path.unlink')
47
+ def test_clearOEIScache(mock_unlink: unittest.mock.MagicMock, mock_exists: unittest.mock.MagicMock, cacheExists: bool) -> None:
48
+ """Test OEIS cache clearing with both existing and non-existing cache."""
49
+ mock_exists.return_value = cacheExists
50
+ clearOEIScache()
51
+
52
+ if cacheExists:
53
+ # Each OEIS ID has two cache files
54
+ expected_calls = len(settingsOEIS) * 2
55
+ assert mock_unlink.call_count == expected_calls
56
+ mock_unlink.assert_has_calls([unittest.mock.call(missing_ok=True)] * expected_calls)
57
+ else:
58
+ mock_exists.assert_called_once()
59
+ mock_unlink.assert_not_called()
60
+
61
+ def testNetworkError(monkeypatch: pytest.MonkeyPatch, pathCacheTesting: Path) -> None:
62
+ """Test network error handling."""
63
+ def mockUrlopen(*args: Any, **kwargs: Any) -> NoReturn:
64
+ raise URLError("Network error")
65
+
66
+ monkeypatch.setattr(urllib.request, 'urlopen', mockUrlopen)
67
+ standardizedEqualToCallableReturn(URLError, getOEISidValues, next(iter(settingsOEIS)))
68
+
69
+ # ===== Command Line Interface Tests =====
70
+ def testHelpText() -> None:
71
+ """Test that help text is complete and examples are valid."""
72
+ outputStream = io.StringIO()
73
+ with redirect_stdout(outputStream):
74
+ getOEISids()
75
+
76
+ helpText = outputStream.getvalue()
77
+
78
+ # Verify content
79
+ for oeisID in oeisIDsImplemented:
80
+ assert oeisID in helpText
81
+ assert settingsOEIS[oeisID]['description'] in helpText
82
+
83
+ # Extract and verify examples
84
+
85
+ cliMatch = regex.search(r'OEIS_for_n (\w+) (\d+)', helpText)
86
+ pythonMatch = regex.search(r"oeisIDfor_n\('(\w+)', (\d+)\)", helpText)
87
+
88
+ assert cliMatch and pythonMatch, "Help text missing examples"
89
+ oeisID, n = pythonMatch.groups()
90
+ n = int(n)
91
+
92
+ # Verify CLI and Python examples use same values
93
+ assert cliMatch.groups() == (oeisID, str(n)), "CLI and Python examples inconsistent"
94
+
95
+ # Verify the example works
96
+ expectedValue = oeisIDfor_n(oeisID, n)
97
+
98
+ # Test CLI execution of the example
99
+ with unittest.mock.patch('sys.argv', ['OEIS_for_n', oeisID, str(n)]):
100
+ outputStream = io.StringIO()
101
+ with redirect_stdout(outputStream):
102
+ OEIS_for_n()
103
+ standardizedEqualToCallableReturn(expectedValue, lambda: int(outputStream.getvalue().strip().split()[0]))
104
+
105
+ def testCLI_InvalidInputs() -> None:
106
+ """Test CLI error handling."""
107
+ testCases = [
108
+ (['OEIS_for_n'], "missing arguments"),
109
+ (['OEIS_for_n', 'A999999', '1'], "invalid OEIS ID"),
110
+ (['OEIS_for_n', 'A001415', '-1'], "negative n"),
111
+ (['OEIS_for_n', 'A001415', 'abc'], "non-integer n"),
112
+ ]
113
+
114
+ for arguments, _testID in testCases:
115
+ with unittest.mock.patch('sys.argv', arguments):
116
+ standardizedSystemExit("error", OEIS_for_n)
117
+
118
+ def testCLI_HelpFlag() -> None:
119
+ """Verify --help output contains required information."""
120
+ with unittest.mock.patch('sys.argv', ['OEIS_for_n', '--help']):
121
+ outputStream = io.StringIO()
122
+ with redirect_stdout(outputStream):
123
+ standardizedSystemExit("nonError", OEIS_for_n)
124
+
125
+ helpOutput = outputStream.getvalue()
126
+ assert "Available OEIS sequences:" in helpOutput
127
+ assert "Usage examples:" in helpOutput
128
+ assert all(oeisID in helpOutput for oeisID in oeisIDsImplemented)
tests/test_other.py ADDED
@@ -0,0 +1,84 @@
1
+ from collections.abc import Callable
2
+ from mapFolding.beDRY import getLeavesTotal, setCPUlimit, validateListDimensions
3
+ from tests.conftest import standardizedEqualToCallableReturn
4
+ from typing import Any, Literal
5
+ from Z0Z_tools import intInnit
6
+ from Z0Z_tools.pytestForYourUse import PytestFor_intInnit, PytestFor_oopsieKwargsie
7
+ import numba
8
+ import pytest
9
+ import sys
10
+
11
+ @pytest.mark.parametrize("listDimensions,expected_intInnit,expected_validateListDimensions", [
12
+ (None, ValueError, ValueError), # None instead of list
13
+ (['a'], ValueError, ValueError), # string
14
+ ([-4, 2], [-4, 2], ValueError), # negative
15
+ ([-3], [-3], ValueError), # negative
16
+ ([0, 0], [0, 0], NotImplementedError), # no positive dimensions
17
+ ([0, 5, 6], [0, 5, 6], (5, 6)), # zeros ignored
18
+ ([0], [0], NotImplementedError), # edge case
19
+ ([1, 2, 3, 4, 5], [1, 2, 3, 4, 5], (1, 2, 3, 4, 5)), # sequential
20
+ ([1, sys.maxsize], [1, sys.maxsize], (1, sys.maxsize)), # maxint
21
+ ([7.5], ValueError, ValueError), # float
22
+ ([1] * 1000, [1] * 1000, (1,) * 1000), # long list
23
+ ([11], [11], NotImplementedError), # single dimension
24
+ ([13, 0, 17], [13, 0, 17], (13, 17)), # zeros handled
25
+ ([2, 2, 2, 2], [2, 2, 2, 2], (2, 2, 2, 2)), # repeated dimensions
26
+ ([2, 3, 4], [2, 3, 4], (2, 3, 4)),
27
+ ([2, 3], [2, 3], (2, 3)),
28
+ ([2] * 11, [2] * 11, (2,) * 11), # power of 2
29
+ ([3, 2], [3, 2], (2, 3)), # return value is sorted
30
+ ([3] * 5, [3] * 5, (3,) * 5), # power of 3
31
+ ([None], TypeError, TypeError), # None
32
+ ([True], TypeError, TypeError), # bool
33
+ ([[17, 39]], TypeError, TypeError), # nested
34
+ ([], ValueError, ValueError), # empty
35
+ ([complex(1,1)], ValueError, ValueError), # complex number
36
+ ([float('inf')], ValueError, ValueError), # infinity
37
+ ([float('nan')], ValueError, ValueError), # NaN
38
+ ([sys.maxsize - 1, 1], [sys.maxsize - 1, 1], (1, sys.maxsize - 1)), # near maxint
39
+ ([sys.maxsize // 2, sys.maxsize // 2, 2], [sys.maxsize // 2, sys.maxsize // 2, 2], (2, sys.maxsize // 2, sys.maxsize // 2)), # overflow protection
40
+ ([sys.maxsize, sys.maxsize], [sys.maxsize, sys.maxsize], (sys.maxsize, sys.maxsize)), # overflow protection
41
+ (range(3, 7), [3, 4, 5, 6], (3, 4, 5, 6)), # range sequence type
42
+ (tuple([3, 5, 7]), [3, 5, 7], (3, 5, 7)), # tuple sequence type
43
+ ])
44
+ def test_listDimensionsAsParameter(listDimensions: None | list[str] | list[int] | list[float] | list[None] | list[bool] | list[list[int]] | list[complex] | range | tuple[int, ...],
45
+ expected_intInnit: type[ValueError] | list[int] | type[TypeError],
46
+ expected_validateListDimensions: type[ValueError] | type[NotImplementedError] | tuple[int, ...] | type[TypeError]) -> None:
47
+ """Test both validateListDimensions and getLeavesTotal with the same inputs."""
48
+ standardizedEqualToCallableReturn(expected_intInnit, intInnit, listDimensions)
49
+ standardizedEqualToCallableReturn(expected_validateListDimensions, validateListDimensions, listDimensions)
50
+
51
+ def test_getLeavesTotal_edge_cases() -> None:
52
+ """Test edge cases for getLeavesTotal."""
53
+ # Order independence
54
+ standardizedEqualToCallableReturn(getLeavesTotal((2, 3, 4)), getLeavesTotal, (4, 2, 3))
55
+
56
+ # Input preservation
57
+ mapShape = (2, 3)
58
+ standardizedEqualToCallableReturn(6, getLeavesTotal, mapShape)
59
+ # Remove the lambda entirely for a simpler approach
60
+ assert mapShape == (2, 3), "Input tuple was modified"
61
+
62
+ @pytest.mark.parametrize("nameOfTest,callablePytest", PytestFor_intInnit())
63
+ def testIntInnit(nameOfTest: str, callablePytest: Callable[[], None]) -> None:
64
+ callablePytest()
65
+
66
+ @pytest.mark.parametrize("nameOfTest,callablePytest", PytestFor_oopsieKwargsie())
67
+ def testOopsieKwargsie(nameOfTest: str, callablePytest: Callable[[], None]) -> None:
68
+ callablePytest()
69
+
70
+ @pytest.mark.parametrize("CPUlimit, expectedLimit", [
71
+ (None, numba.get_num_threads()),
72
+ (False, numba.get_num_threads()),
73
+ (True, 1),
74
+ (4, 4),
75
+ (0.5, max(1, numba.get_num_threads() // 2)),
76
+ (-0.5, max(1, numba.get_num_threads() // 2)),
77
+ (-2, max(1, numba.get_num_threads() - 2)),
78
+ (0, numba.get_num_threads()),
79
+ (1, 1),
80
+ ])
81
+ def test_setCPUlimit(CPUlimit: None | float | bool | Literal[4] | Literal[-2] | Literal[0] | Literal[1], expectedLimit: Any | int) -> None:
82
+ from mapFolding.theSSOT import concurrencyPackage
83
+ if concurrencyPackage == 'numba':
84
+ standardizedEqualToCallableReturn(expectedLimit, setCPUlimit, CPUlimit)
tests/test_tasks.py ADDED
@@ -0,0 +1,50 @@
1
+ from typing import Literal
2
+ from mapFolding.basecamp import countFolds
3
+ from mapFolding.beDRY import getTaskDivisions, setCPUlimit, validateListDimensions, getLeavesTotal
4
+ from mapFolding.noHomeYet import getFoldsTotalKnown
5
+ from tests.conftest import standardizedEqualToCallableReturn
6
+ from Z0Z_tools.pytestForYourUse import PytestFor_defineConcurrencyLimit
7
+ from collections.abc import Callable
8
+ import pytest
9
+
10
+ # TODO add a test. `C` = number of logical cores available. `n = C + 1`. Ensure that `[2,n]` is computed correctly.
11
+ # Or, probably smarter: limit the number of cores, then run a test with C+1.
12
+
13
+ def test_countFoldsComputationDivisionsInvalid(mapShapeTestFunctionality: tuple[int, ...]) -> None:
14
+ standardizedEqualToCallableReturn(ValueError, countFolds, mapShapeTestFunctionality, None, {"wrong": "value"})
15
+
16
+ def test_countFoldsComputationDivisionsMaximum(listDimensionsTestParallelization: list[int]) -> None:
17
+ standardizedEqualToCallableReturn(getFoldsTotalKnown(tuple(listDimensionsTestParallelization)), countFolds, listDimensionsTestParallelization, None, 'maximum')
18
+
19
+ @pytest.mark.parametrize("nameOfTest,callablePytest", PytestFor_defineConcurrencyLimit())
20
+ def test_defineConcurrencyLimit(nameOfTest: str, callablePytest: Callable[[], None]) -> None:
21
+ callablePytest()
22
+
23
+ @pytest.mark.parametrize("CPUlimitParameter", [{"invalid": True}, ["weird"]])
24
+ def test_countFolds_cpuLimitOopsie(mapShapeTestFunctionality: tuple[int, ...], CPUlimitParameter: dict[str, bool] | list[str]) -> None:
25
+ standardizedEqualToCallableReturn(ValueError, countFolds, mapShapeTestFunctionality, None, 'cpu', CPUlimitParameter)
26
+
27
+ @pytest.mark.parametrize("computationDivisions, concurrencyLimit, listDimensions, expectedTaskDivisions", [
28
+ (None, 4, [9, 11], 0),
29
+ ("maximum", 4, [7, 11], 77),
30
+ ("cpu", 4, [3, 7], 4),
31
+ (["invalid"], 4, [19, 23], ValueError),
32
+ (20, 4, [3,5], ValueError)
33
+ ])
34
+ def test_getTaskDivisions(computationDivisions: None | list[str] | Literal['maximum'] | Literal['cpu'] | Literal[20],
35
+ concurrencyLimit: Literal[4],
36
+ listDimensions: list[int],
37
+ expectedTaskDivisions: type[ValueError] | Literal[0] | Literal[77] | Literal[4]) -> None:
38
+ mapShape = validateListDimensions(listDimensions)
39
+ leavesTotal = getLeavesTotal(mapShape)
40
+ standardizedEqualToCallableReturn(expectedTaskDivisions, getTaskDivisions, computationDivisions, concurrencyLimit, leavesTotal)
41
+
42
+ @pytest.mark.parametrize("expected,parameter", [
43
+ (ValueError, [4]), # list
44
+ (ValueError, (2,)), # tuple
45
+ (ValueError, {2}), # set
46
+ (ValueError, {"cores": 2}), # dict
47
+ ])
48
+ def test_setCPUlimitMalformedParameter(expected: type[ValueError] | Literal[2], parameter: list[int] | tuple[int] | set[int] | dict[str, int] | Literal['2']) -> None:
49
+ """Test that invalid CPUlimit types are properly handled."""
50
+ standardizedEqualToCallableReturn(expected, setCPUlimit, parameter)