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
10 changes: 10 additions & 0 deletions changelog.d/334-verify-raw-diagnostics.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Make `chronicle verify-raw` classify Wrangler diagnostics more carefully.
Colour codes are now stripped before matching, so a missing object is still
reported as `missing_remote` when Wrangler colours words inside the message. A
marker now counts only as a whole phrase, and only on a line that holds nothing
but markers or that reports an error. So a marker inside a longer identifier,
or on a line that reports no error, now gives `fetch_error`. An error line that
merely mentions a marker (for example
`error: could not reach bucket; NoSuchKey handling disabled`) is still
classified as `missing_remote`. Both statuses are failures, and neither change
can produce `verified`.
39 changes: 30 additions & 9 deletions chronicle/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from datetime import UTC, datetime
from pathlib import Path
import posixpath
import re
import shlex
import sqlite3
import subprocess
Expand Down Expand Up @@ -968,15 +969,7 @@ def result(
return result("fetch_error", str(exc))
values["fetch"] = fetch
if not fetch.ok:
missing = any(
marker in fetch.stderr.lower()
for marker in (
"nosuchkey",
"specified key does not exist",
"object not found",
"object does not exist",
)
)
missing = _is_missing_object_diagnostic(fetch.stderr)
return result("missing_remote" if missing else "fetch_error", "r2_get_failed")
values["sha256_remote"] = remote_digest
values["size_bytes_remote"] = remote_size
Expand Down Expand Up @@ -1011,6 +1004,34 @@ def _hash_stream(stream: BinaryIO) -> tuple[str, int]:
return digest.hexdigest(), size_bytes


_ANSI_ESCAPE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
_MISSING_OBJECT_MARKER = re.compile(
r"(?<![a-z0-9_])(?:nosuchkey"
r"|(?:the )?specified key does not exist"
r"|object (?:not found|does not exist))(?![a-z0-9_])"
)
_ERROR_LINE = re.compile(r"(?<![a-z0-9_])(?:error|failed)(?![a-z0-9_])|\u2718")


def _is_missing_object_diagnostic(stderr: str) -> bool:
"""Return whether Wrangler's diagnostics say the object is absent.

Colour codes are removed first, because Wrangler wraps words in them. A
marker counts only as a whole phrase, and only on a line that holds nothing
but markers (such as ``NoSuchKey: specified key does not exist``) or is an
error line, so unrelated text that merely mentions a marker stays a fetch
error.
"""
for raw_line in _ANSI_ESCAPE.sub("", stderr).lower().splitlines():
line = " ".join(raw_line.split())
if _MISSING_OBJECT_MARKER.search(line) is None:
continue
remainder = _MISSING_OBJECT_MARKER.sub("", line).strip(" .:;()[]-")
if not remainder or _ERROR_LINE.search(line):
return True
return False


def _stream_r2_object(
location: ArtifactStorageLocation, *, wrangler_command: str
) -> tuple[str, int, ArtifactCommandResult]:
Expand Down
62 changes: 62 additions & 0 deletions tests/test_chronicle_verify_raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,68 @@ def test_verify_raw_missing_object_markers_and_other_errors(tmp_path):
assert _verify(root, fake).entries[0].status == "fetch_error"


# Wrangler's stderr for an absent object, as the NZ hub observed it in a live
# read-back on 2026-10-10 (bytes reproduced here, not a retained log).
_REAL_WRANGLER_MISSING = (
"\x1b[31m\u2718 \x1b[41;31m[\x1b[41;97mERROR\x1b[41;31m]\x1b[0m "
"\x1b[1mThe specified key does not exist.\x1b[0m\n\n\n"
'\U0001fab5 Logs were written to "/tmp/wrangler.log"\n'
)


@pytest.mark.parametrize(
("stderr", "status"),
[
(_REAL_WRANGLER_MISSING, "missing_remote"),
# Colour codes inside the phrase itself.
(
"\x1b[31m\u2718 [ERROR]\x1b[0m The specified \x1b[1mkey\x1b[0m does "
"\x1b[4mnot\x1b[0m exist.",
"missing_remote",
),
("An error occurred (NoSuchKey) when calling GetObject", "missing_remote"),
(" Object not found. ", "missing_remote"),
("NoSuchKey: specified key does not exist", "missing_remote"),
# A marker inside a longer identifier is not the marker.
("error: NoSuchKeyHandler could not start", "fetch_error"),
("error: nosuchkeys table is locked", "fetch_error"),
# A marker merely mentioned on a line that reports no error.
("note: see the docs on NoSuchKey\nconnection reset by peer", "fetch_error"),
("warning: Object not found pages are cached for 60s", "fetch_error"),
(
"retrying; last reply said the specified key does not exist yet",
"fetch_error",
),
# Known limit: an error line that merely mentions a marker still counts.
(
"error: could not reach bucket; NoSuchKey handling disabled",
"missing_remote",
),
],
)
def test_verify_raw_classifies_wrangler_diagnostics(tmp_path, stderr, status):
root = tmp_path / "db" / "data"
_, key = _add_artifact(root)
fake = FakeWrangler(tmp_path / "fake")
fake.set_remote({}, {f"ledger-raw/{key}": stderr})

report = _verify(root, fake)

assert not report.valid
assert report.entries[0].status == status


def test_verify_raw_missing_marker_survives_colour_codes_at_any_position(tmp_path):
root = tmp_path / "db" / "data"
_, key = _add_artifact(root)
fake = FakeWrangler(tmp_path / "fake")
phrase = "[ERROR] The specified key does not exist."
for position in range(len(phrase) + 1):
coloured = phrase[:position] + "\x1b[1;31m" + phrase[position:] + "\x1b[0m"
fake.set_remote({}, {f"ledger-raw/{key}": coloured})
assert _verify(root, fake).entries[0].status == "missing_remote", position


def test_verify_raw_missing_executable_is_fetch_error(tmp_path):
root = tmp_path / "db" / "data"
_add_artifact(root)
Expand Down
Loading