Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d6d1df2
conftest.py and four test files: one helper asks whether a system can…
Oct 16, 2026
0b1d1dd
test_bootstrap_branch_bytes.py: the no-fault test's Windows guard is …
Oct 16, 2026
6ba8c5d
conftest.py and two test files: `skip_message` is held by a test that…
Oct 16, 2026
4184972
test_stdout_encoding.py, test_adopt_git_reads.py, test_adopt_template…
Oct 17, 2026
6b5e907
test_non_utf8_git_reads.py and test_non_utf8_name_callers.py: the `ma…
Oct 17, 2026
4cdb427
Merge branch 'main' into fix/663-non-utf8-name-tests
brettheap Oct 17, 2026
d427cb2
Merge #723's final head 4cdb427c (landed as bad05d2)
Oct 17, 2026
76ae641
Merge main bad05d21.. after #723 landed as bad05d2
Oct 17, 2026
b18b243
tests: three tests ask the helper whether a name can be made, `make_f…
Oct 17, 2026
efb1727
tests: the emulated disk answers every errno but EILSEQ, `make_file` …
Oct 17, 2026
9ba530a
tests: `raised_for_every_errno` passes the errno's label to the failu…
Oct 17, 2026
8f6a38e
tests: the sweep's own test builds its `Disk` before the `pytest.rais…
Oct 17, 2026
f62b3de
Merge branch 'main' into fix/757-non-utf8-test-dirs
brettheap Oct 17, 2026
a149863
Merge origin/main (`e62b5e17`) into fix/757-non-utf8-test-dirs: #818'…
Oct 17, 2026
e387a3c
Merge #806's head a1498632 into l3-853-854-test-skips: it settles the…
Oct 18, 2026
84a09c2
Merge origin/main 3fb30a8f into l3-853-854-test-skips: main brings #8…
Oct 18, 2026
40531e1
tests: the six places that skipped on a failed git call ask the syste…
Oct 18, 2026
60836f5
tests: the three tests of the byte directory have Windows rows that h…
Oct 18, 2026
bcf6952
tests: the six template-value tests that raised `UnicodeEncodeError` …
Oct 18, 2026
22238b4
Merge branch 'main' into fix/757-non-utf8-test-dirs
brettheap Oct 18, 2026
e3bb920
Merge #806's final head 22238b47 (landed as d6071c7)
Oct 18, 2026
fc3cb5e
Merge #806's squash d6071c7 after it landed, taking the branch's side…
Oct 18, 2026
2572a71
tests: windows_fsencode takes a path as os.fsencode does, and six row…
Oct 18, 2026
bd08ef8
Merge #845's final head fc3cb5e9 into l3-853-854-test-skips: the head…
Oct 18, 2026
ba2d7ab
Merge #845's squash 0f693395 into l3-853-854-test-skips: ours for tes…
Oct 18, 2026
ce1041f
Merge branch 'main' into l3-853-854-test-skips
brettheap Oct 18, 2026
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
27 changes: 24 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1235,12 +1235,14 @@ def on_a_branch_that_is_not_utf_8(repo: Path) -> dict:
return held_by_git(repo)


def windows_fsencode(text: str | bytes) -> bytes:
def windows_fsencode(text: str | bytes | os.PathLike) -> bytes:
"""`os.fsencode` as Windows has it: UTF-8 with `surrogatepass`, so every
lone surrogate is the three bytes of a sequence that is not UTF-8. Bytes
go through as they are, as `os.fsencode` passes them on every system. For
lone surrogate is the three bytes of a sequence that is not UTF-8. A path
is taken as the real `os.fsencode` takes it (`os.fspath` first), and
bytes go through as they are, as it passes them on every system. For
`monkeypatch.setattr(os, "fsencode", windows_fsencode)`, which puts the
guard above in front of a test that Linux would let through."""
text = os.fspath(text)
if isinstance(text, bytes):
return text
return text.encode("utf-8", "surrogatepass")
Expand All @@ -1259,6 +1261,25 @@ def skip_message(call, *args) -> str | None:
return None


