Files
pyxis/tests/tools/test_build_map_pack.py
T
Torlando 9591d8074c fix: make install-marker renewal ownership-checked
Greptile round 9 remediation:

The round-8 heartbeat rewrote the marker unconditionally. If
installer A stalled on a single tile operation for longer than the
marker TTL, installer B could legitimately reclaim the expired
marker, and A's next heartbeat then overwrote B's live claim --
stealing the card out from under a running install.

The renewal now reads the marker before rewriting and only
proceeds when it still carries OUR owner (the same owner comparison
used by release and commit-time verification). A reclaimed,
deleted, or corrupt marker means the claim is void: the install
aborts with a clear "wait for the other installer to finish and
retry" error, leaving the foreign claim intact. The partially
published pack stays device-harmless (no record names it) and the
user retries once the other installer finishes.

- CLI: _renew_install_marker() checks owner before rewriting;
  PackError aborts the install (the existing cleanup paths remove
  the staged temp pack and release the marker as a no-op).
- Flasher: renewInstallMarker() checks owner before rewriting;
  fail() aborts the install (the existing cleanup paths remove the
  owned pack and release the marker as a no-op).
- Tests: CLI 88 (renewal refuses to steal a reclaimed/missing/
  corrupt claim; same-owner renewal still advances the epoch);
  flasher 26 (same contract).
2026-08-29 19:17:08 +00:00

1406 lines
63 KiB
Python

