Files
VoiceStudio/tests/test_no_windows_hostile_paths.py
Palash DebnathandClaude Opus 5 9ac1045917 test: reject a backslash in a tracked path too
CodeRabbit's catch on #1799: git stores paths with `/` separators, so a `\`
that survives into a path component is part of a NAME. It is legal to commit
one from Linux or macOS and impossible to check out on Windows, where git
refuses it under `core.protectNTFS` — the same checkout-time failure, before
any test runs, that the stray `:memory:.ses` caused.

The rule moves into a pure `windows_hostile_reason` so it can be exercised
directly: the repo cannot carry a fixture for each hostile shape without
becoming the very thing the test rejects. Both directions are pinned — every
shape Windows refuses, and ordinary paths that merely resemble one (a file
called `console.md`, `com10.py`, a component containing but not ending in a
dot).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HR6J9zKQop9TGGVUwjypnF
2026-09-04 18:04:38 +05:30

116 lines
4.5 KiB
Python

"""No tracked path that Windows cannot check out.
Windows rejects `< > : " | ? *` and control characters in filenames, treats a
trailing space or dot on a path component as invalid, and reserves the device
names CON/PRN/AUX/NUL/COM1-9/LPT1-9. A literal backslash is legal on Linux and
macOS but is the directory separator on Windows, so Git refuses it too (that
one is `core.protectNTFS`, on by default there). Git on Windows refuses such a
path with
`error: invalid path ...` and exits 128 — during **checkout**, before any
build or test step runs. So a single stray file like `:memory:.ses` (a sqlite
session artifact named after the `:memory:` DSN, committed by accident on the
#1798 branch) turns every Windows job red with an error that names a file
nobody edited, while Linux and macOS stay green.
Nothing else catches this: the file need not be referenced by any code, and
the platforms that can check it out do not care. This scans the index on every
platform so the failure surfaces as a named test rather than as a checkout
crash on one leg of the matrix.
"""
import re
import subprocess
import pytest
from pathlib import Path
_ROOT = Path(__file__).resolve().parents[1]
# Characters git-for-Windows rejects outright, plus C0 controls. The backslash
# is in here because git stores paths with `/` separators, so a `\` that
# survives into a path component is part of a NAME — legal to commit from
# Linux, impossible to check out on Windows.
_BAD_CHARS = re.compile(r'[<>:"|?*\\\x00-\x1f]')
# Reserved DOS device names, with or without an extension (`NUL`, `nul.txt`).
_RESERVED = re.compile(r"^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|$)", re.IGNORECASE)
def _tracked_paths():
out = subprocess.run(
["git", "-C", str(_ROOT), "ls-files", "-z"],
capture_output=True, text=True, check=True,
).stdout
return [p for p in out.split("\0") if p]
def windows_hostile_reason(rel):
"""Why Windows cannot check `rel` out, or None if it can.
Pure so the rule itself is testable: the repo cannot carry a fixture for
each hostile shape without becoming the very thing this test rejects.
"""
for part in rel.split("/"):
if _BAD_CHARS.search(part):
return f"illegal character in {part!r}"
if part != part.rstrip(" ."):
return f"component {part!r} ends in a space or dot"
if _RESERVED.match(part):
return f"reserved device name {part!r}"
return None
def test_no_windows_hostile_tracked_paths():
# This test file names the offending characters in its own source, but its
# *path* is what is checked — every tracked path is scanned, none skipped.
offenders = [
f"{rel} ({reason})"
for rel in _tracked_paths()
if (reason := windows_hostile_reason(rel))
]
assert not offenders, (
"Tracked paths Windows cannot check out — git exits 128 during checkout "
"and every Windows CI job fails before it starts:\n "
+ "\n ".join(offenders)
)
@pytest.mark.parametrize(
"rel",
[
":memory:.ses", # the artifact that started this (#1798 branch)
"a/b:c.txt", # colon anywhere in the path, not just the root
"docs/what?.md", # the rest of the reserved punctuation
"src/a<b>.py",
'src/say"hi".py',
"src/a|b.py",
"src/a*b.py",
"notes/back\\slash.md", # legal on POSIX, a separator on Windows
"docs/notes ", # a component may not END in a space…
"docs/notes.", # …or a dot
"docs/dir /x.md", # directory components included
"src/NUL", # reserved device names, bare…
"src/con.txt", # …and with an extension, case-insensitively
"src/COM1.log",
"src/lpt9",
],
)
def test_rejects_every_shape_windows_refuses(rel):
assert windows_hostile_reason(rel), f"{rel!r} should have been rejected"
@pytest.mark.parametrize(
"rel",
[
"backend/core/version.py",
"docs/adr/0001-something.md",
"frontend/src/i18n/locales/zh-CN.json",
"tests/test_no_windows_hostile_paths.py",
"scripts/build-omnivoice-tts.sh",
"bin/omnivoice-tts-linux-aarch64",
"docs/console.md", # merely CONTAINS a device name — fine
"src/nulls.py",
"src/com10.py", # only COM1-9 are reserved
],
)
def test_accepts_ordinary_paths(rel):
assert windows_hostile_reason(rel) is None, f"{rel!r} was rejected in error"