ca89dc5970
Two py<->ts divergences, one structural alignment, one swept-and-gated class of config noise. 1. GitHub `ref` defaulted to the literal "main" in python where TypeScript resolves `config.ref ?? repoInfo.default_branch`, so every repository whose default branch is something else 404d on the one tree fetch the mount is built on and read as empty. `GitHubConfig.ref` is now optional, and `ensure_ref` resolves it lazily beside the existing `ensure_default_branch` -- python's constructor deliberately does no network. Every reader that needs a concrete ref goes through it. `mirage.accessor.github` is fully annotated and leaves mypy's untyped-defs allowlist. 2. `sanitize_label` capped characters where NAME_MAX counts bytes, which the constant's own comment already spelled out. gdocs/gsheets/gslides rendered ~367-byte filenames for a CJK title and gmail/email ~269; ext4 and APFS reject those. `sanitize_label`/`sanitizeLabel` grew a byte budget and every compose site passes what the date, id and suffix leave. Found alongside: TS email's hand-rolled copy of the sanitizer used JS's ASCII-only `\w` (so non-ASCII subjects became underscores there and stayed intact in python), and three sites re-composed `<subject>__<id>.<ext>` independently of readdir, so a search hit could name a path that does not exist. 3. The browser registry hand-rolled `normalizeFields` per entry and cast through 7 `*BrowserCtorConfig` interfaces -- hand-written twins of the real config types. It now delegates to a `normalizeXConfig` beside each config, as node's registry does; the interfaces and 22 double casts are gone. 4. 95 rename entries that only restated `snakeToCamel` deleted across 24 files, with `scripts/check_normalize_renames.py` and a CI step to keep them from returning. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
136 lines
5.7 KiB
Python
136 lines
5.7 KiB
Python
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
|
|
import pathlib
|
|
|
|
TESTS = pathlib.Path(__file__).resolve().parent
|
|
SOURCE = TESTS.parent / "mirage"
|
|
IGNORED = {"__pycache__", ".pytest_cache"}
|
|
|
|
# A test directory earns its place by mirroring a source package, so that a
|
|
# reader can go from `mirage/<pkg>/` to `tests/<pkg>/` without guessing. These
|
|
# eight mirror nothing on purpose: they hold harnesses and fixture data rather
|
|
# than a suite for one source package. Every other test directory must have a
|
|
# twin -- `tests/resource/object_storage/` is what this rule is for, because it
|
|
# collected 24 suites for 19 sibling packages and made `mirage/resource/s3/`
|
|
# look untested.
|
|
UNMIRRORED_DIRS = {
|
|
"commands/builtin/jq":
|
|
"the embedded jq engine spans generic/jq.py and its builder; the "
|
|
"suite is grouped per feature, not per module",
|
|
"commands/custom":
|
|
"exercises the public command-registration API, not a package",
|
|
"commands/native":
|
|
"the native-binary harness: each case runs a real coreutils binary "
|
|
"and diffs mirage against it",
|
|
"config":
|
|
"loader tests plus the bad-config YAML shared with the TypeScript tree",
|
|
"config/fixtures":
|
|
"the YAML files themselves",
|
|
"conformance":
|
|
"runs the cross-language cases under the top-level conformance/ tree",
|
|
"e2e":
|
|
"end-to-end suites that span the package, so no one source dir owns "
|
|
"them",
|
|
"fixtures":
|
|
"shared test doubles imported by suites elsewhere",
|
|
}
|
|
|
|
# A ratchet, not a target. It counts source modules with no `test_<name>.py`
|
|
# anywhere under tests/ -- the weaker of the two readings, because CLAUDE.md
|
|
# asks for a mirror only "where reasonable" and several suites deliberately
|
|
# cover a family in one file. Moving it in either direction is a deliberate
|
|
# act: adding a module with no test raises it, and closing a gap has to be
|
|
# locked in by lowering it.
|
|
#
|
|
# Known blind spot, and the reason this number is a floor rather than a
|
|
# measure of coverage: the match is by basename, so seven unrelated
|
|
# `test_history.py` files make every `history.py` in the tree look
|
|
# mirrored. Reading it strictly -- a test beside its own source twin --
|
|
# would count 816 today. What the ratchet buys is narrower than it looks:
|
|
# a module whose name appears nowhere in the suite cannot be added
|
|
# silently.
|
|
MIRROR_BASELINE = 184
|
|
|
|
|
|
def _test_dirs() -> list[pathlib.Path]:
|
|
return [
|
|
d for d in sorted(TESTS.rglob("*"))
|
|
if d.is_dir() and not IGNORED & set(d.parts)
|
|
]
|
|
|
|
|
|
def _source_modules() -> list[pathlib.Path]:
|
|
return [
|
|
p.relative_to(SOURCE) for p in sorted(SOURCE.rglob("*.py"))
|
|
if not IGNORED & set(p.parts) and p.name != "__init__.py"
|
|
]
|
|
|
|
|
|
def test_tests_declare_no_packages():
|
|
"""`tests/` is a namespace tree; pytest imports it without __init__.py.
|
|
|
|
`pyproject.toml` sets `--import-mode=importlib`, which already resolves
|
|
the same-basename collisions (34 different `test_find.py`) that an
|
|
`__init__.py` would otherwise be needed for. CLAUDE.md forbids them
|
|
outright, and without a check the count reached 87.
|
|
"""
|
|
found = sorted(
|
|
str(p.relative_to(TESTS)) for p in TESTS.rglob("__init__.py")
|
|
if not IGNORED & set(p.parts))
|
|
assert not found, ("tests/ is a namespace tree -- delete these:\n" +
|
|
"\n".join(found))
|
|
|
|
|
|
def test_every_test_directory_mirrors_a_source_package():
|
|
"""Each test directory maps to a `mirage/` package, or is exempt above."""
|
|
missing = [
|
|
rel for rel in (str(d.relative_to(TESTS)) for d in _test_dirs())
|
|
if not (SOURCE / rel).is_dir() and rel not in UNMIRRORED_DIRS
|
|
]
|
|
assert not missing, (
|
|
"these test directories mirror no mirage/ package -- move them onto "
|
|
"the source layout, or add them to UNMIRRORED_DIRS with a reason:\n" +
|
|
"\n".join(f" tests/{rel}" for rel in sorted(missing)))
|
|
|
|
|
|
def test_no_stale_directory_exemption():
|
|
"""An exemption outlives its reason silently; make it fail instead."""
|
|
stale = []
|
|
for rel, reason in UNMIRRORED_DIRS.items():
|
|
if not (TESTS / rel).is_dir():
|
|
stale.append(f" {rel}: gone ({reason})")
|
|
elif (SOURCE / rel).is_dir():
|
|
stale.append(f" {rel}: mirage/{rel} exists now ({reason})")
|
|
assert not stale, ("drop these entries from UNMIRRORED_DIRS:\n" +
|
|
"\n".join(stale))
|
|
|
|
|
|
def test_module_mirror_coverage_holds_the_baseline():
|
|
"""Ratchet the number of source modules that no test file is named for."""
|
|
named = {p.name for p in TESTS.rglob("test_*.py")}
|
|
unmirrored = [
|
|
m for m in _source_modules() if f"test_{m.stem}.py" not in named
|
|
]
|
|
count = len(unmirrored)
|
|
if count > MIRROR_BASELINE:
|
|
added = "\n".join(f" mirage/{m}" for m in unmirrored[:20])
|
|
raise AssertionError(
|
|
f"{count} source modules have no test named for them, above the "
|
|
f"baseline of {MIRROR_BASELINE}. Add `test_<module>.py` beside "
|
|
f"its source twin. Some of the modules counted:\n{added}")
|
|
assert count == MIRROR_BASELINE, (
|
|
f"mirror coverage improved to {count}; lock it in by setting "
|
|
f"MIRROR_BASELINE = {count}")
|