def skip_where_the_environment_cannot_carry(value: str) -> None:
"""Skip where this system's file name encoding cannot encode `value`, as
under `LC_ALL=C PYTHONUTF8=0 PYTHONCOERCECLOCALE=0` (ASCII). It is asked of
a file name (`make_file` cannot write `caf\\u00e9.yml`, and `mkdir` cannot
make `\\u202ecaf`), of an environment value and of a command-line
argument: `subprocess` puts each one through `os.fsencode` before it
starts the child, so a `user.name` of `Jos\\u00e9` or a branch
`main\\u00a0` raises `UnicodeEncodeError` in the test and not in the tool
(#757, #853, #854). Text that is ASCII, or a lone surrogate that
`os.fsencode` gives back as its byte, is carried. Asked through the `os`
module at call time, so a test emulates ASCII with
`monkeypatch.setattr(os, "fsencode", ...)`."""
try:
os.fsencode(value)
except UnicodeEncodeError:
pytest.skip(f"this system's file name encoding "
f"({sys.getfilesystemencoding()}) cannot spell {value!r}")


# --- the `gh` guard (#296) ---------------------------------------------------
#
# NO TEST REACHES THE OPERATOR'S `gh`, and the doctor's `machine` row is still
Expand Down
83 changes: 48 additions & 35 deletions tests/test_adopt_git_reads.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
import pytest

