Repository navigation
fix(tools): use UTF-8 for patch advisor I/O - #10670
Conversation
MarcZX10
left a comment
There was a problem hiding this comment.
The production change looks sound, and the exact-head focused suite passes locally (26/26); diff-check and conventions pass. I found one blocking supported-Python issue in the new regression test: test_relinker_output_is_utf8_independent_of_host_encoding patches subprocess._text_encoding with patch.object without create=True. That private attribute is absent from CPython 3.9 and 3.10 (it appears in the v3.11 source); the test therefore raises AttributeError before exercising the fix on supported interpreters. The repository build guide supports Python 3.9+. I reproduced the test setup failure by removing that attribute before running the new test (2 subtest errors). Please replace the private-attribute patch with a version-independent way to force the parent's default decoder to ASCII (for example, patch the locale encoding source used by those interpreters), then run the focused suite on the oldest supported Python as well as current Python.
Primary source references: CPython 3.9 Lib/subprocess.py https://github.com/python/cpython/blob/v3.9.0/Lib/subprocess.py and CPython 3.10 https://github.com/python/cpython/blob/v3.10.0/Lib/subprocess.py do not define _text_encoding; CPython 3.11 does: https://github.com/python/cpython/blob/v3.11.0/Lib/subprocess.py. The official Python 3.9 subprocess docs state that encoding and errors have been supported since 3.6: https://docs.python.org/3.9/library/subprocess.html.
|
Addressed the private-helper dependency in One version-scope clarification: the generic build guide says Python 3.9+, but Validation:
|
THEMANnt
left a comment
There was a problem hiding this comment.
Reviewed a3e2626eea63 against 66de8e7a12cb. No actionable findings in this focused review.
Source review
The advisor now configures supported output streams as UTF-8 with backslashreplace and decodes captured relinker output as UTF-8 with replacement for malformed bytes. Hash validation, verification summaries, mismatch handling, timeouts, and exit-status policy are preserved. The regressions exercise real child-process decoding and real ASCII text streams. The earlier private-API test patch is gone; the documented advisor minimum and CMake test gate are Python 3.11+. Pristine and candidate files match, with no local amendments.
Validation
- Controller-recorded Windows/Python 3.11.9 runs of
tools/tests/test_suggest_patches.py: baseline 24 passed; pristine PR and candidate each 26 passed; zero skips. - Candidate tests against baseline produced the intended
ascii != utf-8assertion failure. Two additional decoding subtest errors followed ASCII decoder failures during child-output capture; these were not setup errors. The stream control stops at its first assertion. - Recorded whitespace and conventions checks passed.
- GitHub-reported effective CI succeeded for the Ubuntu/Windows relinker jobs, both macOS relinker variants,
relinker-asan, conventions, eligibility, and labels. Linux/Windows product builds, register-context, and write-watch were skipped. The earlier cancelled conventions run was superseded by a successful matching-workflow execution.
Scope
Local validation covers the focused Python tool suite. The author's additional integration/build/interpreter checks were not independently reproduced locally; full application behavior was not tested.
roboterspinne
left a comment
There was a problem hiding this comment.
Reviewed head a3e2626 merged onto upstream/main befe264 (2026-10-24); the merge is clean, also together with my #10667 (same test file, different hunks, either order), check_conventions.py passes and pr_overlap.py reports no overlap with open pull requests.
Environment: Windows 11 Home 10.0.26200, Python 3.13.9 (Anaconda), locale code page 1252; Python only, no build needed.
Results
python -m unittest tools.tests.test_suggest_patcheson the merged tree: 25 of 26 pass. The one failure istest_uppercase_digest_and_conversion_options, which fails onmainwhenever the temp path contains a space (the tool quotes such paths, the test compares unquoted); #10667 fixes exactly that assertion, and with it merged on top all 26 pass. Red/green: againstmain'ssuggest_patches.pyboth payloads oftest_relinker_output_is_utf8_independent_of_host_encodingerror andtest_output_streams_use_utf8fails; both pass on the branch.main's failure modes reproduced: withtext=Trueand no encoding,subprocess.rundecodes with the locale encoding, cp1252 here, which never raises but turns the relinker's UTF-8 into mojibake and maps0xfftoÿ; under a code page that can fail (ascii, cp932) the decode error is raised insubprocess's reader thread and the advisor then dies at line 146 (VERIFY_SUMMARY.search(result.stdout)) withTypeError: expected string or bytes-like object, got NoneType, a traceback instead of a handled exit. The branch decodes both payloads correctly and returns the expected exit codes.- UTF-8 is the right decoder: on Windows the relinker converts its wide
argvto UTF-8 (core/relinker/main.cpp:491-497,WideCharToMultiByte(CP_UTF8, ...)) and prints paths asu8string(core/relinker/io/src/NativePath.cpp:11), so its diagnostics are UTF-8 bytes whatever the console code page;errors="replace"covers stray bytes such as OS error text. - The stream reconfigure fixes a second real failure: with a non-ASCII title or path and stdout on an ascii or cp1252 pipe,
main'sprintraisesUnicodeEncodeError, which the handler at line 188 turns into exit 2Patch advisor: ... codec can't encode, so a real verdict including theRecommended:line is lost; the branch writes UTF-8 and exits with the right code. It overridesPYTHONIOENCODING, ascheck_skips.py:49-50,build_web.py:169-170andcheck_compile.py:229already do. - The revised wrapper answers the change request:
options.setdefault("encoding", "ascii")uses only the documentedencodingkeyword (3.6+), no private helper, and forces an ASCII decode only when the advisor passes no encoding, so the test is red onmainon every platform (an ERROR via the reader thread on Windows, in the main thread on POSIX) and does not depend on the host locale orPYTHONUTF8. Removing any one part of the fix (either reconfigure, theencoding, theerrors) makes a test red. The 3.9/3.10 question is moot for this file anyway:suggest_patches.py:9importstomllibunconditionally,USAGE.md:103says the advisor needs 3.11 or newer, andcore/relinker/CMakeLists.txt:629-634registerspatch_advisor_unitandpatch_advisor_integrationonly forPython3_VERSION >= 3.11, so CI never ran these tests on older interpreters; still better without the private helper.
Nits
test_suggest_patches.py:96:test_output_streams_use_utf8asserts only the streams'encodinganderrorsafteradvise(), and the temp root is ASCII, so no non-ASCII text is ever written through them. A variant that reconfigures only after all output still passes, yet it returns 2 with'ascii' codec can't encode character '\xe9'when the root is created with prefixcafé-. Building the root with a non-ASCII prefix and asserting rc 0 plus the UTF-8 path instdout.buffer.getvalue()would pin what the fix is for.test_suggest_patches.py:82: the relinker regression runs only the exit-0 path, so it checks that the output is decoded as UTF-8 but never that the decoded diagnostic reaches the user; printing happens only in the rejected and exit-2 branches (suggest_patches.py:151-157). Optional: one subcase where the fake relinker exits 2 and the decoded text is asserted in the captured advisor output.
Notes, nothing to change here: main() now reconfigures the real sys.stdout and sys.stderr, so every in-process test that does not replace both streams leaves the runner's streams at utf-8/backslashreplace (cp1252 before, utf-8 after, here); harmless, the tests already leaked output on main, and the other tools do the same. The body's "26/26 on Windows" holds only when the temp path has no space; the five-run, Linux 3.11.0rc1, 244/244 CTest and 3.9/3.10 probe claims come without logs and I could not check them. Hosted CI is green on a3e2626 (ubuntu, windows, both macOS, ASan).
Not checked: Linux runs, Python 3.11 itself, a real relinker child with non-UTF-8 output (only python -c children with the two payloads).
I found no blocking issue: the three changes fix two failures I could reproduce, the revised test uses only documented API, and the counter-checks hold, so the open change request looks addressed to me. For disclosure: I have open PRs (#10667 on tools/tests/test_suggest_patches.py, and relinker PRs #10679, #10658, #10646, #10607, #10596, #10564, #10403, #10402, #10401, #10391, #10390); #10667 changes one assertion in test_uppercase_digest_and_conversion_options, a different test in this file, and is the fix for the one failure above.
Written with AI assistance (Claude Code). I ran the tests above on my Windows 11 machine.
| with patch.object(suggest_patches.sys, "stdout", stdout), patch.object(suggest_patches.sys, "stderr", stderr): | ||
| self.assertEqual(self.advise(), 0) | ||
| for stream in (stdout, stderr): | ||
| self.assertEqual(stream.encoding, "utf-8") |
There was a problem hiding this comment.
Nit: this asserts only the streams' attributes after advise(), and the temp root is ASCII, so no non-ASCII text is ever written through them. A variant that reconfigures only after all output still passes, yet it returns 2 with 'ascii' codec can't encode character '\xe9' when the root is created with prefix café-. Building the root with a non-ASCII prefix and asserting rc 0 plus the UTF-8 path in stdout.buffer.getvalue() would pin what the fix is for.
| return result | ||
|
|
||
| with patch.object(suggest_patches.subprocess, "run", side_effect=verify): | ||
| self.assertEqual(self.advise(), 0) |
There was a problem hiding this comment.
Nit, optional: this runs only the exit-0 path, so it checks that the output is decoded as UTF-8 but never that the decoded diagnostic reaches the user; printing happens only in the rejected and exit-2 branches (suggest_patches.py:151-157). One subcase where the fake relinker exits 2 and the decoded text is asserted in the captured advisor output would cover that.
|
Addressed both output-coverage nits in
Negative controls: moving stream reconfiguration after output passes the old stream test but fails the revised Unicode test; hiding the decoded diagnostic fails both exit-2 subcases. Production code is unchanged. Both Windows advisor CTests pass five runs each; the Linux Python 3.11 full 26-test unit suite and native integration also pass five runs each. Conventions and whitespace checks pass, with no timeout changes. |
MarcZX10
left a comment
There was a problem hiding this comment.
Reviewed exact head e49a927811fefea42be1df2c41e3387f2da340a5. The UTF-8 subprocess decoding and stream reconfiguration are covered by tests for non-ASCII and invalid-byte diagnostics, successful and failing relinker exits, and non-ASCII output paths. The latest test commit also addresses the earlier gaps around asserting user-visible diagnostics and non-ASCII paths. I ran python3 -m unittest tools.tests.test_suggest_patches on this head (24/24 passed), plus git diff --check and the conventions checker (passed). Hosted eligibility, labels and conventions pass; remaining relinker matrix checks are unrelated to these Python-only files.
|
@Bram910 Thanks, checked e49a927 merged onto Written with AI assistance (Claude Code). |
What
Use UTF-8 consistently for the patch advisor's standard streams and captured relinker output. The relinker prints UTF-8, but
subprocess.run(text=True)previously decoded it using the host locale. On a legacy Windows code page or an ASCII locale, non-ASCII diagnostics can be corrupted or fail decoding. Separately, ASCII-configured advisor output streams cannot print a valid recommendation whose path contains non-ASCII characters.Configure standard streams as UTF-8 with
backslashreplace, following the other tools. Decode relinker stdout/stderr as UTF-8 with replacement for malformed diagnostic bytes. Hash validation, verification summaries, mismatch handling and exit-status policy are unchanged.Add regressions using a real Python child that emits UTF-8 and malformed bytes under a forced ASCII subprocess codec, plus actual ASCII
TextIOWrapperstreams. This is separate from #10667's path-quoting assertion fix; both merge orders are clean and the combined unit suite passes.Review correction: the real-child wrapper now uses the documented
encodingargument withoptions.setdefault("encoding", "ascii")to provide a deterministic legacy-codec fallback only when the advisor omits its explicit encoding. No private subprocess helper or locale internals are patched. The child's actual UTF-8/malformed output is still decoded by the real subprocess implementation.Tested
Output-coverage follow-up head
e49a927811fefea42be1df2c41e3387f2da340a5changes tests only. The ASCII-stream case now prints an actual Unicode catalog path to stdout and a Unicode missing-file diagnostic to stderr, checks the UTF-8 bytes, and checks both success/error statuses. Real relinker-child tests exercise both exit 0 and exit 2 for each diagnostic payload and assert the decoded stdout/stderr reach the user on exit 2.Follow-up validation: 26/26 unit tests pass; both Windows advisor CTests pass five times each, and the full Linux Python 3.11 unit suite and native integration each pass five times. A mutant that reconfigures streams only after output passes the old stream test but fails the revised test; suppressing decoded diagnostics fails both exit-2 payload subcases. No timeout or production-code changes.
Branch base: upstream
mainate7e99adcc4580e7dc1337258b3099161105847b1; original published head8d8b2496ed163c5611001ebe1e01b440d2b706cd. Rebased after initial validation; intervening upstream changes affect only shader code, not either changed file. The 26-test unit suite on both Windows and Linux, both registered Windows patch-advisor CTests, and conventions checks pass again after that rebase.Review-correction head
a3e2626eea63c29887a4055ad60aea0287183307: production code is unchanged. 26/26 unit tests pass on Windows Python 3.13.5 and Linux Python 3.11.0rc1; both registered Windows advisor CTests pass five runs each, and the Linux unit suite and native relinker integration each pass five runs. Revised regressions still fail against unchanged main for the encoding bugs, without private-helper setup errors. Conventions and whitespace checks pass.Python-version scope:
core/relinker/CMakeLists.txtalready registers these two advisor tests only for Python 3.11+, and the existing advisor importstomllib. The full advisor suite was therefore not run on 3.9/3.10. Separate real-child decoder probes pass on actual Windows CPython 3.9.13 and Linux CPython 3.10.12: the documented ASCII fallback rejects both UTF-8 and malformed payloads, while explicit UTF-8 with replacement decodes both stdout and stderr correctly. No TOML shim, new dependency or lowered version gate was added.Hosted CI on original published head
8d8b2496ed163c5611001ebe1e01b440d2b706cd: Linux, Windows and both macOS relinker variants, ASan, conventions, eligibility, intake and labels succeed. Full-product Linux/Windows, register-context and write-watch were skipped. These completed runs precede the test-only review correction.Full Windows Ninja Release relinker-only build and CTest: 244/244 passed in 195.43 seconds on pre-rebase head
d629d7c866de857a4b8968a3b82450e87ecd5449(based01070d6c3). Both changed files are identical after the rebase.Red/green: both new regression methods fail against unchanged main on Windows Python 3.13.5 and Linux Python 3.11.0rc1, then pass with this fix. The child writes directly to both file descriptors, so its UTF-8 bytes do not depend on its own standard-stream encoding.
python -m unittest tools.tests.test_suggest_patches: 26/26 passed on Windows and Linux.Real relinker CLI red/green on Windows and native Linux: use a Unicode input filename and directory (
café-にほん) withPYTHONIOENCODING=ascii, and test both matching and mismatching patch bytes. Main fails both cases (Windows exits 1; Linux exits 2). The fix recommends the valid patch with exit 0, rejects mismatching bytes with exit 1, preserves the Unicode paths in output, and leaves the input ELF unchanged.Windows: both registered
patch_advisor_unitandpatch_advisor_integrationCTests pass five consecutive runs each, with no timeout changes.Ubuntu 22.04 under WSL, Python 3.11.0rc1: repeat the entire 26-test unit suite and the existing real-relinker integration script five times each; all pass. The native Linux relinker was rebuilt successfully with GCC 11.4.
Merge compatibility:
git merge-tree --write-treein both orders with test(tools): compare quoted and resolved paths in the patch advisor test #10667's head76008e99b9bf02bcbcbec2ed0d3c5380204cde2bproduces the same clean tree. Run the combined unit test file in memory against this fix: 26/26 passed, including test(tools): compare quoted and resolved paths in the patch advisor test #10667's space-containing temporary roots.The repository's open-PR overlap scanner reports no overlap. Conventions and whitespace checks pass. No title, GPU or full-product testing; this is Python tool I/O handling.
Checklist
main; no other open PR implements the same functionsNotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file.mdfiles or images added to the repository (attach them to this PR)