Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/REGRESSION_LEDGER.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
4 changes: 3 additions & 1 deletion docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <branch>` (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

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
30 changes: 22 additions & 8 deletions tests/test_conformance.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import copy
import io
import json
import os
import re
import shutil
import string
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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")
Expand All @@ -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
Expand Down
Loading