diff --git a/tests/test_adopt_git_reads.py b/tests/test_adopt_git_reads.py index 2420d998..a8059442 100644 --- a/tests/test_adopt_git_reads.py +++ b/tests/test_adopt_git_reads.py @@ -43,7 +43,8 @@ import pytest -from conftest import REPO, appended_to_command_scope, git, make_source_repo +from conftest import (REPO, appended_to_command_scope, git, make_source_repo, + skip_where_a_name_cannot_be_not_utf_8) from test_adopt_template_values import (ADOPT_REFUSED, PLAN_FILE, PLAN_OK, PROJECT, REMOTES, SPLIT_BRANCH, VERIFIED, WORK, adopted, answer, @@ -462,12 +463,10 @@ def test_a_utf8_clone_of_a_source_whose_path_is_not_utf8_adopts(tmp_path): --source ` 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.""" - try: - parent = Path(os.fsdecode(os.path.join(os.fsencode(tmp_path), - b"caf\xe9"))) - parent.mkdir() - except (OSError, UnicodeError) as exc: - pytest.skip(f"no directory here is named b'caf\\xe9': {exc!r}") + 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: diff --git a/tests/test_adopt_plan_escapes.py b/tests/test_adopt_plan_escapes.py index cc1ef280..c6041a72 100644 --- a/tests/test_adopt_plan_escapes.py +++ b/tests/test_adopt_plan_escapes.py @@ -37,7 +37,8 @@ import pytest from conftest import (ADOPT, REPO, blank_unnamed_pin_sources, git, - make_source_repo, resolve) + make_source_repo, resolve, + skip_where_a_name_cannot_be_not_utf_8) sys.path.insert(0, str(REPO / "scripts")) import repo_shape # noqa: E402 @@ -398,12 +399,10 @@ def test_a_source_directory_that_is_not_utf8_is_shown_spelled(adopter, `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.""" - try: - parent = Path(os.fsdecode(os.path.join(os.fsencode(tmp_path), - b"caf\xe9"))) - parent.mkdir() - except (OSError, UnicodeError) as exc: - pytest.skip(f"no directory here is named b'caf\\xe9': {exc!r}") + 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: @@ -458,13 +457,7 @@ def test_a_leg_extracts_a_path_that_is_not_utf8(adopter, tmp_path): lifting that refusal reaches this, so the extraction is tested here, directly. Skipped where no file can be named with that byte (Windows, macOS's APFS): filter-repo works in a clone, which checks the name out.""" - try: - probe = Path(os.fsdecode(os.path.join(os.fsencode(tmp_path), - b"caf\xe9.txt"))) - probe.write_bytes(b"") - probe.unlink() - except (OSError, UnicodeError) as exc: - pytest.skip(f"no file here is named b'caf\\xe9.txt': {exc!r}") + skip_where_a_name_cannot_be_not_utf_8(tmp_path) source = odd_source(tmp_path, b"caf\xe9.txt") commit = git("rev-parse", "HEAD", cwd=source).stdout.strip() remote = tmp_path / "Northwind-code.git" diff --git a/tests/test_adopt_template_values.py b/tests/test_adopt_template_values.py index 05e4f272..fd60cf3b 100644 --- a/tests/test_adopt_template_values.py +++ b/tests/test_adopt_template_values.py @@ -50,7 +50,8 @@ import pytest from conftest import (ADOPT, REPO, WINDOWS_SKIP, blank_unnamed_pin_sources, - make_source_repo, resolve) + make_source_repo, resolve, + skip_where_a_name_cannot_be_not_utf_8) sys.path.insert(0, str(REPO / "scripts")) import shape_materialize # noqa: E402 @@ -292,12 +293,10 @@ def test_execute_refuses_a_local_source_directory_that_is_not_utf8(tmp_path): 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.""" - try: - parent = Path(os.fsdecode(os.path.join(os.fsencode(tmp_path), - b"caf\xe9"))) - parent.mkdir() - except (OSError, UnicodeError) as exc: - pytest.skip(f"no directory here is named b'caf\\xe9': {exc!r}") + 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() refused_then_moved(tmp_path, parent, b"a byte that is not UTF-8") diff --git a/tests/test_clone_url_bytes.py b/tests/test_clone_url_bytes.py index e01b99e7..4d5d6d61 100644 --- a/tests/test_clone_url_bytes.py +++ b/tests/test_clone_url_bytes.py @@ -44,7 +44,8 @@ import pytest -from conftest import ORG, SCAFFOLD, blank_unnamed_pin_sources +from conftest import (ORG, SCAFFOLD, blank_unnamed_pin_sources, + skip_where_a_name_cannot_be_not_utf_8) # Fixtures of the #282 file, on the harness this one shares with it: the # scaffold loaded as a module, and the guard that no process is started. from test_scaffold_free_text_values import ( # noqa: F401 - fixtures @@ -151,18 +152,34 @@ def family_refusal(url: str, reason: str) -> str: f"{FAMILY_REMEDIATION}\n") +def skip_where_this_system_cannot_name(tmp_path: Path, raw: bytes) -> None: + """Skip where `tmp_path/` cannot be made on this system. Two + questions, each asked only of the names it is about: + + - a name that is not UTF-8 goes to `skip_where_a_name_cannot_be_not_utf_8`, + which skips on the two answers that mean "this system cannot carry + it" (Windows names its files in UTF-16, and macOS answers EILSEQ) and + raises any other error of the disk; + - a control character, 0x00 to 0x1F, is refused by Windows in any name, + and a name that holds one is skipped there. + + Nothing else is skipped: U+202E, which NTFS holds, is a name Windows + makes as Linux does (#757; reasoned, no Windows run: CI is Linux only). + `tests/test_non_utf8_name_callers.py` holds the answers by emulation.""" + try: + raw.decode("utf-8") + except UnicodeDecodeError: + skip_where_a_name_cannot_be_not_utf_8(tmp_path) + if os.name == "nt" and any(byte < 0x20 for byte in raw): + pytest.skip("Windows refuses a control character in a file name") + + def directory_named(tmp_path: Path, raw: bytes) -> Path: - """`tmp_path/` made from BYTES, empty; the test is skipped where the - disk cannot hold such a name (macOS refuses a name that is not UTF-8 and - Windows names its files in UTF-16, and refuses a control character).""" - if os.name == "nt": - pytest.skip("Windows names its files in UTF-16, which has no byte " - "that is not UTF-8, and refuses a control character") + """`tmp_path/` made from BYTES, empty; the test is skipped where this + system cannot hold such a name, and fails on any other error of the disk.""" + skip_where_this_system_cannot_name(tmp_path, raw) made = os.fsencode(tmp_path) + os.fsencode(os.sep) + raw - try: - os.mkdir(made) - except (OSError, ValueError, UnicodeError) as error: - pytest.skip(f"this disk cannot hold the name {raw!r}: {error}") + os.mkdir(made) path = Path(os.fsdecode(made)) assert path.is_dir() assert os.listdir(path) == [] diff --git a/tests/test_non_utf8_git_reads.py b/tests/test_non_utf8_git_reads.py index d34d2834..be2fa550 100644 --- a/tests/test_non_utf8_git_reads.py +++ b/tests/test_non_utf8_git_reads.py @@ -121,7 +121,11 @@ def test_make_file_skips_where_fsencode_makes_no_byte_and_writes_nothing( def test_make_file_writes_a_name_that_is_utf8_where_fsencode_makes_no_byte( tmp_path, monkeypatch): """The guard is for the byte and no wider: Windows holds `caf\\u00e9.yml` - as text, and the tests that use it must run there.""" + as text, and the tests that use it must run there. Skipped, before the + Windows `os.fsencode` is put in, where this system's own file name + encoding cannot write `caf\\u00e9.yml` (ASCII); and for nothing else, so a + write the disk refuses fails the test (#757).""" + skip_where_the_environment_cannot_carry(UTF8) monkeypatch.setattr(os, "fsencode", windows_fsencode) target = tmp_path / UTF8 assert skip_message(make_file, target, b"on: [pull_request]\n") is None @@ -495,6 +499,23 @@ def test_a_clean_standard_prints_no_warning(standard, tmp_path, run, warning, "Windows converts it") LEGACY_NAME = "Jos\udce9" + +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 of an + environment value: `subprocess` puts each one through `os.fsencode` + before it starts the child, so a `user.name` of `Jos\\u00e9` raises + `UnicodeEncodeError` in the test and not in the tool (#757). Text that is + ASCII, or a lone surrogate that `os.fsencode` gives back as its byte, is + carried.""" + try: + os.fsencode(value) + except UnicodeEncodeError: + pytest.skip(f"this system's file name encoding " + f"({sys.getfilesystemencoding()}) cannot carry {value!r}") + + REFUSED = "REFUSED family-unwritable-value: " #: How the refusal names the creator when no flag supplied it: `_manifest_flags` #: (#307), so that the two refusals agree on who is meant. @@ -628,6 +649,7 @@ def test_a_user_name_that_is_text_still_initialises(tmp_path, value, dry_run): exit 0, `created_by: "Jose"`. Text a file holds is not refused: an accent, a U+200C ZERO WIDTH NON-JOINER, a backslash and a quote, which the scaffold lets through as well.""" + skip_where_the_environment_cannot_carry(value) result = init(tmp_path, user_name(value), dry_run=dry_run) assert result.returncode == 0, result.stderr + result.stdout assert "REFUSED" not in result.stderr, result.stderr @@ -648,6 +670,7 @@ def test_every_other_name_no_file_can_hold_is_refused_the_same_way( asks it of the same setting: a line break, a bidi control and a control character in `git config user.name` are refused in its words, where the byte that is not UTF-8 is the case the issue lists.""" + skip_where_the_environment_cannot_carry(value) result = init(tmp_path, user_name(value), dry_run=True) assert result.returncode == 2, result.stderr + result.stdout assert result.stderr.startswith( diff --git a/tests/test_non_utf8_name_callers.py b/tests/test_non_utf8_name_callers.py new file mode 100644 index 00000000..3b45ffe0 --- /dev/null +++ b/tests/test_non_utf8_name_callers.py @@ -0,0 +1,333 @@ +# SPDX-License-Identifier: Apache-2.0 +"""A test that needs a directory named with a byte that is not UTF-8 asks the +one helper whether this system can carry it, and every other error of the disk +is the test's own (#757, #461). + +Five tests and `directory_named` each made the directory themselves and turned +ANY `OSError` into `pytest.skip`, beside `skip_where_a_name_cannot_be_not_utf_8` +(#663), which skips on the two answers that mean "this system cannot carry the +name" -- Windows' `os.fsencode` and macOS's EILSEQ -- and raises the rest. So a +disk that filled up, or a `tmp_path` this user cannot write in, read as SKIPPED +there and as a failure in the helper's own tests, and no run fails on a skip. + +Each caller is called here as a plain function, with the disk answering as it +would on the systems this suite does not run on (CI is Linux only, #597), and +asked what it did: + +- the PROBE is refused with EILSEQ (macOS): the test skips, on the helper's + reason; +- the PROBE is refused with EACCES: the test fails with `PermissionError`; +- the test's OWN directory is refused with EACCES, the probe not: the test + fails with `PermissionError`, where it skipped. These rows let the probe + through to the real disk, so they ask it first and skip where it answers + EILSEQ (macOS's APFS, a ZFS with `utf8only`): they would FAIL there on a + system that holds no defect. + +Windows is emulated too, for the one caller whose own question is wider than +the helper's: `directory_named` skips there for a control character, which +Windows refuses in any file name, and for no other name that is text. No +Windows run was made. + +The last groups hold the C-locale half of the same issue. Under +`LC_ALL=C PYTHONUTF8=0 PYTHONCOERCECLOCALE=0` the file name encoding is ASCII, +so `make_file` cannot write `caf\\u00e9.yml` and `subprocess` cannot put a +`user.name` of `Jos\\u00e9` into a child's environment: the three tests that +do it skip where this system's encoding cannot, held here with an ASCII +`os.fsencode`, and for that reason ALONE: a write the disk refuses (EACCES, +ENOSPC) still fails the `make_file` test, which a skip on `make_file`'s own +"filesystem" reason would have turned into a skip. +""" + +from __future__ import annotations + +import errno +import os +from pathlib import Path + +import pytest + +import test_adopt_git_reads as git_reads +import test_adopt_plan_escapes as plan_escapes +import test_adopt_template_values as template_values +import test_clone_url_bytes as clone_url_bytes +import test_non_utf8_git_reads as non_utf8_git_reads +import test_stdout_encoding as stdout_encoding +from conftest import (NOT_UTF_8_PROBE, skip_message, + skip_where_a_name_cannot_be_not_utf_8, windows_fsencode) +# The fixture of `test_adopt_plan_escapes.py`, `adopt-project.py` loaded as a +# module, which two of its tests ask for. +from test_adopt_plan_escapes import adopter # noqa: F401 - fixture + +#: Where the probe of the helper is: a directory named `probe`. +PROBE = "probe" + +#: Every caller of the helper, as `(id, the call, whether it makes a directory +#: of its own after the probe)`; the call is given a `tmp_path` and the +#: `adopter` fixture. `directory_named` is called with the name the issue's +#: own row uses, `caf` and byte 0xE9. +CALLERS = [ + pytest.param( + lambda tmp_path, adopter: stdout_encoding + .test_a_name_that_is_not_utf8_still_comes_out_as_its_own_bytes( + tmp_path, "utf-8:surrogateescape"), + True, id="stdout-encoding"), + pytest.param( + lambda tmp_path, adopter: git_reads + .test_a_utf8_clone_of_a_source_whose_path_is_not_utf8_adopts(tmp_path), + True, id="adopt-git-reads"), + pytest.param( + lambda tmp_path, adopter: template_values + .test_execute_refuses_a_local_source_directory_that_is_not_utf8( + tmp_path), + True, id="adopt-template-values"), + pytest.param( + lambda tmp_path, adopter: plan_escapes + .test_a_source_directory_that_is_not_utf8_is_shown_spelled( + adopter, tmp_path), + True, id="adopt-plan-escapes-directory"), + pytest.param( + lambda tmp_path, adopter: clone_url_bytes.directory_named( + tmp_path, b"caf\xe9"), + True, id="clone-url-bytes"), + pytest.param( + lambda tmp_path, adopter: plan_escapes + .test_a_leg_extracts_a_path_that_is_not_utf8(adopter, tmp_path), + False, id="adopt-plan-escapes-file"), +] +EVERY_CALLER = [pytest.param(row.values[0], id=row.id) for row in CALLERS] +THOSE_WITH_A_DIRECTORY_OF_THEIR_OWN = [ + pytest.param(row.values[0], id=row.id) for row in CALLERS if row.values[1]] + + +def refusing(code: int, *, probe: bool): + """An `os.mkdir` that fails with `code` for the probe (`probe=True`) or + for every directory but the probe, and makes the rest. `Path.mkdir` calls + it too.""" + real = os.mkdir + + def mkdir(path, *args, **kwargs): + name = os.path.basename(os.fsdecode(path)) + if name.startswith(PROBE) is probe: + raise OSError(code, os.strerror(code), os.fsdecode(path)) + return real(path, *args, **kwargs) + return mkdir + + +def fail_where_the_caller_ends(call, *args) -> None: + """Fail where `call(*args)` ends without an error, SKIPPED or not: the + caller was to raise. A `pytest.raises` block may hold one call and no + more (python:S5778), so the failure is made here and not beside it.""" + reason = skip_message(call, *args) + pytest.fail("the caller ended " + + (f"SKIPPED: {reason}" if reason else "without an error")) + + +@pytest.fixture +def a_system_that_carries_the_byte(): + if len(os.fsencode(NOT_UTF_8_PROBE)) != 1: + pytest.skip("on this system the first question is the one that skips") + + +@pytest.fixture +def a_disk_that_takes_the_probe(tmp_path, a_system_that_carries_the_byte): + """The rows that let the probe through to the real disk ask it first: + macOS's APFS, or a ZFS with `utf8only`, answers EILSEQ to it, and those + rows would FAIL there on a system that holds no defect.""" + skip_where_a_name_cannot_be_not_utf_8(tmp_path) + + +# --- the disk's refusals, on the systems CI does not run -------------------- + +@pytest.mark.usefixtures("a_system_that_carries_the_byte") +@pytest.mark.parametrize("call", EVERY_CALLER) +def test_a_disk_that_refuses_the_name_with_eilseq_skips_the_caller( + tmp_path, monkeypatch, adopter, call): + """macOS (emulated): the probe is refused as `Illegal byte sequence`, so + the test is SKIPPED, on the helper's reason.""" + monkeypatch.setattr(os, "mkdir", refusing(errno.EILSEQ, probe=True)) + reason = skip_message(call, tmp_path, adopter) + assert reason is not None, "the caller went on past the probe's EILSEQ" + assert "EILSEQ" in reason, reason + + +@pytest.mark.usefixtures("a_system_that_carries_the_byte") +@pytest.mark.parametrize("call", EVERY_CALLER) +def test_a_disk_that_refuses_the_probe_with_eacces_fails_the_caller( + tmp_path, monkeypatch, adopter, call): + """A folder nobody can write in: the helper raises it, as its own tests + hold, so the test FAILS, and no run reads it as a skip.""" + monkeypatch.setattr(os, "mkdir", refusing(errno.EACCES, probe=True)) + with pytest.raises(PermissionError): + fail_where_the_caller_ends(call, tmp_path, adopter) + + +@pytest.mark.usefixtures("a_disk_that_takes_the_probe") +@pytest.mark.parametrize("call", THOSE_WITH_A_DIRECTORY_OF_THEIR_OWN) +def test_a_disk_that_refuses_the_tests_own_directory_fails_the_test( + tmp_path, monkeypatch, adopter, call): + """The probe is made and removed, and then the directory the test means + to use is refused with EACCES: that is the test's own, raised, where it + was a skip that said "no directory here is named ...".""" + monkeypatch.setattr(os, "mkdir", refusing(errno.EACCES, probe=False)) + with pytest.raises(PermissionError): + fail_where_the_caller_ends(call, tmp_path, adopter) + + +@pytest.mark.usefixtures("a_system_that_carries_the_byte") +def test_the_fixture_that_guards_those_rows_asks_the_real_disk( + tmp_path, monkeypatch, request): + """`a_disk_that_takes_the_probe` skips where the disk answers EILSEQ to the + probe, as APFS does; asked here after the disk is made to, because a + fixture runs before the test body and Linux's disk would never say it.""" + monkeypatch.setattr(os, "mkdir", refusing(errno.EILSEQ, probe=True)) + reason = skip_message(request.getfixturevalue, + "a_disk_that_takes_the_probe") + assert reason is not None, "the fixture let a refused probe through" + assert "EILSEQ" in reason, reason + + +# --- `directory_named`'s own question: Windows, emulated -------------------- + +def windows_names_it(monkeypatch) -> None: + """Windows, as far as `skip_where_this_system_cannot_name` can ask it: + `os.name` is `nt` and `os.fsencode` gives a surrogate three bytes.""" + monkeypatch.setattr(os, "name", "nt") + monkeypatch.setattr(os, "fsencode", windows_fsencode) + + +def test_where_windows_cannot_carry_the_byte_directory_named_skips_and_makes_nothing( + tmp_path, monkeypatch): + """`directory_named` itself is called, so that it asks before it makes + anything: a version that skipped after its `os.mkdir` would leave a + directory here, or fail on `os.fsencode` of a path under the emulation.""" + windows_names_it(monkeypatch) + reason = skip_message(clone_url_bytes.directory_named, tmp_path, + b"caf\xe9") + assert reason is not None, "a name that is not UTF-8 was not skipped" + assert "os.fsencode" in reason, reason + assert list(tmp_path.iterdir()) == [] + + +@pytest.mark.parametrize("raw", [b"line\nbreak", b"a\x1bb"], + ids=["line-feed", "escape"]) +def test_where_windows_refuses_a_control_character_directory_named_skips( + tmp_path, monkeypatch, raw): + windows_names_it(monkeypatch) + reason = skip_message(clone_url_bytes.directory_named, tmp_path, raw) + assert reason is not None, f"{raw!r} was not skipped on Windows" + assert "control character" in reason, reason + assert list(tmp_path.iterdir()) == [] + + +def test_where_windows_holds_a_bidi_override_the_name_check_does_not_skip( + tmp_path, monkeypatch): + """The row #459 names (R433 O5): NTFS holds U+202E, so the `bidi-override` + row of the clone-URL tests runs on Windows as it does here. Reasoned, and + asked here only of the skip: no Windows run was made. It asks + `skip_where_this_system_cannot_name` and not `directory_named`, which + goes on to make a `Path`, and under an emulated `os.name` of `nt` that is + a `WindowsPath`, which Linux cannot make.""" + windows_names_it(monkeypatch) + raw = "\u202eevil".encode("utf-8") + assert skip_message(clone_url_bytes.skip_where_this_system_cannot_name, + tmp_path, raw) is None + + +@pytest.mark.skipif(os.name == "nt", reason="Windows is the case above") +@pytest.mark.parametrize("raw", [b"line\nbreak", b"a\x1bb"], + ids=["line-feed", "escape"]) +def test_a_system_that_is_not_windows_makes_a_control_character_directory( + tmp_path, raw): + """The control: a POSIX disk takes the name, so no skip.""" + assert skip_message(clone_url_bytes.skip_where_this_system_cannot_name, + tmp_path, raw) is None + assert os.path.basename(clone_url_bytes.directory_named( + tmp_path, raw)) == os.fsdecode(raw) + + +# --- under an ASCII file name encoding (LC_ALL=C PYTHONUTF8=0) -------------- + +def ascii_fsencode(value): + """`os.fsencode` where the file name encoding is ASCII, with + `surrogateescape`: text outside ASCII raises, and a lone surrogate that + stands for a byte is that byte.""" + value = os.fspath(value) + if isinstance(value, bytes): + return value + return value.encode("ascii", "surrogateescape") + + +MAKE_FILE_TEST = ( + non_utf8_git_reads + .test_make_file_writes_a_name_that_is_utf8_where_fsencode_makes_no_byte) + + +def test_make_files_utf8_name_test_skips_where_the_encoding_is_ascii( + tmp_path, monkeypatch): + """`make_file` cannot write `caf\\u00e9.yml` where the file name encoding is + ASCII, so the test about the guard failed on `assert skip_message(...) + is None`, on a system that holds no defect. It asks the encoding first, + and is SKIPPED on that reason.""" + monkeypatch.setattr(os, "fsencode", ascii_fsencode) + reason = skip_message(MAKE_FILE_TEST, tmp_path, monkeypatch) + assert reason is not None, "the test went on past an ASCII encoding" + assert "file name encoding" in reason, reason + + +@pytest.mark.parametrize("code", [errno.EACCES, errno.ENOSPC], + ids=["EACCES", "ENOSPC"]) +def test_a_write_the_disk_refuses_still_fails_the_make_file_test( + tmp_path, monkeypatch, code): + """The name is plain UTF-8, so a refused write is the disk's and not the + encoding's: the test FAILS on its assert, as it does without this change. + A skip on `make_file`'s own "filesystem" reason would end it SKIPPED, and + no run fails on that.""" + non_utf8_git_reads.skip_where_the_environment_cannot_carry( + non_utf8_git_reads.UTF8) + + def write_bytes(self, data): + raise OSError(code, os.strerror(code), str(self)) + monkeypatch.setattr(Path, "write_bytes", write_bytes) + with pytest.raises(AssertionError): + fail_where_the_caller_ends(MAKE_FILE_TEST, tmp_path, monkeypatch) + + +@pytest.mark.parametrize("value", ["Jos\u00e9", "Mehr\u200cdad", "\u202eevil"], + ids=["accent", "zwnj", "u202e"]) +def test_a_user_name_the_encoding_cannot_carry_skips_where_it_is_ascii( + monkeypatch, value): + monkeypatch.setattr(os, "fsencode", ascii_fsencode) + reason = skip_message( + non_utf8_git_reads.skip_where_the_environment_cannot_carry, value) + assert reason is not None, f"{value!r} was carried by an ASCII encoding" + assert "file name encoding" in reason, reason + + +@pytest.mark.parametrize("value", ["Jose", "Jos\udce9", "Ada\nLovelace", + "a\x1bb"], + ids=["ascii", "byte", "line-feed", "escape"]) +def test_a_user_name_the_encoding_carries_is_not_skipped_even_where_it_is_ascii( + monkeypatch, value): + """ASCII text, a lone surrogate that is a byte, and the control characters + are the environment's own: the tests of them still run.""" + monkeypatch.setattr(os, "fsencode", ascii_fsencode) + assert skip_message( + non_utf8_git_reads.skip_where_the_environment_cannot_carry, + value) is None + + +@pytest.mark.parametrize("test, args", [ + pytest.param("test_a_user_name_that_is_text_still_initialises", + ("Jos\u00e9", True), id="text-still-initialises"), + pytest.param("test_every_other_name_no_file_can_hold_is_refused_the_same_way", + ("\u202eevil", "it holds a bidirectional control"), + id="every-other-name"), +]) +def test_the_two_tests_that_pass_the_name_in_an_environment_skip_where_it_is_ascii( + tmp_path, monkeypatch, test, args): + """Both of them fail on the `UnicodeEncodeError` that `subprocess` raises + in the test (the tool is not reached), under an ASCII encoding.""" + monkeypatch.setattr(os, "fsencode", ascii_fsencode) + reason = skip_message(getattr(non_utf8_git_reads, test), tmp_path, *args) + assert reason is not None, "the test ran, and raised or passed on ASCII" + assert "file name encoding" in reason, reason diff --git a/tests/test_stdout_encoding.py b/tests/test_stdout_encoding.py index 252d1d51..03283488 100644 --- a/tests/test_stdout_encoding.py +++ b/tests/test_stdout_encoding.py @@ -51,7 +51,8 @@ import pytest from conftest import (ADOPT, ORG, PROJECT, REPO, SCAFFOLD, - blank_unnamed_pin_sources, git, make_source_repo) + blank_unnamed_pin_sources, git, make_source_repo, + skip_where_a_name_cannot_be_not_utf_8) sys.path.insert(0, str(REPO / "scripts")) import shape_materialize # noqa: E402 @@ -206,11 +207,9 @@ def test_a_name_that_is_not_utf8_still_comes_out_as_its_own_bytes(tmp_path, `日本.md` -- escaped on cp1252 -- where plain `backslashreplace` would print `\\udcff` instead. POSIX only: neither Windows nor macOS holds a file name that is not UTF-8.""" + skip_where_a_name_cannot_be_not_utf_8(tmp_path) parent = os.path.join(os.fsencode(tmp_path), b"b\xffd") - try: - os.mkdir(parent) - except (OSError, UnicodeError) as exc: - pytest.skip(f"this filesystem holds no name that is not UTF-8: {exc}") + os.mkdir(parent) if b"b\xffd" not in os.listdir(os.fsencode(tmp_path)): pytest.skip("this filesystem rewrote a name that is not UTF-8") source = make_source_repo(tmp_path / "Thing", tree={