# Copyright (c) 2026 Pyxis contributors
# SPDX-License-Identifier: MIT
from __future__ import annotations
import hashlib
import importlib.util
import os
from pathlib import Path
import struct
import subprocess
import sys
import zlib
import pytest
ROOT = Path(__file__).resolve().parents[2]
TOOL = ROOT / "tools/maps/build_map_pack.py"
def load_tool():
spec = importlib.util.spec_from_file_location("build_map_pack", TOOL)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def png(width: int = 256, height: int = 256) -> bytes:
def chunk(kind: bytes, data: bytes) -> bytes:
return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data))
raw = b"".join(b"\x00" + b"\x00\x00\x00" * width for _ in range(height))
return (
b"\x89PNG\r\n\x1a\n"
+ chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0))
+ chunk(b"IDAT", zlib.compress(raw))
+ chunk(b"IEND", b"")
)
def png_chunks(chunks: list[tuple[bytes, bytes]]) -> bytes:
def chunk(kind: bytes, data: bytes) -> bytes:
return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data))
return b"\x89PNG\r\n\x1a\n" + b"".join(chunk(kind, data) for kind, data in chunks)
def ihdr(*, bit_depth: int = 8, color_type: int = 2, interlace: int = 0) -> bytes:
return struct.pack(">IIBBBBB", 256, 256, bit_depth, color_type, 0, 0, interlace)
def raw_rgb() -> bytes:
return b"".join(b"\x00" + b"\x00\x00\x00" * 256 for _ in range(256))
def rewrite_crc(data: bytearray) -> bytes:
data[-4:] = struct.pack("<I", zlib.crc32(data[:-4]))
return bytes(data)
def put_tile(source: Path, z: int | str, x: int | str, y: int | str, data: bytes | None = None) -> Path:
path = source / str(z) / str(x) / f"{y}.png"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(png() if data is None else data)
return path
def metadata(**changes: str) -> dict[str, str]:
values = {
"pack_id": "local-pack",
"name": "Local Pack",
"attribution": "Example Maps contributors",
"source": "user-supplied XYZ export",
"license": "CC-BY-4.0",
}
values.update(changes)
return values
def make_rectangle(source: Path) -> None:
for z, xs, ys in ((1, range(2), range(2)), (2, range(1, 3), range(1, 3))):
for x in xs:
for y in ys:
put_tile(source, z, x, y)
def digest_tree(path: Path) -> str:
digest = hashlib.sha256()
for item in sorted(path.rglob("*"), key=lambda value: value.as_posix()):
digest.update(item.relative_to(path).as_posix().encode("ascii"))
if item.is_file():
digest.update(item.read_bytes())
return digest.hexdigest()
def test_valid_pack_is_deterministic_and_independently_validated(tmp_path: Path) -> None:
tool = load_tool()
source = tmp_path / "xyz"
make_rectangle(source)
first = tool.build_map_pack(source, tmp_path / "sd-a", **metadata())
second = tool.build_map_pack(source, tmp_path / "sd-b", **metadata())
assert first == tmp_path / "sd-a/pyxis-map/packs/local-pack"
assert (first / "manifest.pmp").read_bytes() == (second / "manifest.pmp").read_bytes()
assert digest_tree(first) == digest_tree(second)
parsed = tool.validate_pack(first)
assert parsed["tile_count"] == 8
assert parsed["min_zoom"] == 1
assert parsed["max_zoom"] == 2
assert (first / "tiles/2/2/2.png").read_bytes() == png()
def test_manifest_matches_committed_153_byte_cpp_fixture() -> None:
tool = load_tool()
fixture = bytes.fromhex(
"504d504b0100100099000000000000000c776573742d636f6173745f310a576573"
"7420436f617374194578616d706c65204d61707320636f6e7472696275746f7273"
"0d6c6f63616c2d6578616d706c650943432d42592d342e300203020c0000000201"
"010000000200000001000000020000000000000000000000030204000000050000"
"0000000000010000000600000007000000d55ffd67"
)
extents = [
tool.ZoomExtent(2, ((1, 2),), 1, 2),
tool.ZoomExtent(3, ((0, 1), (6, 7)), 4, 5),
]
actual = tool.serialize_manifest(
pack_id="west-coast_1",
name="West Coast",
attribution="Example Maps contributors",
source="local-example",
license="CC-BY-4.0",
extents=extents,
tile_count=12,
)
assert len(fixture) == 153
assert actual == fixture
def test_sparse_xyz_tree_builds_version_two_exact_row_spans(tmp_path: Path) -> None:
tool = load_tool()
source = tmp_path / "xyz"
for x, y in ((1, 1), (1, 2), (2, 1)):
put_tile(source, 2, x, y)
built = tool.build_map_pack(source, tmp_path / "sd", sparse=True, **metadata())
parsed = tool.validate_pack(built)
assert parsed["format_version"] == 2
assert parsed["tile_count"] == 3
assert parsed["row_spans"] == [
tool.RowSpan(2, 1, 1, 2),
tool.RowSpan(2, 2, 1, 1),
]
def test_disjoint_rows_require_explicit_sparse_or_antimeridian_mode(tmp_path: Path) -> None:
tool = load_tool()
source = tmp_path / "xyz"
for x in (0, 1, 6, 7):
for y in (3, 4):
put_tile(source, 3, x, y)
with pytest.raises(tool.PackError, match="incomplete rectangle"):
tool.build_map_pack(source, tmp_path / "default", **metadata())
sparse = tool.build_map_pack(source, tmp_path / "sd-a", sparse=True, **metadata())
sparse_manifest = tool.validate_pack(sparse)
assert sparse_manifest["format_version"] == 2
assert len(sparse_manifest["row_spans"]) == 4
built = tool.build_map_pack(source, tmp_path / "sd-b", antimeridian_zooms={3}, **metadata())
assert tool.validate_pack(built)["format_version"] == 1
assert tool.validate_pack(built)["extents"][0].intervals == ((0, 1), (6, 7))
@pytest.mark.parametrize("bad_id", ["../escape", "Bad", "has/slash", "", "a" * 32])
def test_pack_id_grammar_rejects_traversal_and_noncanonical_ids(tmp_path: Path, bad_id: str) -> None:
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, 0, 0, 0)
with pytest.raises(tool.PackError, match="pack ID"):
tool.build_map_pack(source, tmp_path / "sd", **metadata(pack_id=bad_id))
@pytest.mark.parametrize(
("parts", "message"),
[(('01', '0', '0'), "noncanonical"), (('1', '+0', '0'), "noncanonical"), (('1', '0', '00'), "noncanonical")],
)
def test_noncanonical_xyz_names_are_rejected(tmp_path: Path, parts: tuple[str, str, str], message: str) -> None:
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, *parts)
with pytest.raises(tool.PackError, match=message):
tool.build_map_pack(source, tmp_path / "sd", **metadata())
def test_unexpected_files_and_duplicate_colliding_keys_are_rejected(tmp_path: Path) -> None:
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, 1, 0, 0)
put_tile(source, "1", "0", "00")
with pytest.raises(tool.PackError, match="noncanonical|duplicate"):
tool.build_map_pack(source, tmp_path / "sd", **metadata())
def test_symlinks_are_rejected_without_following_them(tmp_path: Path) -> None:
tool = load_tool()
source = tmp_path / "xyz"
outside = tmp_path / "outside.png"
outside.write_bytes(png())
(source / "0/0").mkdir(parents=True)
(source / "0/0/0.png").symlink_to(outside)
with pytest.raises(tool.PackError, match="symlink"):
tool.build_map_pack(source, tmp_path / "sd", **metadata())
@pytest.mark.parametrize(
("data", "message"),
[
(b"not png", "PNG"),
(b"\x89PNG\r\n\x1a\n" + b"\0" * 40, "IHDR"),
(png()[:33], "PNG structure"),
(png(255, 256), "256x256"),
],
)
def test_invalid_png_signature_ihdr_and_dimensions_are_rejected(tmp_path: Path, data: bytes, message: str) -> None:
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, 0, 0, 0, data)
with pytest.raises(tool.PackError, match=message):
tool.build_map_pack(source, tmp_path / "sd", **metadata())
@pytest.mark.parametrize("z,x,y", [(23, 0, 0), (1, 2, 0), (1, 0, 2)])
def test_xyz_bounds_are_enforced(tmp_path: Path, z: int, x: int, y: int) -> None:
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, z, x, y)
with pytest.raises(tool.PackError, match="range"):
tool.build_map_pack(source, tmp_path / "sd", **metadata())
def test_tile_count_and_total_byte_quotas_are_enforced(tmp_path: Path) -> None:
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, 0, 0, 0)
with pytest.raises(tool.PackError, match="tile count"):
tool.build_map_pack(source, tmp_path / "count", max_tiles=0, **metadata())
with pytest.raises(tool.PackError, match="total bytes"):
tool.build_map_pack(source, tmp_path / "bytes", max_bytes=len(png()) - 1, **metadata())
with pytest.raises(tool.PackError, match="hard limit"):
tool.build_map_pack(source, tmp_path / "count-hard", max_tiles=tool.DEFAULT_MAX_TILES + 1, **metadata())
with pytest.raises(tool.PackError, match="hard limit"):
tool.build_map_pack(source, tmp_path / "bytes-hard", max_bytes=tool.DEFAULT_MAX_BYTES + 1, **metadata())
@pytest.mark.parametrize("field", ["name", "attribution", "source", "license"])
def test_required_legal_and_name_metadata(field: str, tmp_path: Path) -> None:
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, 0, 0, 0)
with pytest.raises(tool.PackError, match=field):
tool.build_map_pack(source, tmp_path / "sd", **metadata(**{field: ""}))
def test_existing_output_is_refused_without_modification(tmp_path: Path) -> None:
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, 0, 0, 0)
target = tmp_path / "sd/pyxis-map/packs/local-pack"
target.mkdir(parents=True)
marker = target / "keep"
marker.write_text("unchanged")
with pytest.raises(tool.PackError, match="already exists"):
tool.build_map_pack(source, tmp_path / "sd", **metadata())
assert marker.read_text() == "unchanged"
def test_temporary_output_is_cleaned_after_failure(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, 0, 0, 0)
def fail_copy(*_args, **_kwargs):
raise OSError("injected copy failure")
monkeypatch.setattr(tool, "copy_tile", fail_copy)
with pytest.raises(OSError, match="injected"):
tool.build_map_pack(source, tmp_path / "sd", **metadata())
packs = tmp_path / "sd/pyxis-map/packs"
assert not (packs / "local-pack").exists()
assert not list(packs.glob(".local-pack.tmp-*"))
def test_cli_builds_local_tree_and_has_no_network_fetch_code(tmp_path: Path) -> None:
source = tmp_path / "xyz"
put_tile(source, 0, 0, 0)
result = subprocess.run(
[sys.executable, os.fspath(TOOL), os.fspath(source), os.fspath(tmp_path / "sd"),
"--pack-id", "cli-pack", "--name", "CLI Pack", "--attribution", "Example contributors",
"--source", "local export", "--license", "CC-BY-4.0"],
check=False, capture_output=True, text=True,
)
assert result.returncode == 0, result.stderr
assert (tmp_path / "sd/pyxis-map/packs/cli-pack/manifest.pmp").is_file()
source_text = TOOL.read_text(encoding="utf-8").lower()
forbidden = ("urllib", "requests", "httpx", "aiohttp", "socket", "http://", "https://")
assert not [token for token in forbidden if token in source_text]
@pytest.mark.parametrize("level", ["zoom", "x"])
def test_intermediate_source_symlink_swap_is_rejected(tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
level: str) -> None:
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, 0, 0, 0)
outside = tmp_path / "outside"
put_tile(outside, 0, 0, 0)
real_discover = tool._discover_tiles_fd
def swapped_discover(*args, **kwargs):
result = real_discover(*args, **kwargs)
victim = source / "0" if level == "zoom" else source / "0/0"
victim.rename(victim.with_name(victim.name + "-saved"))
target = outside / "0" if level == "zoom" else outside / "0/0"
victim.symlink_to(target, target_is_directory=True)
return result
monkeypatch.setattr(tool, "_discover_tiles_fd", swapped_discover)
with pytest.raises(tool.PackError, match="changed|symlink"):
tool.build_map_pack(source, tmp_path / "sd", **metadata())
assert not (tmp_path / "sd/pyxis-map/packs/local-pack").exists()
@pytest.mark.parametrize("component", ["pyxis-map", "packs"])
def test_intermediate_output_symlink_swap_never_publishes_outside(tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
component: str) -> None:
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, 0, 0, 0)
output = tmp_path / "sd"
outside = tmp_path / "outside"
outside.mkdir()
real_verify = tool._verify_output_chain
def swapped_verify(*args, **kwargs):
victim = output / component if component == "pyxis-map" else output / "pyxis-map/packs"
victim.rename(victim.with_name(victim.name + "-saved"))
victim.symlink_to(outside, target_is_directory=True)
return real_verify(*args, **kwargs)
monkeypatch.setattr(tool, "_verify_output_chain", swapped_verify)
with pytest.raises(tool.PackError, match="output directory changed"):
tool.build_map_pack(source, output, **metadata())
assert not (outside / "local-pack").exists()
@pytest.mark.parametrize("race", ["replace", "grow", "truncate", "mutate", "fifo", "symlink"])
def test_validation_to_copy_races_fail_closed(tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
race: str) -> None:
tool = load_tool()
source = tmp_path / "xyz"
tile = put_tile(source, 0, 0, 0)
original_size = tile.stat().st_size
real_discover = tool._discover_tiles_fd
def raced_discover(*args, **kwargs):
result = real_discover(*args, **kwargs)
if race == "replace":
replacement = tile.with_suffix(".replacement")
replacement.write_bytes(png())
os.replace(replacement, tile)
elif race == "grow":
with tile.open("ab") as stream:
stream.write(b"x" * 4096)
elif race == "truncate":
tile.write_bytes(png()[:-12])
elif race == "mutate":
data = bytearray(tile.read_bytes())
data[-1] ^= 1
tile.write_bytes(data)
elif race == "fifo":
tile.unlink()
os.mkfifo(tile)
else:
outside = tmp_path / "outside.png"
outside.write_bytes(png())
tile.unlink()
tile.symlink_to(outside)
return result
monkeypatch.setattr(tool, "_discover_tiles_fd", raced_discover)
with pytest.raises(tool.PackError, match="changed|regular file|symlink|quota|PNG"):
tool.build_map_pack(source, tmp_path / "sd", max_bytes=original_size, **metadata())
def test_quota_is_checked_before_any_tile_read(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
tool = load_tool()
source = tmp_path / "xyz"
tile = put_tile(source, 0, 0, 0)
with tile.open("ab") as stream:
stream.truncate(32 * 1024 * 1024)
reads = 0
real_read = os.read
def tracked_read(fd: int, size: int) -> bytes:
nonlocal reads
reads += 1
return real_read(fd, size)
monkeypatch.setattr(tool.os, "read", tracked_read)
with pytest.raises(tool.PackError, match="total bytes"):
tool.build_map_pack(source, tmp_path / "sd", max_bytes=1024, **metadata())
assert reads == 0
def test_entry_visit_cap_rejects_before_unbounded_enumeration(tmp_path: Path) -> None:
tool = load_tool()
source = tmp_path / "xyz"
source.mkdir()
for index in range(tool.MAX_VISITED_ENTRIES_BASE + 1):
(source / f"junk-{index}").mkdir()
with pytest.raises(tool.PackError, match="visited-entry quota|noncanonical"):
tool.discover_tiles(source, max_tiles=0, max_bytes=0)
def test_atomic_noreplace_unsupported_fails_closed(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, 0, 0, 0)
monkeypatch.setattr(tool, "_renameat2", None)
with pytest.raises(tool.PackError, match="atomic no-replace unsupported"):
tool.build_map_pack(source, tmp_path / "sd", **metadata())
assert not (tmp_path / "sd/pyxis-map/packs/local-pack").exists()
def test_post_rename_fsync_reports_published_durability_uncertain(tmp_path: Path,
monkeypatch: pytest.MonkeyPatch) -> None:
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, 0, 0, 0)
real_rename = tool._rename_noreplace
def rename_then_break_fsync(*args, **kwargs):
real_rename(*args, **kwargs)
monkeypatch.setattr(tool.os, "fsync", lambda _fd: (_ for _ in ()).throw(OSError("fsync fault")))
monkeypatch.setattr(tool, "_rename_noreplace", rename_then_break_fsync)
with pytest.raises(tool.PublishedDurabilityError) as caught:
tool.build_map_pack(source, tmp_path / "sd", **metadata())
assert caught.value.published is True
assert caught.value.target == tmp_path / "sd/pyxis-map/packs/local-pack"
assert caught.value.target.is_dir()
@pytest.mark.parametrize("data", [
png_chunks([(b"IHDR", ihdr()), (b"ABCD", b""), (b"IDAT", zlib.compress(raw_rgb())), (b"IEND", b"")]),
png_chunks([(b"IHDR", ihdr()), (b"abcd", b""), (b"IDAT", zlib.compress(raw_rgb())), (b"IEND", b"")]),
png_chunks([(b"IHDR", ihdr()), (b"PLTE", b"\x00\x00\x00" * 257),
(b"IDAT", zlib.compress(raw_rgb())), (b"IEND", b"")]),
png_chunks([(b"IHDR", ihdr()), (b"IDAT", b"invalid-deflate"), (b"IEND", b"")]),
png_chunks([(b"IHDR", ihdr()), (b"IDAT", zlib.compress(raw_rgb())[:-2]), (b"IEND", b"")]),
png_chunks([(b"IHDR", ihdr()), (b"IDAT", zlib.compress(raw_rgb()) + b"trailing"), (b"IEND", b"")]),
png_chunks([(b"IHDR", ihdr()), (b"IDAT", zlib.compress(raw_rgb())[:10]), (b"tEXt", b"x"),
(b"IDAT", zlib.compress(raw_rgb())[10:]), (b"IEND", b"")]),
png_chunks([(b"IHDR", ihdr(color_type=3)), (b"IDAT", zlib.compress(b"\x00" * (257 * 256))),
(b"IEND", b"")]),
png_chunks([(b"IHDR", ihdr(color_type=3)), (b"PLTE", b"\x00\x00\x00"),
(b"IDAT", zlib.compress(b"".join(b"\x00" + b"\xff" * 256 for _ in range(256)))),
(b"IEND", b"")]),
png_chunks([(b"IHDR", ihdr(color_type=2)), (b"PLTE", b"\x00\x00\x00"),
(b"PLTE", b"\x00\x00\x00"), (b"IDAT", zlib.compress(raw_rgb())), (b"IEND", b"")]),
png_chunks([(b"IHDR", ihdr(color_type=3)), (b"IDAT", zlib.compress(b"\x00" * (257 * 256))),
(b"PLTE", b"\x00\x00\x00"), (b"IEND", b"")]),
png_chunks([(b"IHDR", ihdr()), (b"IDAT", zlib.compress(b"\x05" + raw_rgb()[1:])), (b"IEND", b"")]),
png_chunks([(b"IHDR", ihdr(interlace=1)), (b"IDAT", zlib.compress(raw_rgb())), (b"IEND", b"")]),
])
def test_malformed_png_critical_state_and_deflate_are_rejected(tmp_path: Path, data: bytes) -> None:
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, 0, 0, 0, data)
with pytest.raises(tool.PackError, match="PNG"):
tool.build_map_pack(source, tmp_path / "sd", **metadata())
def test_python_manifest_mutations_match_cpp_validation_rules() -> None:
tool = load_tool()
valid = bytearray(tool.serialize_manifest(
**metadata(), extents=[tool.ZoomExtent(0, ((0, 0),), 0, 0)], tile_count=1,
))
values = metadata()
strings_end = 16 + sum(1 + len(values[field]) for field in
("pack_id", "name", "attribution", "source", "license"))
extent = strings_end + 7
mutations = []
bad = bytearray(valid); bad[extent + 1] = 3; mutations.append(bad)
bad = bytearray(valid); struct.pack_into("<I", bad, extent + 18, 1); mutations.append(bad)
bad = bytearray(valid); bad[strings_end] = 1; mutations.append(bad)
bad = bytearray(valid); struct.pack_into("<I", bad, strings_end + 3, 2); mutations.append(bad)
for mutation in mutations:
with pytest.raises(tool.PackError):
tool.parse_manifest(rewrite_crc(mutation))
with pytest.raises(tool.PackError):
tool.serialize_manifest(**metadata(), extents=[tool.ZoomExtent(1, ((1, 0),), 0, 0)], tile_count=999)
def test_documented_cli_uses_supported_ascii_metadata(tmp_path: Path) -> None:
tool = load_tool()
docs = (ROOT / "docs/offline-map-packs.md").read_text(encoding="utf-8")
assert 'Map data (c) Example contributors' in docs
source = tmp_path / "my-xyz"
put_tile(source, 0, 0, 0)
assert tool.main([
os.fspath(source), os.fspath(tmp_path / "sd"), "--pack-id", "regional-map",
"--name", "Regional Map", "--attribution", "Map data (c) Example contributors",
"--source", "Local export supplied by the user", "--license", "CC-BY-4.0",
]) == 0
def test_write_all_handles_partial_write_and_rejects_zero_progress(tmp_path: Path,
monkeypatch: pytest.MonkeyPatch) -> None:
tool = load_tool()
destination = tmp_path / "output"
descriptor = os.open(destination, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
real_write = os.write
try:
monkeypatch.setattr(tool.os, "write", lambda fd, data: real_write(fd, data[:max(1, len(data) // 2)]))
tool._write_all(descriptor, b"complete payload", "test")
finally:
os.close(descriptor)
assert destination.read_bytes() == b"complete payload"
monkeypatch.setattr(tool.os, "write", lambda _fd, _data: 0)
with pytest.raises(OSError, match="short write"):
tool._write_all(-1, b"x", "test")
def test_output_relocation_between_verify_and_rename_is_rolled_back(tmp_path: Path,
monkeypatch: pytest.MonkeyPatch) -> None:
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, 0, 0, 0)
output = tmp_path / "sd"
outside = tmp_path / "outside"
outside.mkdir()
real_rename = tool._rename_noreplace
def relocate_then_rename(*args, **kwargs):
packs = output / "pyxis-map/packs"
moved = outside / "moved-packs"
packs.rename(moved)
real_rename(*args, **kwargs)
monkeypatch.setattr(tool, "_rename_noreplace", relocate_then_rename)
with pytest.raises(tool.PackError, match="changed during publication"):
tool.build_map_pack(source, output, **metadata())
assert not (outside / "moved-packs/local-pack").exists()
assert not (output / "pyxis-map/packs/local-pack").exists()
def test_post_commit_close_failure_is_typed_as_published(tmp_path: Path,
monkeypatch: pytest.MonkeyPatch) -> None:
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, 0, 0, 0)
output = tmp_path / "sd"
real_verify = tool._verify_output_chain
real_close = tool.os.close
calls = 0
def arm_after_second_verify(*args, **kwargs):
nonlocal calls
calls += 1
result = real_verify(*args, **kwargs)
if calls == 2:
failed = False
def fail_once(fd: int):
nonlocal failed
if not failed:
failed = True
raise OSError("close fault")
return real_close(fd)
monkeypatch.setattr(tool.os, "close", fail_once)
return result
monkeypatch.setattr(tool, "_verify_output_chain", arm_after_second_verify)
with pytest.raises(tool.PublishedDurabilityError) as caught:
tool.build_map_pack(source, output, **metadata())
assert caught.value.published is True
assert (output / "pyxis-map/packs/local-pack").is_dir()
# --- PMPK v3 (indexless) + PMAS v3 (active map set) -------------------------
def test_indexless_manifest_matches_js_flasher_byte_for_byte() -> None:
tool = load_tool()
fixture = bytes.fromhex(
"504d504b03001000d0000000000000001f776f726c642d6f736d2d6272696768742d7a30"
"2d7a392d323032363038303116576f726c64204f534d20427269676874207a302d7a392f"
"286329204f70656e4d617054696c657320286329204f70656e5374726565744d61702063"
"6f6e7472696275746f7273274f7865642773204d61702054696c6520446f776e6c6f6164"
"657220284f534d2042726967687429264f534d204f44624c3b207374796c652043432d"
"42592d342e302f4253442d332d436c6175736500096f460100e9a28313"
)
actual = tool.serialize_indexless_manifest(
pack_id="world-osm-bright-z0-z9-20260801",
name="World OSM Bright z0-z9",
attribution="(c) OpenMapTiles (c) OpenStreetMap contributors",
source="Oxed's Map Tile Downloader (OSM Bright)",
license="OSM ODbL; style CC-BY-4.0/BSD-3-Clause",
min_zoom=0,
max_zoom=9,
tile_count=83567,
)
assert actual == fixture
parsed = tool.parse_manifest(actual)
assert parsed["format_version"] == 3
assert parsed["tile_count"] == 83567
assert parsed["min_zoom"] == 0
assert parsed["max_zoom"] == 9
assert parsed["extents"] == [] and parsed["row_spans"] == []
def test_indexless_manifest_rejects_bad_ranges() -> None:
tool = load_tool()
base = dict(pack_id="p", name="P", attribution="A", source="S", license="L")
with pytest.raises(tool.PackError, match="range or tile count"):
tool.serialize_indexless_manifest(min_zoom=3, max_zoom=1, tile_count=1, **base)
with pytest.raises(tool.PackError, match="range or tile count"):
tool.serialize_indexless_manifest(min_zoom=0, max_zoom=23, tile_count=1, **base)
with pytest.raises(tool.PackError, match="range or tile count"):
tool.serialize_indexless_manifest(min_zoom=0, max_zoom=2, tile_count=0, **base)
def test_pmas_v3_matches_js_flasher_byte_for_byte() -> None:
tool = load_tool()
fixture = bytes.fromhex(
"504d415303007500070000000a6f736d2d6272696768742f286329204f70656e4d6170"
"54696c657320286329204f70656e5374726565744d617020636f6e7472696275746f72"
"73021f776f726c642d6f736d2d6272696768742d7a302d7a392d323032363038303108"
"76697267696e69619e5ec1bc"
)
actual = tool.encode_active_map_set(
generation=7,
map_set_id="osm-bright",
attribution="(c) OpenMapTiles (c) OpenStreetMap contributors",
pack_ids=["world-osm-bright-z0-z9-20260801", "virginia"],
)
assert actual == fixture
parsed = tool.parse_active_map_set(actual)
assert parsed["format_version"] == 3
assert parsed["generation"] == 7
assert parsed["map_set_id"] == "osm-bright"
assert parsed["packs"] == ["world-osm-bright-z0-z9-20260801", "virginia"]
def test_pmas_rejects_duplicates_and_zero_generation() -> None:
tool = load_tool()
with pytest.raises(tool.PackError, match="duplicate"):
tool.encode_active_map_set(generation=1, map_set_id="s", attribution="a",
pack_ids=["p", "p"])
with pytest.raises(tool.PackError, match="generation"):
tool.encode_active_map_set(generation=0, map_set_id="s", attribution="a",
pack_ids=["p"])
with pytest.raises(tool.PackError, match="packs"):
tool.encode_active_map_set(generation=1, map_set_id="s", attribution="a",
pack_ids=[])
def test_style_install_emits_indexless_v3_and_is_validated(tmp_path: Path) -> None:
tool = load_tool()
source = tmp_path / "xyz"
# Deliberately sparse: not a rectangle and not antimeridian, so v1 would fail.
for x, y in ((0, 0), (0, 3), (1, 1), (2, 2), (3, 0)):
put_tile(source, 2, x, y)
policy = tool.STYLE_POLICIES["osm-bright"]
built = tool.build_map_pack(
source, tmp_path / "sd", pack_id="world-osm-bright-z0-z9-20260801",
name="World OSM Bright z0-z9",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"], style="osm-bright",
)
parsed = tool.validate_pack(built)
assert parsed["format_version"] == 3
assert parsed["tile_count"] == 5
assert parsed["min_zoom"] == 2 and parsed["max_zoom"] == 2
def test_style_policy_mismatch_is_rejected(tmp_path: Path) -> None:
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, 0, 0, 0)
with pytest.raises(tool.PackError, match="style policy"):
tool.build_map_pack(source, tmp_path / "sd", pack_id="p", name="P",
attribution="wrong", source="wrong", license="wrong",
style="osm-bright")
def test_sparse_beyond_span_cap_falls_back_to_indexless_v3(tmp_path: Path) -> None:
tool = load_tool()
source = tmp_path / "xyz"
# Every zoom is a single column (x = 0) with an even-y (noncontiguous)
# set, so no zoom is a rectangle and the sparse path is required:
# zoom 10: 512 tiles (even y), 512 single-tile row spans
# zoom 9: 256 tiles (even y), 256 spans
# 768 row spans > MAX_ROW_SPANS (512), so v2 cannot represent it and the
# builder must fall back to an indexless v3 pack.
for y in range(0, 1024, 2):
put_tile(source, 10, 0, y)
for y in range(0, 512, 2):
put_tile(source, 9, 0, y)
built = tool.build_map_pack(source, tmp_path / "sd", sparse=True, **metadata())
parsed = tool.validate_pack(built)
assert parsed["format_version"] == 3
assert parsed["tile_count"] == 768
assert parsed["min_zoom"] == 9 and parsed["max_zoom"] == 10
def test_activation_publishes_pmas_and_is_idempotent_reorder(tmp_path: Path) -> None:
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, 1, 0, 0)
policy = tool.STYLE_POLICIES["osm-bright"]
sd = tmp_path / "sd"
tool.build_map_pack(source, sd, pack_id="pack-a", name="A",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"], style="osm-bright", activate=True)
active = (sd / "pyxis-map/active-pack.0").read_bytes()
record = tool.decode_active_selection(active)
assert record["format_version"] == 3
assert record["generation"] == 1
assert record["map_set_id"] == "osm-bright"
assert record["packs"] == ["pack-a"]
style_record = (sd / "pyxis-map/map-sets/osm-bright.pmas").read_bytes()
assert style_record == active
# A second pack of the same style moves to the front, generation bumps.
source2 = tmp_path / "xyz2"
put_tile(source2, 1, 1, 1)
tool.build_map_pack(source2, sd, pack_id="pack-b", name="B",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"], style="osm-bright", activate=True)
# Generation 2 goes into the other slot.
active2 = (sd / "pyxis-map/active-pack.1").read_bytes()
record2 = tool.decode_active_selection(active2)
assert record2["generation"] == 2
assert record2["packs"] == ["pack-b", "pack-a"]
# The old slot keeps its record; the firmware picks the higher generation.
assert (sd / "pyxis-map/active-pack.0").read_bytes() == active
assert (sd / "pyxis-map/map-sets/osm-bright.pmas").read_bytes() == active2
def test_activation_requires_style(tmp_path: Path) -> None:
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, 0, 0, 0)
with pytest.raises(tool.PackError, match="requires --style"):
tool.build_map_pack(source, tmp_path / "sd", **metadata(), activate=True)
def _seed_style_pack(tool, tmp_path: Path, sd: Path, index: int, pack_id: str) -> None:
source = tmp_path / f"seed{index}"
put_tile(source, 1, index % 2, index % 2)
policy = tool.STYLE_POLICIES["osm-bright"]
tool.build_map_pack(source, sd, pack_id=pack_id, name=f"Seed {index}",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"], style="osm-bright", activate=True)
def test_interrupted_activation_replacement_keeps_previous_records(tmp_path: Path,
monkeypatch: pytest.MonkeyPatch) -> None:
# Simulating a crash between the temp write and its rename must never
# truncate the previous style record or active slot (P1: non-atomic
# O_TRUNC replacement corrupts style discovery).
tool = load_tool()
policy = tool.STYLE_POLICIES["osm-bright"]
source = tmp_path / "xyz"
put_tile(source, 1, 0, 0)
sd = tmp_path / "sd"
tool.build_map_pack(source, sd, pack_id="pack-a", name="A",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"], style="osm-bright", activate=True)
style_path = sd / "pyxis-map/map-sets/osm-bright.pmas"
slot_path = sd / "pyxis-map/active-pack.0"
style_before = style_path.read_bytes()
slot_before = slot_path.read_bytes()
real_renameat2 = tool._renameat2
def interrupted_renameat2(fd, src, dst, dirfd, flags):
# Interrupt only the record replacement (flags 0); pack staging
# (RENAME_NOREPLACE, flags 1) must proceed so the failure lands
# after publication, exactly like a crash mid-activation.
if flags == 0:
raise OSError("injected interruption before rename")
return real_renameat2(fd, src, dst, dirfd, flags)
monkeypatch.setattr(tool, "_renameat2", interrupted_renameat2)
source2 = tmp_path / "xyz2"
put_tile(source2, 1, 1, 1)
with pytest.raises(tool.PackError, match="cannot atomically replace"):
tool.build_map_pack(source2, sd, pack_id="pack-b", name="B",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"], style="osm-bright", activate=True)
# Previous records are byte-identical; no staging debris is left behind.
assert style_path.read_bytes() == style_before
assert slot_path.read_bytes() == slot_before
assert tool.decode_active_selection(style_before)["packs"] == ["pack-a"]
assert not list((sd / "pyxis-map/map-sets").glob(".osm-bright.pmas.tmp-*"))
assert not list((sd / "pyxis-map").glob("active-pack.1.tmp-*"))
# The just-published pack stays valid on disk; the failure is confined to
# the record publication, which a later run can repair.
assert (sd / "pyxis-map/packs/pack-b/manifest.pmp").is_file()
def test_activation_ninth_distinct_pack_fails_before_publishing(tmp_path: Path) -> None:
# The firmware rejects PMAS records with more than MAX_PACKS entries, so
# the writer must validate the inherited composition before the new pack
# is permanently published -- otherwise the failure orphans the pack
# (P1: activation failure leaves orphan pack).
tool = load_tool()
assert tool.MAX_ACTIVE_PACKS == 8
policy = tool.STYLE_POLICIES["osm-bright"]
sd = tmp_path / "sd"
for index in range(tool.MAX_ACTIVE_PACKS):
_seed_style_pack(tool, tmp_path, sd, index, f"seed-{index}")
style_path = sd / "pyxis-map/map-sets/osm-bright.pmas"
slots = [sd / f"pyxis-map/active-pack.{i}" for i in range(2)]
style_before = style_path.read_bytes()
slots_before = [slot.read_bytes() for slot in slots]
assert tool.decode_active_selection(style_before)["generation"] == tool.MAX_ACTIVE_PACKS
source = tmp_path / "ninth"
put_tile(source, 1, 0, 1)
with pytest.raises(tool.PackError, match="pack limit exceeded"):
tool.build_map_pack(source, sd, pack_id="ninth-pack", name="Ninth",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"], style="osm-bright", activate=True)
# No record was rewritten and the rejected pack was not published.
assert style_path.read_bytes() == style_before
assert [slot.read_bytes() for slot in slots] == slots_before
assert not (sd / "pyxis-map/packs/ninth-pack").exists()
def test_activation_conflicting_slots_rejected_before_publishing(tmp_path: Path) -> None:
# Two active slots at the same generation with different content is a
# conflict activate_map_set refuses -- the preflight must refuse it too,
# before the new pack is permanently published (Greploop round 2).
tool = load_tool()
policy = tool.STYLE_POLICIES["osm-bright"]
sd = tmp_path / "sd"
(sd / "pyxis-map").mkdir(parents=True)
slot0 = tool.encode_active_map_set(generation=5, map_set_id="osm-bright",
attribution=policy["attribution"],
pack_ids=["existing-a"])
slot1 = tool.encode_active_map_set(generation=5, map_set_id="osm-bright",
attribution=policy["attribution"],
pack_ids=["existing-b"])
(sd / "pyxis-map/active-pack.0").write_bytes(slot0)
(sd / "pyxis-map/active-pack.1").write_bytes(slot1)
source = tmp_path / "xyz"
put_tile(source, 1, 0, 0)
with pytest.raises(tool.PackError, match="conflicting active map-set records"):
tool.build_map_pack(source, sd, pack_id="conflict-pack", name="C",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"], style="osm-bright", activate=True)
# The conflicting slots are untouched and nothing was published.
assert (sd / "pyxis-map/active-pack.0").read_bytes() == slot0
assert (sd / "pyxis-map/active-pack.1").read_bytes() == slot1
assert not (sd / "pyxis-map/packs/conflict-pack").exists()
def test_activation_exhausted_generation_rejected_before_publishing(tmp_path: Path) -> None:
# When the highest active slot is already at the maximum generation,
# activate_map_set rejects the activation. The preflight must reject it
# too, BEFORE the new pack is published -- otherwise the failure orphans
# the pack (Greploop round 5).
tool = load_tool()
policy = tool.STYLE_POLICIES["osm-bright"]
sd = tmp_path / "sd"
(sd / "pyxis-map").mkdir(parents=True)
exhausted = tool.encode_active_map_set(generation=0xFFFFFFFF,
map_set_id="osm-bright",
attribution=policy["attribution"],
pack_ids=["existing-a"])
(sd / "pyxis-map/active-pack.0").write_bytes(exhausted)
source = tmp_path / "xyz"
put_tile(source, 1, 0, 0)
with pytest.raises(tool.PackError, match="generation is exhausted"):
tool.build_map_pack(source, sd, pack_id="exhausted-pack", name="E",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"], style="osm-bright", activate=True)
# The exhausted slot is untouched and nothing was published.
assert (sd / "pyxis-map/active-pack.0").read_bytes() == exhausted
assert not (sd / "pyxis-map/packs/exhausted-pack").exists()
def test_interrupted_activation_retry_converges_records(tmp_path: Path) -> None:
# A run interrupted between the style-record commit and the slot-record
# commit leaves the style record current but the active slot not yet
# committed. Both recovery paths then converge forward: the CLI retry
# (this test) re-runs activation against the verified pack, and the
# firmware's device-side style re-activation
# (MapStyleCatalog::activate) resequences the current style record into
# the slot. The style record must never be older than the slot, because
# the device-side path would otherwise reseed the slot from stale
# composition and drop a newly installed pack (Greploop rounds 3-6:
# the web flasher's style-first order).
tool = load_tool()
policy = tool.STYLE_POLICIES["osm-bright"]
sd = tmp_path / "sd"
(sd / "pyxis-map").mkdir(parents=True)
source = tmp_path / "xyz"
put_tile(source, 1, 0, 0)
real_renameat2 = tool._renameat2
interrupted = {"done": False}
def interrupted_renameat2(oldfd, oldpath, newfd, newpath, flags):
# Interrupt only the slot-record replacement (flags 0, newpath an
# active-pack slot). Pack staging (flags 1) and the style-record
# commit (flags 0, newpath the .pmas) must proceed so the failure
# lands between the two record commits: style record current, slot
# not yet committed.
if not interrupted["done"] and flags == 0 and newpath.startswith(b"active-pack"):
interrupted["done"] = True
raise OSError("injected interruption between record commits")
return real_renameat2(oldfd, oldpath, newfd, newpath, flags)
monkeypatch = pytest.MonkeyPatch()
monkeypatch.setattr(tool, "_renameat2", interrupted_renameat2)
try:
with pytest.raises(tool.PackError, match="cannot atomically replace"):
tool.build_map_pack(source, sd, pack_id="resume-pack", name="R",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"], style="osm-bright", activate=True)
finally:
monkeypatch.undo()
# The pack published and the style record committed; the slot did not.
# The style record is current (never older than the slot), so no
# recovery path can reseed the slot from stale data.
assert (sd / "pyxis-map/packs/resume-pack/manifest.pmp").is_file()
style_path = sd / "pyxis-map/map-sets/osm-bright.pmas"
assert style_path.exists()
style = tool.decode_active_selection(style_path.read_bytes())
assert style["packs"][0] == "resume-pack"
assert not (sd / "pyxis-map/active-pack.0").exists()
# Retry with the same source: the pack verifies byte-identical and the
# slot converges to the style record's composition.
result = tool.build_map_pack(source, sd, pack_id="resume-pack", name="R",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"], style="osm-bright", activate=True)
assert result == sd / "pyxis-map/packs/resume-pack"
slot = tool.decode_active_selection((sd / "pyxis-map/active-pack.0").read_bytes())
assert slot == tool.decode_active_selection(style_path.read_bytes())
assert slot["packs"][0] == "resume-pack"
def _flock_holder(path: str) -> subprocess.Popen:
# Spawn a subprocess that takes an exclusive flock on the lock file and
# signals readiness over stdout. This is the only way to exercise a real
# concurrent lock holder: flock is a per-process kernel state, so a fake
# pid in a file cannot hold it.
code = (
"import fcntl, os, sys, time\n"
"fd = os.open(sys.argv[1], os.O_RDWR | os.O_CREAT, 0o644)\n"
"fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)\n"
"sys.stdout.write('ready\\n')\n"
"sys.stdout.flush()\n"
"time.sleep(60)\n"
)
return subprocess.Popen([sys.executable, "-c", code, path],
stdout=subprocess.PIPE, text=True)
def test_concurrent_activation_is_serialized_by_lock(tmp_path: Path) -> None:
# A second builder cannot activate while the first holds the install
# flock; when the holder exits, the kernel releases the lock and the
# build proceeds (Greploop round 3 + round 4: lock must be a real,
# race-free kernel primitive, not a pid file).
tool = load_tool()
policy = tool.STYLE_POLICIES["osm-bright"]
sd = tmp_path / "sd"
(sd / "pyxis-map").mkdir(parents=True)
lock_file = sd / "pyxis-map/.pyxis-install.lock"
source = tmp_path / "xyz"
put_tile(source, 1, 0, 0)
holder = _flock_holder(str(lock_file))
try:
assert holder.stdout.readline().strip() == "ready"
with pytest.raises(tool.PackError, match="already running"):
tool.build_map_pack(source, sd, pack_id="locked-pack", name="L",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"], style="osm-bright", activate=True)
assert not (sd / "pyxis-map/packs/locked-pack").exists()
finally:
holder.terminate()
holder.wait()
# Holder exited: the kernel released the flock, so the build now proceeds.
result = tool.build_map_pack(source, sd, pack_id="locked-pack", name="L",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"], style="osm-bright", activate=True)
assert (sd / "pyxis-map/packs/locked-pack/manifest.pmp").is_file()
assert (sd / "pyxis-map/active-pack.0").is_file()
# The lock file is persistent (never unlinked) -- see _install_lock_at.
assert lock_file.exists()
def test_existing_pack_rejects_different_source(tmp_path: Path) -> None:
# Resume is only allowed for a byte-identical republish; a different
# source must fail before touching the pack or the records.
tool = load_tool()
policy = tool.STYLE_POLICIES["osm-bright"]
sd = tmp_path / "sd"
(sd / "pyxis-map").mkdir(parents=True)
source = tmp_path / "xyz"
put_tile(source, 1, 0, 0)
tool.build_map_pack(source, sd, pack_id="dup-pack", name="D",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"])
pack_before = (sd / "pyxis-map/packs/dup-pack/manifest.pmp").read_bytes()
different = tmp_path / "xyz2"
put_tile(different, 1, 0, 1) # different tile -> different manifest
with pytest.raises(tool.PackError, match="does not match this source"):
tool.build_map_pack(different, sd, pack_id="dup-pack", name="D",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"], style="osm-bright", activate=True)
assert (sd / "pyxis-map/packs/dup-pack/manifest.pmp").read_bytes() == pack_before
assert not (sd / "pyxis-map/map-sets").exists()
def test_decode_selection_handles_v1_and_v2(tmp_path: Path) -> None:
tool = load_tool()
# v1: 48 bytes, magic, version 1, len 48, generation, then id at 12..44
pack = b"v1pack"
v1 = bytearray(b"PMAS")
v1 += bytes([1, 0])
v1 += struct.pack("<H", 48)
v1 += struct.pack("<I", 5)
v1 += bytes([len(pack)])
v1 += pack
v1 += bytes(44 - len(v1))
v1 += struct.pack("<I", zlib.crc32(bytes(v1)))
got = tool.decode_active_selection(bytes(v1))
assert got["format_version"] == 1
assert got["packs"] == ["v1pack"]
assert got["generation"] == 5
# v2: magic, version 2, reserved 0, len, generation, then
# map-set id, attribution, pack count, and one pack with one span.
v2 = bytearray(b"PMAS")
v2 += bytes([2, 0])
body = bytearray()
body += bytes([6]) + b"mapset" # map_set_id (identifier grammar: 3+ chars)
body += bytes([1]) + b"a" # attribution
body += bytes([1]) # pack count
body += bytes([2]) + b"p1" # pack id
body += struct.pack("<H", 1) # span count
body += bytes([1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]) # one span z1 y0 x0..1
total = 12 + len(body) + 4
v2 += struct.pack("<H", total)
v2 += struct.pack("<I", 9)
v2 += body
v2 += struct.pack("<I", zlib.crc32(bytes(v2)))
got2 = tool.decode_active_selection(bytes(v2))
assert got2["format_version"] == 2
assert got2["packs"] == ["p1"]
assert got2["generation"] == 9
def test_unfilter_fast_path_matches_slow_path() -> None:
tool = load_tool()
import random as _random
_random.seed(1234)
# All-None-filter decode must equal the generic predictor loop output.
rows = bytearray()
for _ in range(256):
rows += b"\x00" + bytes(_random.getrandbits(8) for _ in range(768))
fast = tool._unfilter_png_rows(bytes(rows), 768, 3, "fast")
slow = tool._unfilter_png_rows(bytes(rows), 768, 3, "slow")
assert fast == slow
# A mixed-filter image must still take the correct generic path.
mixed = bytearray(rows)
mixed[0] = 2 # Up filter on first row: value == above (zero row) => same pixels
assert tool._unfilter_png_rows(bytes(mixed), 768, 3, "mixed") == fast
def test_metadata_json_is_ignored_but_validated(tmp_path: Path) -> None:
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, 0, 0, 0)
(source / "metadata.json").write_text('{"maxZoom": 0}', encoding="utf-8")
built = tool.build_map_pack(source, tmp_path / "sd", **metadata())
parsed = tool.validate_pack(built)
assert parsed["tile_count"] == 1
assert not (built / "tiles/metadata.json").exists()
bad = tmp_path / "xyz-bad"
put_tile(bad, 0, 0, 0)
(bad / "metadata.json").write_text("not json", encoding="utf-8")
with pytest.raises(tool.PackError, match="metadata.json is not valid JSON"):
tool.build_map_pack(bad, tmp_path / "sd-bad", **metadata())
def test_indexed_png_fast_path_accepts_in_range_and_rejects_out_of_range(tmp_path: Path) -> None:
tool = load_tool()
# 8-bit indexed, 2-entry palette, indices 0/1 only -> in range, must pass.
good = png_chunks([(b"IHDR", ihdr(color_type=3)), (b"PLTE", b"\x00\x00\x00" * 2),
(b"IDAT", zlib.compress(b"".join(b"\x00" + b"\x01\x00" * 128 for _ in range(256)))),
(b"IEND", b"")])
source = tmp_path / "xyz"
put_tile(source, 0, 0, 0, good)
built = tool.build_map_pack(source, tmp_path / "sd", **metadata())
assert tool.validate_pack(built)["tile_count"] == 1
# Same tile but one pixel uses index 5 (>= 2 entries) -> must be rejected.
bad = png_chunks([(b"IHDR", ihdr(color_type=3)), (b"PLTE", b"\x00\x00\x00" * 2),
(b"IDAT", zlib.compress(b"".join(b"\x00" + (b"\x05" + b"\x01" * 255) for _ in range(256)))),
(b"IEND", b"")])
source_bad = tmp_path / "xyz-bad"
put_tile(source_bad, 0, 0, 0, bad)
with pytest.raises(tool.PackError, match="palette index out of range"):
tool.build_map_pack(source_bad, tmp_path / "sd-bad", **metadata())
def test_fresh_foreign_install_marker_is_refused_before_any_publication(
tmp_path: Path) -> None:
# A live cross-producer installer (the web flasher holds a Web Locks
# name, which this process's flock cannot see) is announced by a fresh
# on-disk marker in pyxis-map/. The builder must refuse up front,
# before staging tiles or publishing.
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, 1, 0, 0)
policy = tool.STYLE_POLICIES["osm-bright"]
sd = tmp_path / "sd"
(sd / "pyxis-map").mkdir(parents=True)
(sd / "pyxis-map/.pyxis-installing").write_text(
"PYXI 1 web-deadbeef " + str(int(__import__("time").time() * 1000)))
with pytest.raises(tool.PackError, match="already running"):
tool.build_map_pack(source, sd, pack_id="pack-a", name="A",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"], style="osm-bright", activate=True)
# Nothing was staged, published, or activated: no pack, no
# records. The empty packs/ directory is created before the
# marker check and is harmless (the firmware reads only fixed
# named paths). The foreign marker is left in place: the other
# installer is live.
assert not (sd / "pyxis-map/packs/pack-a").exists()
assert not (sd / "pyxis-map/active-pack.0").exists()
assert not (sd / "pyxis-map/map-sets").exists()
# The foreign marker was not touched: the other installer is live.
assert (sd / "pyxis-map/.pyxis-installing").is_file()
def test_stale_install_marker_is_reclaimed_and_released(tmp_path: Path) -> None:
# An abandoned installer (crashed builder, closed tab) leaves a stale
# marker. The builder reclaims it, installs, and releases it on the
# happy path.
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, 1, 0, 0)
policy = tool.STYLE_POLICIES["osm-bright"]
sd = tmp_path / "sd"
(sd / "pyxis-map").mkdir(parents=True)
stale = int(__import__("time").time() * 1000) - 24 * 3600 * 1000
(sd / "pyxis-map/.pyxis-installing").write_text(f"PYXI 1 cli-{stale:x} {stale}")
tool.build_map_pack(source, sd, pack_id="pack-a", name="A",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"], style="osm-bright", activate=True)
assert (sd / "pyxis-map/packs/pack-a/manifest.pmp").is_file()
assert (sd / "pyxis-map/active-pack.0").is_file()
# The builder released its own marker; nothing is left behind.
assert not (sd / "pyxis-map/.pyxis-installing").exists()
def test_release_never_deletes_a_foreign_marker(tmp_path: Path) -> None:
# If a later installer reclaimed the marker after ours outlasted the
# TTL, release must leave the foreign claim alone.
tool = load_tool()
sd = tmp_path / "sd"
(sd / "pyxis-map").mkdir(parents=True)
pyxis_fd = os.open(str(sd / "pyxis-map"), os.O_RDONLY)
try:
token = tool._acquire_install_marker(pyxis_fd)
# A later installer overwrote our claim.
foreign = "PYXI 1 web-cafebabe " + str(int(__import__("time").time() * 1000))
(sd / "pyxis-map/.pyxis-installing").write_text(foreign)
tool._release_install_marker(pyxis_fd, token)
assert (sd / "pyxis-map/.pyxis-installing").read_text() == foreign
finally:
os.close(pyxis_fd)
def test_commit_time_revalidation_aborts_and_retry_converges(tmp_path: Path) -> None:
# Commit-time revalidation: if a cross-producer installer commits new
# activation state after our records were derived, the publication is
# aborted before any record write. The published pack is kept (device-
# harmless while no record names it) and a retry converges the records.
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, 1, 0, 0)
policy = tool.STYLE_POLICIES["osm-bright"]
sd = tmp_path / "sd"
real_activate = tool.activate_map_set
real_verify = tool._verify_activation_state_at
def racing_activate(pyxis_fd, **kwargs):
# One-shot: fires only on the first (aborted) run.
if raced["fired"]:
real_activate(pyxis_fd, **kwargs)
return
raced["fired"] = True
# Derivation happens inside the real activate_map_set (raw slot and
# style bytes are read there); run it once so the snapshot is
# taken, then simulate a concurrent cross-producer commit landing
# between that read and the commit-time revalidation. A real
# installer commits both records (style first, slot second), so the
# simulated competitor does the same.
real_activate(pyxis_fd, **kwargs)
assert tool._read_selection_at(pyxis_fd, "active-pack.1") is None
record2 = tool.encode_active_map_set(
generation=2, map_set_id="osm-bright",
attribution=policy["attribution"], pack_ids=["other-pack", "raced-pack"])
map_sets_fd = tool._mkdir_open(pyxis_fd, "map-sets")[0]
try:
tool._write_atomic_at(map_sets_fd, "osm-bright.pmas", record2)
tool._write_atomic_at(pyxis_fd, "active-pack.0", record2)
finally:
os.close(map_sets_fd)
# Now run the commit-time revalidation for real: it must see the
# changed slot bytes (our derivation snapshot had both slots empty)
# and abort before any record write.
map_sets_fd = tool._mkdir_open(pyxis_fd, "map-sets")[0]
token = tool._read_selection_at(pyxis_fd, tool._INSTALL_MARKER_NAME).decode("ascii")
try:
real_verify(pyxis_fd, map_sets_fd,
slot_raw=[None, None], style_raw=None,
style_name="osm-bright.pmas", marker_token=token)
raise AssertionError("revalidation accepted a changed slot")
finally:
os.close(map_sets_fd)
raced = {"fired": False}
tool.activate_map_set = racing_activate
try:
with pytest.raises(tool.PackError, match="changed during installation"):
tool.build_map_pack(source, sd, pack_id="pack-a", name="A",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"], style="osm-bright", activate=True)
finally:
tool.activate_map_set = real_activate
# The pack was published but no record was written by this run: the
# card holds exactly the raced installer's consistent gen-2 records
# (style first, slot second -- no split state from this aborted run).
assert (sd / "pyxis-map/packs/pack-a/manifest.pmp").is_file()
raced_record = tool.encode_active_map_set(
generation=2, map_set_id="osm-bright",
attribution=policy["attribution"], pack_ids=["other-pack", "raced-pack"])
assert (sd / "pyxis-map/active-pack.0").read_bytes() == raced_record
assert (sd / "pyxis-map/map-sets/osm-bright.pmas").read_bytes() == raced_record
assert not (sd / "pyxis-map/active-pack.1").exists()
# No marker is left behind.
assert not (sd / "pyxis-map/.pyxis-installing").exists()
# Retry: the pack verifies, records converge, and the new selection
# includes both packs with pack-a at the front.
result = tool.build_map_pack(source, sd, pack_id="pack-a", name="A",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"], style="osm-bright", activate=True)
record3 = tool.decode_active_selection((sd / "pyxis-map/active-pack.1").read_bytes())
assert record3["generation"] == 3
assert record3["packs"] == ["pack-a", "other-pack", "raced-pack"]
assert Path(result) == sd / "pyxis-map" / "packs" / "pack-a"
def test_marker_is_renewed_during_long_publication_and_released_by_owner(
tmp_path: Path) -> None:
# Round 9: a publication can outlive the marker TTL (a full world pack
# on slow SD storage). The builder must renew its marker's epoch
# during the tile loop so the claim never ages out, and release must
# still work afterwards: release compares the owner, not the full
# token, because renewals advance the epoch.
tool = load_tool()
source = tmp_path / "xyz"
for x in range(30):
put_tile(source, 5, x, 0)
policy = tool.STYLE_POLICIES["osm-bright"]
sd = tmp_path / "sd"
(sd / "pyxis-map").mkdir(parents=True)
renewals: list[tuple[str, int]] = []
original_renew = tool._renew_install_marker
def counting_renew(pyxis_fd, owner):
original_renew(pyxis_fd, owner)
raw = (sd / "pyxis-map" / ".pyxis-installing").read_text()
parts = raw.strip().split(" ")
assert parts[0] == "PYXI" and parts[1] == "1" and parts[2] == owner
renewals.append((owner, int(parts[3])))
tool._renew_install_marker = counting_renew
try:
tool.build_map_pack(source, sd, pack_id="pack-a", name="A",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"], style="osm-bright", activate=True)
finally:
tool._renew_install_marker = original_renew
# 30 tiles: one in-loop renewal at tile index 24 plus the final
# renewal after the last tile.
assert len(renewals) == 2, f"expected 2 renewals, got {len(renewals)}"
# The renewal kept our owner identity (release below depends on it).
owner = renewals[0][0]
assert owner.startswith("cli-")
# The install completed and the builder released its own marker even
# though renewals advanced the epoch past the acquired token.
assert (sd / "pyxis-map/packs/pack-a/manifest.pmp").is_file()
assert (sd / "pyxis-map/active-pack.0").is_file()
assert not (sd / "pyxis-map/.pyxis-installing").exists()
def test_long_publication_marker_stays_live_past_ttl(
tmp_path: Path) -> None:
# Round 9 (the actual race): a marker whose epoch has aged past the
# TTL is reclaimable ONLY if its owner stopped renewing (crashed).
# A live installer's heartbeat keeps the on-disk epoch fresh, so a
# second producer must refuse it. The observable contract:
# stale + never renewed -> reclaimed (abandoned install);
# stale + renewed to now -> refused (live installer).
# The second case is exactly what the tile-loop renewal produces: the
# on-disk epoch is always the last renewal, which is fresh by
# construction. Verify the decision boundary directly.
tool = load_tool()
now_ms = int(__import__("time").time() * 1000)
ttl_ms = tool._INSTALL_MARKER_TTL_SECONDS * 1000
# Aged 16 minutes: past the TTL.
assert not tool._marker_is_fresh(now_ms - 16 * 60 * 1000)
# Renewed just now (live owner's heartbeat): fresh again.
assert tool._marker_is_fresh(now_ms)
# And the reclaim path really keys off the on-disk epoch: a marker
# aged past the TTL with no renewals is reclaimable end-to-end.
source = tmp_path / "xyz"
put_tile(source, 1, 0, 0)
policy = tool.STYLE_POLICIES["osm-bright"]
sd = tmp_path / "sd"
(sd / "pyxis-map").mkdir(parents=True)
stale = now_ms - 16 * 60 * 1000
(sd / "pyxis-map/.pyxis-installing").write_text(f"PYXI 1 cli-crash {stale}")
tool.build_map_pack(source, sd, pack_id="pack-a", name="A",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"], style="osm-bright", activate=True)
assert (sd / "pyxis-map/packs/pack-a/manifest.pmp").is_file()
assert not (sd / "pyxis-map/.pyxis-installing").exists()
# Sanity on the TTL constant the boundary above used.
assert ttl_ms == 900 * 1000
def test_marker_renewal_refuses_to_steal_reclaimed_claim(
tmp_path: Path) -> None:
# Round 10 (Greptile): a blind heartbeat would let a stalled installer
# steal back a marker that a second producer LEGITIMATELY reclaimed
# after our TTL expired. The renewal must be ownership-checked:
# foreign owner -> abort (PackError), the foreign claim is left
# intact for the other installer to finish.
tool = load_tool()
card = tmp_path / "sd"
card.mkdir()
pyxis = card / "pyxis-map"
pyxis.mkdir()
pyxis_fd = os.open(str(pyxis), os.O_RDONLY | os.O_DIRECTORY)
try:
# Producer B reclaims after our (producer A) marker aged out.
(pyxis / ".pyxis-installing").write_text(
f"PYXI 1 cli-other {int(__import__('time').time() * 1000)}\n")
# Renewing as a DIFFERENT owner must abort...
with pytest.raises(tool.PackError, match="reclaimed during installation"):
tool._renew_install_marker(pyxis_fd, "cli-us")
# ...and must leave B's claim untouched.
raw = (pyxis / ".pyxis-installing").read_text().strip().split(" ")
assert raw[2] == "cli-other"
# Renewing as the SAME owner is allowed and advances the epoch.
before = int(raw[3])
tool._renew_install_marker(pyxis_fd, "cli-other")
after = (pyxis / ".pyxis-installing").read_text().strip().split(" ")
assert int(after[3]) >= before
# A missing or corrupt marker is also refused (claim void), never
# overwritten.
(pyxis / ".pyxis-installing").unlink()
with pytest.raises(tool.PackError, match="reclaimed during installation"):
tool._renew_install_marker(pyxis_fd, "cli-other")
assert not (pyxis / ".pyxis-installing").exists()
(pyxis / ".pyxis-installing").write_text("garbage\n")
with pytest.raises(tool.PackError, match="reclaimed during installation"):
tool._renew_install_marker(pyxis_fd, "cli-other")
finally:
os.close(pyxis_fd)