from conftest import (REPO, appended_to_command_scope, git, make_source_repo,
skip_where_a_name_cannot_be_not_utf_8)
skip_where_a_name_cannot_be_not_utf_8,
skip_where_the_environment_cannot_carry)
from test_adopt_template_values import (ADOPT_REFUSED, PLAN_FILE, PLAN_OK,
PROJECT, REMOTES, SPLIT_BRANCH,
VERIFIED, WORK, adopted, answer,
Expand Down Expand Up @@ -462,15 +463,12 @@ def test_a_utf8_clone_of_a_source_whose_path_is_not_utf8_adopts(tmp_path):
at a path that is text has the old path as its origin, so `execute
--source <the clone>` ended in this issue's traceback; it now adopts.
Skipped where no directory can be named with the byte 0xE9 (Windows,
macOS's APFS) or git will not work in one."""
macOS's APFS); a failure of git under it fails the test (#853)."""
skip_where_a_name_cannot_be_not_utf_8(tmp_path)
parent = Path(os.fsdecode(os.path.join(os.fsencode(tmp_path),
b"caf\xe9")))
parent.mkdir()
try:
source = make_source_repo(parent / "Thing")
except AssertionError as exc:
pytest.skip(f"git will not work under {parent.name!r} here: {exc}")
source = make_source_repo(parent / "Thing")
clone = tmp_path / "clone"
git("clone", "-q", "--", str(source), str(clone), cwd=tmp_path)
runs = {"plan": planned(tmp_path, source)}
Expand Down Expand Up @@ -522,6 +520,25 @@ def test_a_utf8_clone_of_a_source_whose_path_is_not_utf8_adopts(tmp_path):
LOST_REFUSAL = b"REFUSED plan-tracking-branch-missing: "


def skip_where_no_ref_can_be_named(source: Path, branch: str) -> None:
"""Skip where this system cannot give `source` a ref named `branch`,
asked before any git call makes one (#853). A name that is not UTF-8
asks `skip_where_a_name_cannot_be_not_utf_8` of the directory `source`
is in, on the disk its ref files go to: Windows' `os.fsencode` first,
then macOS's EILSEQ. A name that is text asks only whether this
system's file name encoding can spell it, as a command line must
(`subprocess` puts each argument through `os.fsencode`; ASCII under
`LC_ALL=C` cannot), since every disk git runs on holds text. What git
does after it is the test's own: a git call that fails, on a full disk
or a directory it cannot write in, fails the test."""
try:
branch.encode("utf-8")
except UnicodeEncodeError:
skip_where_a_name_cannot_be_not_utf_8(source.parent)
else:
skip_where_the_environment_cannot_carry(branch)


def with_default_branch(source: Path, where: str,
branch: str = BRANCH) -> Path:
"""`source`, on `main`, with `branch` (`BRANCH` unless given) as its
Expand All @@ -531,23 +548,21 @@ def with_default_branch(source: Path, where: str,
source: a plan that read the wrong branch would adopt the wrong commit,
and a `--tracking-branch main` names a branch it has.

Skipped where no branch can be named so (the byte 0xE9 on Windows or
macOS's APFS), or git here does not give the name back as it was
written."""
Skipped where this system cannot name the branch
(`skip_where_no_ref_can_be_named`: the byte 0xE9 on Windows or macOS's
APFS, or text an ASCII file name encoding cannot spell), asked before
git is, and where git here does not give the name back as it was
written. A git call between the two that fails fails the test (#853)."""
ref = "refs/remotes/origin/HEAD" if where == ORIGIN_HEAD else "HEAD"
written = branch.encode("utf-8", "surrogateescape")
try:
git("switch", "-q", "-c", branch, cwd=source)
git("commit", "-q", "--allow-empty", "-m", "One ahead", cwd=source)
if where == ORIGIN_HEAD:
git("switch", "-q", "main", cwd=source)
for name in (branch, "main"):
git("update-ref", f"refs/remotes/origin/{name}", name,
cwd=source)
git("symbolic-ref", ref, f"refs/remotes/origin/{branch}",
cwd=source)
except (AssertionError, UnicodeError, OSError) as exc:
pytest.skip(f"no branch here is named {written!r}: {exc!r}")
skip_where_no_ref_can_be_named(source, branch)
git("switch", "-q", "-c", branch, cwd=source)
git("commit", "-q", "--allow-empty", "-m", "One ahead", cwd=source)
if where == ORIGIN_HEAD:
git("switch", "-q", "main", cwd=source)
for name in (branch, "main"):
git("update-ref", f"refs/remotes/origin/{name}", name, cwd=source)
git("symbolic-ref", ref, f"refs/remotes/origin/{branch}", cwd=source)
read = subprocess.run(["git", "symbolic-ref", ref], cwd=str(source),
capture_output=True, check=False)
if not read.stdout.rstrip(b"\r\n").endswith(b"/" + written):
Expand All @@ -559,15 +574,14 @@ def moved_to_a_branch_that_is_not_utf8(base: Path) -> Path:
"""A source on `main`, planned and answered, whose `main` is then
renamed `BRANCH`: a plan that tracks a branch the source no longer has,
and a source whose HEAD is on one that is not UTF-8. Skipped as
`with_default_branch` is."""
`with_default_branch` is, before anything is made; a failure of git
fails the test (#853)."""
skip_where_no_ref_can_be_named(base / "Thing", BRANCH)
source = make_source_repo(base / "Thing")
made = planned(base, source)
assert made.returncode == 0, said(made)
answer(base)
try:
git("branch", "-m", "main", BRANCH, cwd=source)
except (AssertionError, UnicodeError, OSError) as exc:
pytest.skip(f"no branch here is named {NOT_UTF8_BRANCH!r}: {exc!r}")
git("branch", "-m", "main", BRANCH, cwd=source)
return source


Expand Down Expand Up @@ -720,15 +734,14 @@ def test_plan_reads_a_default_branch_ending_in_white_space_whole(tmp_path,
def remembered_on(source: Path, branch: str) -> Path:
"""`source` with `origin/HEAD` naming `origin/<branch>`, which it has
fetched, and no branch `branch` of its own, as `git clone -b main` of a
repository whose default branch is `branch` leaves it. Skipped as
`with_default_branch` is, where git refuses or alters the name."""
try:
git("update-ref", f"refs/remotes/origin/{branch}", "refs/heads/main",
cwd=source)
git("symbolic-ref", "refs/remotes/origin/HEAD",
f"refs/remotes/origin/{branch}", cwd=source)
except (AssertionError, UnicodeError, OSError) as exc:
pytest.skip(f"no ref here is named {branch!r}: {exc!r}")
repository whose default branch is `branch` leaves it. Skipped where
this system cannot name the ref, asked before git is, as
`with_default_branch` is; a failure of git fails the test (#853)."""
skip_where_no_ref_can_be_named(source, branch)
git("update-ref", f"refs/remotes/origin/{branch}", "refs/heads/main",
cwd=source)
git("symbolic-ref", "refs/remotes/origin/HEAD",
f"refs/remotes/origin/{branch}", cwd=source)
return source


Expand Down
8 changes: 3 additions & 5 deletions tests/test_adopt_plan_escapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,15 +398,13 @@ def test_a_source_directory_that_is_not_utf8_is_shown_spelled(adopter,
so did `check`'s `WHAT WILL HAPPEN, against <path> @`, and each raised
`UnicodeEncodeError` on a strict UTF-8 stdout after the plan was written.
Both show it spelled now. Skipped where no directory can be named with
the byte 0xE9 (Windows, macOS's APFS) or git will not work in one."""
the byte 0xE9 (Windows, macOS's APFS); a failure of git under it fails
the test (#853)."""
skip_where_a_name_cannot_be_not_utf_8(tmp_path)
parent = Path(os.fsdecode(os.path.join(os.fsencode(tmp_path),
b"caf\xe9")))
parent.mkdir()
try:
source = make_source_repo(parent / "Thing")
except AssertionError as exc:
pytest.skip(f"git will not work under b'caf\\xe9' here: {exc}")
source = make_source_repo(parent / "Thing")
shown = adopter._spelled(str(source.resolve()))
assert "\\udce9" in shown
plan = tmp_path / PLAN_FILE
Expand Down
67 changes: 40 additions & 27 deletions tests/test_adopt_template_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@

from conftest import (ADOPT, REPO, WINDOWS_SKIP, blank_unnamed_pin_sources,
make_source_repo, resolve,
skip_where_a_name_cannot_be_not_utf_8)
skip_where_a_name_cannot_be_not_utf_8,
skip_where_the_environment_cannot_carry)

sys.path.insert(0, str(REPO / "scripts"))
import shape_materialize # noqa: E402
Expand Down Expand Up @@ -197,7 +198,11 @@ def test_plan_refuses_a_flag_no_file_can_hold(tmp_path, flag, value):
`plan ok`, and `execute` raised in `write_lf` after both legs were
pushed (a surrogate), or adopted with the line break, the tab or the
bidi control in the manifest and the README. Now `plan` refuses, and
writes no plan."""
writes no plan. Skipped where this system's file name encoding cannot
spell `value` (U+2028, U+202E or U+2066 in ASCII under `LC_ALL=C`),
asked first: `subprocess` cannot put it on `plan`'s command line there,
and raised `UnicodeEncodeError` in the test, not in the tool."""
skip_where_the_environment_cannot_carry(value)
source = make_source_repo(tmp_path / "Thing")
runs = plan_check_execute(tmp_path, source, flag, value)
assert_no_traceback_and_no_leg(tmp_path, runs)
Expand Down Expand Up @@ -246,26 +251,20 @@ def test_plan_names_the_elector_it_took_from_git_config(tmp_path):
b"again")


def refused_then_moved(base: Path, parent: Path, reason: bytes,
skip_where_git_fails: bool = True) -> None:
def refused_then_moved(base: Path, parent: Path, reason: bytes) -> None:
"""A source in the directory `parent`: `plan` and `check` pass it, as
#247 made them, and `execute` refuses its path as the assembly root's
clone URL, giving `reason`, before any leg exists. Then the remediation
the refusal gives, followed as it is worded -- `parent` renamed, then
`plan --source <its new path>`, `check` and `execute` run again -- ends
`adoption verified`.

A source repository that git cannot make under `parent` skips the test
where `skip_where_git_fails` is true, for a name that is not UTF-8, which
git may not work under. A name that is valid text asks for it false, and
the failure is raised: the cause is the disk or git, never the name
(#834)."""
try:
source = make_source_repo(parent / "Thing")
except AssertionError as exc:
if not skip_where_git_fails:
raise
pytest.skip(f"git will not work under {parent.name!r} here: {exc}")
The source repository is made under `parent` with no guard: the caller
has asked whether this system can carry the name, and git works under
any directory a system carries, so a failure of git there (a disk that
filled after the `mkdir`) is the test's own and is raised, for a name
that is not UTF-8 (#853) as for one that is valid text (#834)."""
source = make_source_repo(parent / "Thing")
runs = plan_check_execute(base, source)
assert_no_traceback_and_no_leg(base, runs)
assert runs["plan"].returncode == 0, said(runs["plan"])
Expand Down Expand Up @@ -300,8 +299,8 @@ def test_execute_refuses_a_local_source_directory_that_is_not_utf8(tmp_path):
once said to clone the source to a UTF-8 path and pass that with
--source, which ended in a traceback: the clone's `origin` is the old
path, which `_remote_repository` reads as UTF-8. Skipped where no
directory can be named with the byte 0xE9 (Windows, macOS's APFS) or
git will not work in one."""
directory can be named with the byte 0xE9 (Windows, macOS's APFS); a
failure of git under it fails the test (#853)."""
skip_where_a_name_cannot_be_not_utf_8(tmp_path)
parent = Path(os.fsdecode(os.path.join(os.fsencode(tmp_path),
b"caf\xe9")))
Expand All @@ -315,15 +314,18 @@ def test_execute_refuses_a_local_source_directory_holding_a_bidi_control(
"""A source under a directory whose name begins with U+202E RIGHT-TO-LEFT
OVERRIDE: on main `execute` adopted it, and the shape's README showed
the rest of its `git clone` line reversed. Now it is refused as the
byte is, and moving the source adopts. Nothing here is guarded: U+202E
is valid text, which a POSIX disk, NTFS and APFS hold and git works
under (reasoned, not run, on the last two), so an error of the disk when
the directory is made, and a failure of git when the source is, are the
test's own and are raised, not skipped (#834)."""
byte is, and moving the source adopts. Skipped where this system's file
name encoding cannot spell U+202E (ASCII under `LC_ALL=C`), which is
asked before the `mkdir`, where it raised `UnicodeEncodeError` (#854).
Nothing else is guarded: U+202E is valid text, which a POSIX disk, NTFS
and APFS hold and git works under (reasoned, not run, on the last two),
so an error of the disk when the directory is made, and a failure of git
when the source is, are the test's own and are raised, not skipped
(#834)."""
parent = tmp_path / "\u202ecaf"
skip_where_the_environment_cannot_carry(parent.name)
parent.mkdir()
refused_then_moved(tmp_path, parent, b"a bidirectional control",
skip_where_git_fails=False)
refused_then_moved(tmp_path, parent, b"a bidirectional control")


# --- `check` and `execute`: a plan value edited by hand ----------------------
Expand Down Expand Up @@ -647,7 +649,13 @@ def test_a_valid_non_ascii_elector_still_adopts(tmp_path, elected_by):

The manifest is asked through the reader, not for a line: since #250 a
YAML template's value is written as `double_quoted` spells it, so the
line holds U+200C as an escape there, and as it stands before."""
line holds U+200C as an escape there, and as it stands before.

Skipped where this system's file name encoding cannot spell the name
(ASCII under `LC_ALL=C`), asked first: `subprocess` cannot put it on
`plan`'s command line there, and raised `UnicodeEncodeError` in the
test, not in the tool."""
skip_where_the_environment_cannot_carry(elected_by)
source = make_source_repo(tmp_path / "Thing")
runs = plan_check_execute(tmp_path, source, "--elected-by", elected_by)
assert_no_traceback(runs)
Expand All @@ -666,8 +674,13 @@ def test_a_valid_non_ascii_elector_still_adopts(tmp_path, elected_by):
def test_a_utf8_source_directory_still_adopts_with_local_remotes(tmp_path):
"""A source under a directory `café`: its path is the clone URL, which a
file holds, so it adopts, and the shape's README (beside the source's
own) names it."""
source = make_source_repo(tmp_path / "café" / "Thing")
own) names it. Skipped where this system's file name encoding cannot
spell the directory's name (ASCII under `LC_ALL=C`), asked before the
directory is made, where `mkdir` raised `UnicodeEncodeError` in the
test."""
parent = tmp_path / "caf\u00e9"
skip_where_the_environment_cannot_carry(parent.name)
source = make_source_repo(parent / "Thing")
runs = plan_check_execute(tmp_path, source)
assert_no_traceback(runs)
assert runs["check"].returncode == 0, said(runs["check"])
Expand Down
Loading
Loading