migrate pyqt5 to pyside6
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
"""The calibration sequence document: round trip, refusals, derived summary.
|
||||
|
||||
The format is a contract with another machine's bench, so most of these tests
|
||||
are about what happens when the file is *wrong* — a sequence half-applied is
|
||||
worse than one rejected.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from orisys_autocal.sequence_io import (
|
||||
DOCUMENT_KIND,
|
||||
FORMAT_VERSION,
|
||||
SequenceFormatError,
|
||||
SequencePoint,
|
||||
_version_tuple,
|
||||
format_duration,
|
||||
format_span,
|
||||
from_document,
|
||||
load,
|
||||
save,
|
||||
summarise,
|
||||
to_document,
|
||||
)
|
||||
|
||||
#: Where scratch files go. Deliberately not ``tmp_dir``: this suite has to run
|
||||
#: in a sandboxed shell, and ``tmp_dir`` needs a writable process temp root that
|
||||
#: such an environment may not provide (the same reason ``conftest.make_window``
|
||||
#: avoids it).
|
||||
_SCRATCH = Path(__file__).resolve().parents[1] / "outputs" / "_test_sequence_io"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tmp_dir():
|
||||
shutil.rmtree(_SCRATCH, ignore_errors=True)
|
||||
_SCRATCH.mkdir(parents=True, exist_ok=True)
|
||||
yield _SCRATCH
|
||||
shutil.rmtree(_SCRATCH, ignore_errors=True)
|
||||
|
||||
PROFILE = {
|
||||
"mode": "flat",
|
||||
"pitch_mm": 2.0,
|
||||
"margin_mm": 1.0,
|
||||
"radius_mm": 0.0,
|
||||
"clear_height_mm": 5.0,
|
||||
"approach_speed_mm_s": 2.0,
|
||||
"press_speed_mm_s": 0.5,
|
||||
"press_depth_mm": 0.2,
|
||||
"dwell_s": 0.5,
|
||||
"settle_window_s": 0.5,
|
||||
"repeats": 3,
|
||||
"order": "serpentine",
|
||||
}
|
||||
|
||||
|
||||
def _points(count: int = 6) -> list[SequencePoint]:
|
||||
return [
|
||||
SequencePoint(
|
||||
id=index + 1,
|
||||
target_mm={"x": 2.0 * index, "y": 0.0, "z": 12.4},
|
||||
sensor_mm={"x": 2.0 * index, "y": 0.0, "z": 0.0},
|
||||
clearance_mm=5.0,
|
||||
)
|
||||
for index in range(count)
|
||||
]
|
||||
|
||||
|
||||
def _document(**overrides) -> dict:
|
||||
payload = to_document(
|
||||
profile=PROFILE,
|
||||
points=_points(),
|
||||
name="sweep",
|
||||
units_per_mm=10.0,
|
||||
app_version="0.1.0",
|
||||
shared_version="0.1.0",
|
||||
shared_api="0.1",
|
||||
)
|
||||
payload.update(overrides)
|
||||
return payload
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ round trip
|
||||
|
||||
|
||||
def test_a_document_round_trips_through_a_file(tmp_dir):
|
||||
path = tmp_dir / "sweep.oryseq"
|
||||
save(_document(), path)
|
||||
back = load(path, current_profile=PROFILE)
|
||||
assert back.point_count == 6
|
||||
assert back.name == "sweep"
|
||||
assert back.profile["pitch_mm"] == 2.0
|
||||
assert back.units_per_mm == 10.0
|
||||
assert not back.warnings
|
||||
# Both frames survive: the platform target and the sensor position.
|
||||
assert back.points[0].target_mm["z"] == pytest.approx(12.4)
|
||||
assert back.points[0].sensor_mm is not None
|
||||
|
||||
|
||||
def test_the_written_file_is_readable_json_with_the_house_style(tmp_dir):
|
||||
path = tmp_dir / "sweep.oryseq"
|
||||
save(_document(), path)
|
||||
text = path.read_text(encoding="utf-8")
|
||||
assert text.endswith("\n")
|
||||
assert "\n " in text, "the document is not indented"
|
||||
assert "\\u" not in text, "non-ASCII was escaped"
|
||||
payload = json.loads(text)
|
||||
assert payload["kind"] == DOCUMENT_KIND
|
||||
assert payload["format_version"] == FORMAT_VERSION
|
||||
|
||||
|
||||
def test_saving_leaves_no_temporary_file_behind(tmp_dir):
|
||||
path = tmp_dir / "sweep.oryseq"
|
||||
save(_document(), path)
|
||||
assert [entry.name for entry in tmp_dir.iterdir()] == ["sweep.oryseq"]
|
||||
|
||||
|
||||
def test_saving_over_an_existing_file_replaces_it(tmp_dir):
|
||||
path = tmp_dir / "sweep.oryseq"
|
||||
save(_document(), path)
|
||||
save(_document(name="second"), path)
|
||||
assert json.loads(path.read_text(encoding="utf-8"))["name"] == "second"
|
||||
|
||||
|
||||
# -------------------------------------------------------------------- refusals
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("case", "mutate", "expected"),
|
||||
[
|
||||
("wrong kind", lambda d: d.update(kind="something.else"), "not a sequence document"),
|
||||
("missing version", lambda d: d.pop("format_version"), "format_version"),
|
||||
("version not a string", lambda d: d.update(format_version=1.0), "format_version"),
|
||||
("newer version", lambda d: d.update(format_version="2.0"), "newer AutoCal"),
|
||||
("missing frame", lambda d: d.pop("frame"), "frame"),
|
||||
("wrong unit", lambda d: d.update(frame={"length_unit": "um"}), "length_unit"),
|
||||
("missing profile", lambda d: d.pop("profile"), "profile"),
|
||||
("points not a list", lambda d: d.update(points={}), "must be a list"),
|
||||
("point not an object", lambda d: d.update(points=["nope"]), "not an object"),
|
||||
("point missing axis", lambda d: d.update(points=[{"target_mm": {"x": 1, "y": 2}}]), "missing 'z'"),
|
||||
("point without target", lambda d: d.update(points=[{}]), "no target_mm"),
|
||||
],
|
||||
)
|
||||
def test_a_bad_document_is_refused_with_a_specific_message(case, mutate, expected):
|
||||
document = _document()
|
||||
mutate(document)
|
||||
with pytest.raises(SequenceFormatError) as excinfo:
|
||||
from_document(document, current_profile=PROFILE)
|
||||
assert expected in str(excinfo.value), case
|
||||
|
||||
|
||||
def test_a_non_json_file_is_refused_with_its_position(tmp_dir):
|
||||
path = tmp_dir / "broken.oryseq"
|
||||
path.write_text("this is not json", encoding="utf-8")
|
||||
with pytest.raises(SequenceFormatError) as excinfo:
|
||||
load(path, current_profile=PROFILE)
|
||||
assert "not valid JSON" in str(excinfo.value)
|
||||
assert "line 1" in str(excinfo.value)
|
||||
|
||||
|
||||
def test_a_missing_file_is_refused_not_crashed(tmp_dir):
|
||||
with pytest.raises(SequenceFormatError):
|
||||
load(tmp_dir / "absent.oryseq", current_profile=PROFILE)
|
||||
|
||||
|
||||
def test_a_json_array_is_refused(tmp_dir):
|
||||
path = tmp_dir / "array.oryseq"
|
||||
path.write_text("[1, 2, 3]", encoding="utf-8")
|
||||
with pytest.raises(SequenceFormatError) as excinfo:
|
||||
load(path, current_profile=PROFILE)
|
||||
assert "top level" in str(excinfo.value)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- summary
|
||||
|
||||
|
||||
def test_the_summary_reports_count_and_travel_span():
|
||||
summary = summarise(_points(6), PROFILE)
|
||||
assert summary["count"] == 6
|
||||
assert summary["min_mm"]["x"] == 0.0
|
||||
assert summary["max_mm"]["x"] == 10.0
|
||||
assert summary["span_mm"]["x"] == pytest.approx(10.0)
|
||||
# A constant coordinate has a nil span, and says so.
|
||||
assert summary["span_mm"]["z"] == pytest.approx(0.0)
|
||||
|
||||
|
||||
def test_the_summary_is_recomputed_and_wins_over_the_file():
|
||||
"""A hand-edited summary must not be believed."""
|
||||
document = _document()
|
||||
document["summary"]["count"] = 999
|
||||
document["summary"]["span_mm"]["x"] = 999.0
|
||||
back = from_document(document, current_profile=PROFILE)
|
||||
assert back.summary["count"] == 6
|
||||
assert back.summary["span_mm"]["x"] == pytest.approx(10.0)
|
||||
assert back.warnings and "disagrees" in back.warnings[0]
|
||||
|
||||
|
||||
def test_an_agreeing_summary_produces_no_warning():
|
||||
back = from_document(_document(), current_profile=PROFILE)
|
||||
assert not any("disagrees" in warning for warning in back.warnings)
|
||||
|
||||
|
||||
def test_no_points_imports_the_profile_and_says_so():
|
||||
document = _document()
|
||||
document.pop("points")
|
||||
back = from_document(document, current_profile=PROFILE)
|
||||
assert back.point_count == 0
|
||||
assert any("no points" in warning for warning in back.warnings)
|
||||
|
||||
|
||||
def test_a_missing_profile_key_keeps_the_current_value():
|
||||
document = _document()
|
||||
document["profile"].pop("pitch_mm")
|
||||
back = from_document(document, current_profile=PROFILE)
|
||||
assert back.profile["pitch_mm"] == 2.0
|
||||
assert any("pitch_mm" in warning for warning in back.warnings)
|
||||
|
||||
|
||||
def test_a_disabled_point_is_counted_separately():
|
||||
points = _points(4)
|
||||
points[1] = SequencePoint(id=2, target_mm=points[1].target_mm, enabled=False)
|
||||
summary = summarise(points, PROFILE)
|
||||
assert summary["count"] == 4
|
||||
assert summary["enabled"] == 3
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- duration
|
||||
|
||||
|
||||
def test_the_duration_estimate_scales_with_repeats_and_points():
|
||||
one = summarise(_points(1), {**PROFILE, "repeats": 1})["estimated_duration_s"]
|
||||
three = summarise(_points(1), {**PROFILE, "repeats": 3})["estimated_duration_s"]
|
||||
assert three > one
|
||||
six = summarise(_points(6), {**PROFILE, "repeats": 3})["estimated_duration_s"]
|
||||
assert six > three
|
||||
|
||||
|
||||
def test_no_points_means_no_estimate():
|
||||
summary = summarise([], PROFILE)
|
||||
assert summary["estimated_duration_s"] == 0.0
|
||||
assert format_duration(0.0) == "—"
|
||||
|
||||
|
||||
def test_the_duration_is_formatted_as_a_bench_timer():
|
||||
assert format_duration(72.0) == "1:12"
|
||||
assert format_duration(3600.0) == "1:00:00"
|
||||
assert format_duration(None) == "—"
|
||||
assert format_duration(float("nan")) == "—"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- spans
|
||||
|
||||
|
||||
def test_a_zero_span_prints_once_not_twice():
|
||||
assert format_span(12.4, 12.4) == "12.400"
|
||||
assert format_span(0.0, 0.0) == "0.000"
|
||||
|
||||
|
||||
def test_a_real_span_shows_both_ends_and_the_length():
|
||||
assert format_span(0.0, 10.0) == "0.000–10.000 (10.000)"
|
||||
|
||||
|
||||
def test_a_missing_span_is_a_dash():
|
||||
assert format_span(None, None) == "—"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- version
|
||||
|
||||
|
||||
def test_versions_compare_as_tuples_not_strings():
|
||||
"""``"1.10" < "1.9"`` as strings would load a newer file as if supported."""
|
||||
assert _version_tuple("1.10") > _version_tuple("1.9")
|
||||
assert _version_tuple("1.0") == (1, 0)
|
||||
assert _version_tuple("1") == (1,)
|
||||
# A same-major newer minor is still refused, but is *recognised* as newer.
|
||||
document = _document()
|
||||
document["format_version"] = "1.10"
|
||||
with pytest.raises(SequenceFormatError):
|
||||
from_document(document, current_profile=PROFILE)
|
||||
Reference in New Issue
Block a user