Skip to content

Commit dda71aa

Browse files
authored
Make export reports byte-deterministic (#701)
## Summary - serialize component export reports as explicit UTF-8 bytes with LF endings - reuse the byte serializer for GGUF reuse package metadata - simulate Windows text newline translation in a platform-independent regression ## Root cause `ComponentExportReport.write_json()` used `Path.write_text()`. Windows text mode translated the serializer's LF characters to CRLF, so persisted `export_report.json` bytes differed from the deterministic serializer and package hashes varied by platform. ## Tests - `python -m pytest src/mobius/_model_package_test.py -q --tb=short` (90 passed) - `lintrunner f --output oneline src/mobius/_export_report.py src/mobius/_model_package.py src/mobius/_model_package_test.py` - `git diff --check` Signed-off-by: Justin Chu <justinchuby@users.noreply.github.com> Copilot-Session: 43bb31ac-c136-44bc-8cdf-70cbcedd4a42
1 parent dcf8bab commit dda71aa

3 files changed

Lines changed: 26 additions & 5 deletions

File tree

‎src/mobius/_export_report.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,9 +306,13 @@ def to_json(self) -> str:
306306
"""Serialize with deterministic key ordering and a trailing newline."""
307307
return json.dumps(self.to_dict(), indent=2, sort_keys=True) + "\n"
308308

309+
def to_bytes(self) -> bytes:
310+
"""Serialize to deterministic UTF-8 bytes with LF line endings."""
311+
return self.to_json().encode("utf-8")
312+
309313
def write_json(self, path: str | Path) -> None:
310-
"""Write the deterministic report."""
311-
Path(path).write_text(self.to_json(), encoding="utf-8")
314+
"""Write the deterministic report without platform newline translation."""
315+
Path(path).write_bytes(self.to_bytes())
312316

313317
@classmethod
314318
def from_dict(cls, payload: Mapping[str, object]) -> ComponentExportReport:

‎src/mobius/_model_package.py‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -650,9 +650,7 @@ def save(
650650
+ "\n"
651651
).encode()
652652
if self.export_report is not None:
653-
package_metadata[_EXPORT_REPORT] = (
654-
self.export_report.to_json().encode()
655-
)
653+
package_metadata[_EXPORT_REPORT] = self.export_report.to_bytes()
656654
if self.draft_manifest is not None:
657655
package_metadata["draft_manifest.json"] = (
658656
json.dumps(self.draft_manifest, indent=2, sort_keys=True) + "\n"

‎src/mobius/_model_package_test.py‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import logging
99
import threading
1010
import types
11+
from pathlib import Path
1112

1213
import onnx_ir as ir
1314
import pytest
@@ -274,6 +275,24 @@ def test_partial_report_roundtrips_deterministically(self, tmp_path):
274275
assert components["tokenizer"]["export_status"] == "omitted"
275276
assert components["tokenizer"]["runtime_validation_status"] == "unvalidated"
276277

278+
def test_report_write_preserves_lf_when_text_writes_translate_newlines(
279+
self, tmp_path, monkeypatch
280+
):
281+
report = _partial_export_report()
282+
report_path = tmp_path / "export_report.json"
283+
284+
def write_text_with_windows_newlines(
285+
path: Path, data: str, encoding: str | None = None, **_kwargs
286+
) -> int:
287+
encoded = data.replace("\n", "\r\n").encode(encoding or "utf-8")
288+
return path.write_bytes(encoded)
289+
290+
monkeypatch.setattr(Path, "write_text", write_text_with_windows_newlines)
291+
report.write_json(report_path)
292+
293+
assert report_path.read_bytes() == report.to_bytes()
294+
assert b"\r\n" not in report_path.read_bytes()
295+
277296
def test_save_without_report_rejects_stale_report(self, tmp_path):
278297
report_path = tmp_path / "export_report.json"
279298
report_path.write_text(_partial_export_report().to_json(), encoding="utf-8")

0 commit comments

Comments
 (0)