diff --git a/docs/REGRESSION_LEDGER.md b/docs/REGRESSION_LEDGER.md index 1ae9056..e20ea47 100644 --- a/docs/REGRESSION_LEDGER.md +++ b/docs/REGRESSION_LEDGER.md @@ -28,6 +28,7 @@ How to add one: [TESTING.md](TESTING.md#the-ledger). | 2026-10-08 | F15: an empty, complex, object or text array is written as an image result (or crashes with KeyError) | `portable_dtype` and `_write_one` do not refuse what no host can open | this PR | `test_error_codes.py::ImagesNoHostCanOpen.test_complex_object_and_text_arrays_are_unsupported_dtype`, `test_roundtrip_matrix.py::Matrix.test_worker_protocol` (`make_array/*`) | | 2026-10-08 | F16: hints on the wrong kind of parameter, `Min(5), Max(1)`, output markers on inputs and duplicate output names are accepted and ignored | `introspection` validates only some misuses | this PR | `test_error_codes.py::HintMisuse.test_bounds_on_text_and_an_empty_range`, `test_error_codes.py::HintMisuse.test_duplicate_output_names`, `test_roundtrip_hints.py::InvalidCombinations.test_every_invalid_combination_is_refused_with_a_message_that_names_the_culprit` | | 2026-10-12 | An output declared `Name("../other_run/result")`, an absolute name or `Name("cells/area")` wrote outside the result folder (or failed because the directory did not exist) | `introspection._outputs` checked only duplicate names; the writers joined `name + suffix` onto the job folder unchecked | [#104](https://github.com/CellMigrationLab/LabConstrictor-Tools/pull/104): unsafe names are a `DeclarationError` (`introspection._check_output_name`); `convert._job_file` refuses any path that is not a direct child of the job folder | `test_output_names.py::Declaration.test_unsafe_names_are_a_declaration_error_for_every_output_type`, `test_output_names.py::Writers.test_every_writer_refuses_a_name_that_leaves_the_job_folder` | +| 2026-10-12 | `ci` on `main` went red on Windows and macOS with Python 3.10 only (pull requests were green): the wheel test died with `SystemExit ... invalid command 'bdist_wheel'`, and on Windows 3.10 `conformance --path` printed nothing | the runners' Python 3.10 ships a setuptools older than 70.1, which has no built-in `bdist_wheel` and exits instead of raising (the test only skipped on `Exception`); the command-line test started Python with an empty environment, which Windows 3.10 cannot do without `SYSTEMROOT`; pull requests run Windows/macOS on 3.12 only, so neither showed before the merge | this PR: the `test` extra asks for `setuptools>=70.1`, the wheel test skips only on that one `bdist_wheel` message (any other error fails), the command-line test passes `SYSTEMROOT`; `docs/TESTING.md` says how to run the full matrix on a branch | `tests/test_conformance.py::Packaging.test_the_built_wheel_contains_the_file_and_the_loader_reads_it_from_there`, `tests/test_conformance.py::Loader.test_the_command_line_gives_the_path_and_validates` | | 2026-10-12 | Found writing the cross-host data: the 12 Oct spec rules (number limits, coordinate frame, calibration, retention) lived only as prose and as copies in four code bases, so one host could drift without any test noticing | No shared, validated, packaged source of the rules: the proposed `conformance.json` did not exist and PROTOCOL.md already pointed at it | this PR: `labconstrictor_tools/conformance.json` (package data) and `conformance.load()` / `validate()`; data checked against the code and against plain-Python oracles | `test_conformance.py::Schema.test_each_kind_of_damage_is_named`, `test_conformance.py::AgainstTheCode.test_limits_match_the_code_and_the_spec`, `test_conformance.py::AgainstTheCode.test_the_compat_vectors_give_the_same_answer_as_the_reference`, `test_conformance.py::CoordinateData.test_the_placement_formula_for_every_downsample_variant`, `test_conformance.py::Packaging.test_the_built_wheel_contains_the_file_and_the_loader_reads_it_from_there` (and the sabotage rows `conformance:*`) | | 2026-10-08 | F20: on ARM (Apple silicon) a uint64 image holding 2**64-1 is stored as float32 2**64 instead of being refused | `portable_dtype` checks exactness by casting the float32 back to uint64, an out-of-range cast that is undefined: x86 gives 0 (refused), ARM saturates (accepted) | this PR | `test_error_codes.py::Float32Exactness.test_uint64_beyond_float32_is_refused_with_unsupported_dtype_on_every_cpu` (numpy warns about the out-of-range cast on x86, the test turns that into a failure), `test_roundtrip_matrix.py::Matrix.test_worker_protocol` (`echo_image/uint64/beyond-float32`; found by the macOS job of CI) | | 2026-10-12 | A host sending a misspelt, renamed or removed parameter (`sigam` for `sigma`) got COMPLETE with the default value of the right one | `convert.load_inputs` looped over the declared inputs and never looked at extra keys (only the command line checked names) | [#106](https://github.com/CellMigrationLab/LabConstrictor-Tools/pull/106) | `test_roundtrip_unknown_inputs.py::UnknownInputs.test_a_misspelt_renamed_or_extra_input_is_refused_and_the_tool_does_not_run` | diff --git a/docs/TESTING.md b/docs/TESTING.md index f120ac8..089a63e 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -60,7 +60,9 @@ Why: a PR branch push used to run both the `push` and the `pull_request` trigger running. Now `push` is limited to `main`, a newer push to the same ref cancels the older run (never on `main`), the test jobs wait for `lint` (about 30 s) so a lint failure costs one runner instead of six, and the PR matrix drops only the combinations where a 3.10-only or 3.12-only difference is rare (Windows/macOS 3.10). Every job has `timeout-minutes: 60` and pip is cached. The matrix choice is the -`include` expression in `ci.yml`. +`include` expression in `ci.yml`. The cost: a failure that exists only on Windows or macOS with Python 3.10 reaches `main` unseen (the wheel test on an old +setuptools did exactly that, 2026-10-12), because pull requests do not run those two jobs. To run the full matrix on a branch before merging, start `ci` by hand: +`gh workflow run ci.yml --ref ` (or Actions, ci, Run workflow, pick the branch); a `workflow_dispatch` run is not a `pull_request` event, so it gets all 6 test jobs. ## How to add a case diff --git a/pyproject.toml b/pyproject.toml index c4843ca..b5b02d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ requires-python = ">=3.10" dependencies = [] # standard library only: it has to live in every LabConstrictor app environment [project.optional-dependencies] -test = ["numpy", "pandas", "tifffile", "scipy", "scikit-image", "ipywidgets", "ipython", "psutil", "hypothesis"] # what the test suites and the example app need +test = ["numpy", "pandas", "tifffile", "scipy", "scikit-image", "ipywidgets", "ipython", "psutil", "hypothesis", "setuptools>=70.1"] # what the test suites and the example app need; setuptools 70.1 has bdist_wheel built in (test_conformance builds the wheel in-process on every OS and Python, 3.10 runners start with an older one) [project.urls] Repository = "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/CellMigrationLab/LabConstrictor-Tools" diff --git a/tests/test_conformance.py b/tests/test_conformance.py index 425e63e..2248d25 100644 --- a/tests/test_conformance.py +++ b/tests/test_conformance.py @@ -10,6 +10,7 @@ import copy import io import json +import os import re import shutil import string @@ -55,20 +56,25 @@ def test_the_file_is_in_the_package_and_valid(self): def test_the_command_line_gives_the_path_and_validates(self): env = {"PYTHONPATH": str(ROOT), "PATH": ""} + if os.name == "nt": + # Python on Windows cannot start without SYSTEMROOT (random numbers, DLL search); it is not a PATH lookup + env["SYSTEMROOT"] = os.environ.get("SYSTEMROOT", "") path = subprocess.run( [sys.executable, "-m", "labconstrictor_tools.conformance", "--path"], capture_output=True, text=True, env=env, ) - self.assertEqual(Path(path.stdout.strip()), conformance.path()) + self.assertEqual(Path(path.stdout.strip()), conformance.path(), path.stderr) valid = subprocess.run( [sys.executable, "-m", "labconstrictor_tools.conformance", "--validate"], capture_output=True, text=True, env=env, ) - self.assertEqual((valid.returncode, valid.stdout.strip()), (0, "conformance.json is valid")) + self.assertEqual( + (valid.returncode, valid.stdout.strip()), (0, "conformance.json is valid"), valid.stderr + ) usage = subprocess.run( [sys.executable, "-m", "labconstrictor_tools.conformance"], capture_output=True, @@ -838,6 +844,11 @@ def test_every_key_of_the_file_is_in_the_layout_table_of_the_doc(self): self.assertIn(name, text, name) +BDIST_WHEEL_MISSING = ( + "invalid command 'bdist_wheel'" # what an old setuptools says when it cannot build a wheel +) + + class Packaging(unittest.TestCase): def test_pyproject_ships_the_file(self): text = (ROOT / "pyproject.toml").read_text(encoding="utf-8") @@ -862,15 +873,18 @@ def test_the_built_wheel_contains_the_file_and_the_loader_reads_it_from_there(se out.mkdir() cwd = Path.cwd() try: - import os - os.chdir(tree) with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()): wheel = build_meta.build_wheel(str(out)) - except ( - Exception - ) as error: # noqa: BLE001 - an offline or odd build environment is a skip, not a failure of the data - self.skipTest("the wheel could not be built here (%s: %s)" % (type(error).__name__, error)) + except SystemExit as error: + # setuptools before 70.1 has no built-in bdist_wheel (it came with the `wheel` package) and exits instead of raising; + # only that one condition is a skip, any other exit is a real failure + if BDIST_WHEEL_MISSING not in str(error): + raise + self.skipTest( + "this setuptools cannot build a wheel (no bdist_wheel: needs setuptools>=70.1 or the `wheel` package; " + "the `test` extra of pyproject.toml asks for it)" + ) finally: os.chdir(cwd) import zipfile