diff --git a/CLAUDE.md b/CLAUDE.md index b6c4797e..1e06c5b8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1025,27 +1025,6 @@ way (no live Windows execution available here), that is noted explicitly rather plain-language message naming Constrained Language Mode specifically if it fails, rather than letting the failure surface piecemeal as five-plus separate "Could not write ~x" messages later. -- **Item 52: `tools/pyproj_deps.py`'s exit code 1 is overloaded between its intentional - "no `[project].dependencies` found" contract and a catch-all for any genuinely unexpected - exception, making a real bug in that script indistinguishable from the normal case.** CONFIRMED - directly against `tools/pyproj_deps.py` source and `run_setup.bat`'s own consumption of it, in - the pyproject.toml dependency-extraction block (`if exist "pyproject.toml" (...)`, the block that - calls `:emit_from_base64 "~pyproj_deps.py" HP_PYPROJ_DEPS`). The documented contract (exit - 0/1/2 = ok/not-found/malformed-TOML) is - correct and intentional -- `run_setup.bat`'s silent no-op on exit 1 is CORRECT for the - "not-found" case, not a bug (an earlier external review of this same code mischaracterized this - as "swallowing a standard exception," which is not accurate -- exit 1 for "not found" is by - design). The real, narrower gap: `pyproj_deps.py`'s own top-level `except Exception: - sys.exit(1)` catch-all means a genuinely unexpected exception ALSO exits 1, so - `run_setup.bat`'s `if errorlevel 1 ( if errorlevel 2 (...) )` structure -- which only logs a - WARN for errorlevel >= 2 -- silently treats a real crash exactly like the benign "nothing to do - here" case. Low severity (the script is small and stable; this would only bite if a future - Python version or TOML edge case triggers an unhandled exception somewhere not already caught by - the script's own narrower `except` blocks) and low priority given that. Fix, if picked up: - either a distinct exit code for the top-level catch-all (e.g. 3), or an unconditional low-tier - log line (not a WARN) on any errorlevel 1 so the fact is at least visible in `~setup.log` for a - future debugging session, without changing user-facing behavior. - - **Item 59: CodeRabbit's automated review did not run on PR #435, and should be manually triggered on every future PR -- owner-requested standing process fix, not a code change.** This repo (fewer than 10 stars, Organization UI config) requires a manual trigger for @@ -1106,6 +1085,79 @@ way (no live Windows execution available here), that is noted explicitly rather OLD rules before running the bootstrapper, asserting the file ends up with `-text` and no leftover `eol=crlf` line for `*.bat`/`*.cmd`. +- **Item 61: `check_delimiters.py` does not catch a cross-line `(`/`)` pair inside `rem` comment + text, even though cmd.exe's own parser is just as vulnerable to it as it is for `echo` text -- + a real gap that caused a genuine CI-breaking regression across all 8 lanes (PR #445, Item 52's + own fix).** `docs/agent-lessons-learned.md`'s "A literal `(`/`)` inside `echo` text..." entry + documents both the original 2026-07 echo-text incident AND this newly-confirmed `rem`-text + sibling -- read that entry for the full mechanism and incident trace before starting this item. + + **Root cause, already diagnosed**: `check_delimiters.py`'s `.bat`/`.cmd` handling treats a `rem` + line as fully opaque (`if upper.startswith("REM ") or ...: continue`, skipping it from + paren-scanning entirely) instead of scanning its characters the way the `echo`-line path does + (`is_bat_echo_line`, tracked via `is_echo_open` on the bracket stack, flagging a `(` that opens + on an echo line already nested inside a real block and closes on a LATER line). Real cmd.exe + does not distinguish `rem` from `echo` for this purpose -- its block-closing search is a raw + character scan across the whole block's text regardless of which command a given line belongs + to -- so `rem` comments are exactly as exposed to this hazard as `echo` text, but the checker + only defends the `echo` case today. + + **High-level fix**: extend the existing `is_echo_open`/bracket-stack machinery to also apply to + `rem` lines -- drop the current `continue`-and-skip shortcut for REM lines, route them through + the same character scan `echo` lines already get, and reuse the identical "already nested inside + an open bracket, closes on a different line" flagging logic (scoped the same way, so a harmless + top-level `rem` header block with no enclosing `if`/`for` doesn't false-positive -- this repo's + own file header, `run_setup.bat` lines 1-40ish, has several legitimately-balanced-per-line or + intentionally `^`-escaped parens that must stay clean). + + **Scope note, NOT yet done as part of the Item 52 fix that surfaced this**: a full audit of + every PRE-EXISTING cross-line `rem`-comment paren pair already in `run_setup.bat` (there are + many, scattered throughout the file's ~5300 lines) was explicitly NOT performed -- Item 52's own + fix only reworded the ONE `rem` block it had just introduced and broken. Whether any of the + pre-existing ones are ALSO genuinely hazardous (nested inside a real open block, not just a + top-level header) is unknown and unverified; the checker fix above would surface them + automatically once implemented -- do not assume the file is currently clean of other latent + instances of this same bug just because CI has been green so far (an existing hazard only + manifests when the SPECIFIC surrounding code happens to also be reached/reparsed in a way that + exposes it, exactly as this one sat undetected until Item 52 added new code near it). + + **Coverage gap to close in the same slice**: `tests/test_check_delimiters_import.py`'s existing + `test_paren_*` cases cover the `echo`-line hazard; add an analogous `rem`-line case (a `rem` + block whose `(` opens on one line and matching `)` closes on a later line, nested inside a real + `if`/`for` block) proving the extended checker catches it, plus a negative case (a top-level + `rem` header block with no enclosing bracket) proving it doesn't false-positive. + + **Scope WIDENED, same PR (#445), via a second real CI incident on the SAME code block: a + SAME-LINE, self-contained, balanced `(`/`)` pair -- not just cross-line pairs -- can ALSO corrupt + parsing when nested deep enough inside real `if (...)` blocks, contradicting this checker's own + (and this repo's own documented) assumption that same-line pairs are unconditionally safe.** + After the rem-comment fix above was pushed, CI still failed identically; root-caused via a + downloaded diagnostics artifact's real `~envsmoke_bootstrap.log` showing the exact same + corruption signature as the original PR #408 incident (`falling was unexpected at this time.`), + traced to a NEW `>> "%LOG%" echo ... (exit 3); falling back ...` line whose `(exit 3)` pair opens + and closes on the SAME line -- yet still corrupted parsing, nested FOUR levels deep. This is a + DIFFERENT shape from `:print_fastpath_ambiguous_note`'s own precedent (a plain top-level `echo` + with no enclosing block, confirmed safe) -- whether the redirection prefix (`>>` before `echo`) + or the nesting depth (4, one deeper than any previously-confirmed case) is the actual + distinguishing condition was NOT isolated; the fix (remove the parens) resolved it regardless. + See `docs/agent-lessons-learned.md`'s corresponding entry and `docs/agent-closed-backlog.md`'s + Item 52 entry for the full trace, and `tests/test_check_delimiters_import.py`'s + `test_paren_pair_on_redirected_echo_line_deeply_nested_is_a_known_false_negative` for a + regression fixture documenting the checker's current false-negative on this exact shape. + + **Revised item scope**: the high-level fix above (extend `is_echo_open`-style tracking to `rem` + lines) is necessary but NOT sufficient on its own -- it still only catches CROSS-line pairs. + Whoever picks up this item should ALSO investigate whether extending the same-line-pair + "always safe" assumption is correct at all once genuinely nested (vs. top-level), and if not, + design a check for that case too (e.g. flag ANY `(`/`)` pair -- same-line or cross-line -- found + inside `echo`/`rem` text that is already nested inside a real open bracket, not just cross-line + ones) -- balanced against the real risk of false-positiving on the MANY existing, presumably-safe + same-line nested echo statements already in `run_setup.bat` (not audited; needs its own careful + pass, likely requiring live-cmd.exe verification per this repo's own established practice for + this hazard class, not static reasoning alone -- static reasoning about this exact hazard class + has now been wrong multiple times in this repo's history, per `docs/agent-lessons-learned.md`'s + "`:log` echoes UNQUOTED" entry's own general warning). + ## Cold Storage (promising ideas, deliberately shelved -- revisit only if a named trigger fires) Moved to `docs/agent-cold-storage.md` (2026-07-31, to reduce this file's per-session context diff --git a/docs/agent-closed-backlog.md b/docs/agent-closed-backlog.md index 7ff04136..45a867b0 100644 --- a/docs/agent-closed-backlog.md +++ b/docs/agent-closed-backlog.md @@ -2460,6 +2460,108 @@ run of the same regex logic before landing, not just reasoned about). the expanded (wrong) path instead of the real subfolder and the hint would never fire. `docs/agent-ndjson.md` and `batch-check.yml`'s "Upload test logs" step updated to match. +### Item 52 (closed 2026-08-18) + +- **`tools/pyproj_deps.py`'s exit code 1 was overloaded between its intentional "no + `[project].dependencies` found" contract and a catch-all for any genuinely unexpected + exception, making a real bug in that script indistinguishable from the normal case.** The + documented contract (exit 0/1/2 = ok/not-found/malformed-TOML) was correct and intentional -- + `run_setup.bat`'s silent no-op on exit 1 was already correct for the "not-found" case. The real + gap: the script's own top-level `except Exception: sys.exit(1)` catch-all meant a genuinely + unexpected exception (a bug, an unusual I/O failure) also exited 1, so `run_setup.bat`'s `if + errorlevel 1 ( if errorlevel 2 (...) )` structure -- which only logs a WARN for errorlevel >= 2 + -- silently treated a real crash exactly like the benign "nothing to do here" case. + + **Fix shipped**: the catch-all now exits 3 instead of 1, and `run_setup.bat`'s consuming block + (the pyproject.toml dependency-extraction block right after `:emit_from_base64 "~pyproj_deps.py" + HP_PYPROJ_DEPS`) gained a new `if errorlevel 3` branch, checked BEFORE `if errorlevel 2` (since + `if errorlevel N` is a `>=N` test, checking the higher threshold first is required or errorlevel + 3 would also satisfy the errorlevel-2 check and get mislabeled as a TOML parse error) -- logs an + unconditional, log-file-only line (`>> "%LOG%" echo ...`, not `call :log`, so it does not also + echo to console -- per the item's own "without changing user-facing behavior" scope) so the fact + is at least visible in `~setup.log` for a future debugging session. + + **A real bug found and fixed while implementing this, caught by the existing test suite, not + found in review.** The first version of the fix broke `test_no_pyproject_toml_exits_1` -- + a MISSING `pyproject.toml` (the common, documented "not found" case, and also the case any + bare invocation of this script outside `run_setup.bat`'s own `if exist "pyproject.toml"` guard + would hit) raises `FileNotFoundError` at the same `read_text()` call site as the genuinely + unexpected cases (a directory named `pyproject.toml`, a permission failure) -- both fell into + the SAME outer `except Exception:` block, so simply changing that block's exit code broke the + documented exit-1 contract for the primary, most common case. + + **A second real bug found via CodeRabbit's review on PR #445, in the FIRST fix for the bug + above.** That fix checked `pathlib.Path('pyproject.toml').exists()` explicitly before + `read_text()`, exiting 1 immediately if absent. CodeRabbit (backed by a real web search against + CPython's own pathlib changelog, not just static reasoning) found that Python 3.14+ changed + `Path.exists()` (and `is_dir`/`is_file`/etc.) to swallow ANY `OSError` -- including + `PermissionError` -- and return `False` instead of raising, specifically to make these query + methods consistent with `os.path.exists()`. Since this bootstrapper's own embedded-helper + Python baseline explicitly targets always-latest conda-forge Python (3.14 is already the + current stable, already in the REQ-009 Tier 5 embed version table's own `LATEST_MINOR`), a + genuinely permission-denied `pyproject.toml` on 3.14+ would make `exists()` return `False` -- + silently misclassified as "not found" (1) instead of the intended "real error" (3), the exact + ambiguity this whole fix exists to close. Fixed by removing the `exists()` check entirely and + instead catching `FileNotFoundError` specifically around the `read_text()` call itself -- any + OTHER exception (a directory, a permission failure, or anything else) is not caught by this + narrower handler and falls through to the outer `except Exception:` (exit 3), while a genuinely + missing file still exits 1 via the specific `FileNotFoundError` catch. + + **Regression coverage**: `tests/test_pyproj_deps.py::TomllibPath::test_unexpected_exception_exits_3_not_1` + creates `pyproject.toml` as a DIRECTORY (not a file) -- cross-platform (raises + `IsADirectoryError` on POSIX, `PermissionError` on Windows; either way uncaught by the narrower + `FileNotFoundError` handler) -- and asserts exit code 3, not 1. The pre-existing + `test_no_pyproject_toml_exits_1` (genuinely missing file) continues to assert exit code 1, now + exercising the new `FileNotFoundError`-specific catch instead of incidentally sharing the same + catch-all as the directory case. `HP_PYPROJ_DEPS` payload re-synced via `tools/sync_payload.py` + after each fix. + + **A third real bug found the same day, this one a genuine CI-breaking regression across ALL 8 + lanes (`real`, `uv`, `contract-uv`, `contract-uv-fail`, `uv-dl-fallback`, `justme-test`, `cache` + all failed identically) -- confirmed via real CI logs, not caught locally by any tool in this + repo's own sanity sweep.** The `run_setup.bat` consuming block's own `rem` comment (explaining + "check the highest threshold first" for the new `if errorlevel 3`/`if errorlevel 2` dispatch) + split a parenthetical remark's `(`/`)` across THREE separate `rem` lines, nested three levels + deep inside real `if (...)` blocks. This is the identical hazard class already documented and + fixed once before for `echo` text (`docs/agent-lessons-learned.md`'s "A literal `(`/`)` inside + `echo` text..." entry, PR #408) -- cmd.exe's block-closing parser counts `(`/`)` characters in + `rem` comment text exactly the same way it does in `echo` text, with no concept of "this is just + a comment." `check_delimiters.py` did NOT catch it: unlike its `echo`-line handling (which scans + characters and tracks cross-line-opened parens), its `.bat`/`.cmd` path treats `rem` lines as + fully opaque and skips them from paren-scanning entirely -- a genuine, previously-undiscovered + blind spot in the checker itself, not just a one-off authoring mistake. Every CI lane doing a + real bootstrap failed with zero console output a few seconds in, since cmd.exe must determine + where the corrupted block's raw text ends BEFORE it can decide whether to execute or skip it -- + the corruption happens at parse time, independent of whether `pyproject.toml` itself exists on + disk for any given test fixture. Fixed by rewording the comment to avoid the literal `(`/`)` + characters entirely (` -- ` in place of the parenthetical), per the identical rule the `echo` + case already established. **The general checker gap (extending `check_delimiters.py`'s existing + `echo`-line cross-line-paren tracking to also cover `rem` lines) is filed separately as CLAUDE.md + Active Backlog Item 61** -- not implemented as part of this fix, which only needed to unblock the + one broken instance, not audit or re-armor every pre-existing `rem` block already in the file. + + **A FOURTH real bug, in the SAME commit as the third: the rem-comment fix alone did not resolve + the regression -- CI still failed identically after it was pushed, confirmed via a second round + of live CI evidence.** The new `>> "%LOG%" echo ... (exit 3); falling back to requirements.txt or + pipreqs.` line (added alongside the rem comment, in the SAME original commit) has its own literal + `(`/`)` pair -- `(exit 3)` -- that opens AND closes on the SAME line, inside echo text. Per the + established rule and `check_delimiters.py`'s own code, a same-line self-contained pair is + supposed to be harmless (the checker deliberately never flags one) -- but a downloaded diagnostics + artifact's real `~envsmoke_bootstrap.log` showed the IDENTICAL corruption signature as the + original PR #408 incident: `falling was unexpected at this time.`, with "falling" being the exact + next word after `(exit 3)` in this line. This same-line pair sits nested FOUR levels deep inside + real `if (...)` blocks -- one level deeper than any previously-confirmed safe case -- and is a + `>> file echo ...` REDIRECTED form, not a plain top-level `echo` statement; which factor (depth, + or the redirection prefix) actually matters was not isolated, since the live evidence made the + fix (remove the parens) unambiguous without needing to. Fixed by rewording `(exit 3)` to `, exit + 3` -- no literal parens at all. **Revises the established rule**: "same-line, self-contained, + balanced" is not a blanket safe-harbor for text nested inside a real open block -- it is only + confirmed-safe for a genuinely top-level statement with no enclosing bracket (the + `:print_fastpath_ambiguous_note` precedent). See `docs/agent-lessons-learned.md`'s corresponding + entry for the full trace. Both this bug and the rem-comment bug shipped in the same original + commit and had to be found and fixed in two SEPARATE rounds, each confirmed only by live Windows + CI evidence pulled from a downloaded diagnostics artifact -- local tooling caught neither one. + ## Known Findings (diagnosed, no action warranted) - **Backlog item numbering: renumber-on-collision convention dropped, 2026-07-31 owner decision.** diff --git a/docs/agent-lessons-learned.md b/docs/agent-lessons-learned.md index 5d0f7d88..8c331966 100644 --- a/docs/agent-lessons-learned.md +++ b/docs/agent-lessons-learned.md @@ -455,6 +455,62 @@ list; the recurring traps that have actually bitten us: `)` closes on a different line -- scoped to "already nested" so a harmless top-level echo (no enclosing block, confirmed against `:print_fastpath_ambiguous_note`) doesn't false-positive. Tests: `tests/test_check_delimiters_import.py`'s three `test_paren_*` cases. + + **The identical hazard applies to `rem` comment text too, and `check_delimiters.py` does NOT + catch it -- confirmed as a second real shipped regression, PR #445 (Item 52), all 8 CI lanes + broken simultaneously.** A `rem` comment explaining "check the highest threshold first" split a + parenthetical remark's `(`/`)` across three separate `rem` lines, nested three levels deep + inside real `if (...)` blocks (`if exist ( if not errorlevel 1 ( if errorlevel 1 ( ... rem + lines here ... )`). Symptom matched the echo-hazard bug exactly: every CI lane that runs a real + bootstrap (`real`, `uv`, `contract-uv`, `contract-uv-fail`, `uv-dl-fallback`, `justme-test`, + `cache` -- effectively everything except the empty-repo/`HP_CI_SKIP_ENV` fast paths) failed with + zero console output a few seconds into the run, since parsing corrupts REGARDLESS of whether the + block's own runtime condition (`if exist "pyproject.toml"`) ever evaluates true -- cmd.exe must + determine where a parenthesized block's raw text ENDS before it can decide whether to execute or + skip it. **Root cause of the checker's blind spot**: `check_delimiters.py`'s own `.bat`/`.cmd` + handling (`check()`, the `if upper.startswith("REM ") or ... : continue` line) treats a `rem` + line as fully opaque and skips it from paren-scanning ENTIRELY -- unlike the `echo`-line handling + above, which DOES scan the line's characters and specifically tracks whether an opened `(` closes + on a different line. This means `check_delimiters.py` currently under-protects `rem` comments + relative to `echo` text: it will report a clean file even when a `rem` block contains exactly + this hazard. **Fix applied to the specific instance**: reworded the comment to avoid the literal + `(`/`)` characters entirely (` -- ` in place of the parenthetical), per the same rule as the echo + case. **The general gap in `check_delimiters.py` itself is NOT yet closed** -- extending its + existing `is_echo_open`-style tracking to also cover `rem` lines (dropping the current + `continue`-and-skip shortcut, replacing it with the same character-scan-plus-cross-line-check the + echo path already has) would close this class of bug the same way the echo fix did in 2026-07 -- + flagged as a candidate follow-up item, not implemented as part of this fix (this fix only needed + to unblock the one broken instance, not audit or re-armor every pre-existing `rem` block in the + file -- a large, separate undertaking on a ~5300-line file with many other cross-line `rem` + parens whose actual nesting-safety was not individually re-verified here). + + **The rem-comment fix above did NOT fully resolve the regression -- a SECOND, independent paren + hazard in the SAME code block was found only via a second round of live CI evidence, after the + first fix was pushed and CI still failed identically.** The new `>> "%LOG%" echo ... (exit 3); + falling back to requirements.txt or pipreqs.` line (added in the same change as the rem comment) + has a literal `(`/`)` pair -- `(exit 3)` -- that opens AND closes on the SAME line, inside echo + text. Per this entry's own established rule and `check_delimiters.py`'s own code (`last.char == + "(" and last.is_echo_open and line != last.line`), a same-line, self-contained pair is supposed + to be harmless -- the checker deliberately does NOT flag it, and the precedent case + (`:print_fastpath_ambiguous_note`) confirmed a same-line pair is fine when the echo statement is + TOP-LEVEL (no enclosing block). **This assumption does not hold here**: confirmed via a + downloaded diagnostics artifact's real bootstrap log (`~envsmoke_bootstrap.log`) showing the + EXACT corruption signature from the original PR #408 incident -- `falling was unexpected at this + time.` -- with "falling" being the very next word after `(exit 3)` in this line's own text. This + same-line pair sits nested FOUR levels deep inside real `if (...)` blocks, one level deeper than + any previously-confirmed case, and is a `>> file echo ...` REDIRECTED form, not a plain top-level + `echo` statement -- either factor (nesting depth, or the redirection prefix interacting with + cmd.exe's paren-parsing differently than a bare `echo`) could be the actual distinguishing + condition; which one was NOT isolated, since the live CI evidence made further speculation + unnecessary once the fix (remove the parens) was confirmed to work. **Revised rule, stronger than + the original**: do not treat "same-line, self-contained, balanced" as a blanket safe-harbor for + ANY echo/rem text nested inside a real open block -- it is only KNOWN-safe for a genuinely + top-level statement with no enclosing bracket. When in doubt inside a nested block, remove + literal parens from wrapped text entirely (` -- ` or `,`) rather than relying on same-line + balance alone. Fixed by rewording `(exit 3)` to `, exit 3` (no parens at all). Both bugs (the + rem-comment cross-line pair, and this echo same-line pair) shipped in the SAME original commit + and had to be found and fixed in two separate rounds, each confirmed only by live Windows CI + evidence -- local tooling (`check_delimiters.py`, the full sanity sweep) caught NEITHER one. - **Avoid `EnableDelayedExpansion`; if unavoidable, wrap it tightly.** `!` becomes special under delayed expansion, and a parent shell launched with `/V:ON` causes `!`-collisions. `tests/harness.ps1` `batch.bang.scan` enforces "no `!` in live batch code lines." diff --git a/run_setup.bat b/run_setup.bat index ea61494f..e58bace0 100644 --- a/run_setup.bat +++ b/run_setup.bat @@ -1322,7 +1322,13 @@ if exist "pyproject.toml" ( if not errorlevel 1 ( "%HP_PY%" "~pyproj_deps.py" "%HP_PYPROJ_REQ%" >nul 2>&1 if errorlevel 1 ( - if errorlevel 2 ( + rem derived requirement: check the highest threshold first -- "if errorlevel N" is a + rem GEQ N test, so errorlevel 3 -- pyproj_deps.py's own unexpected-internal-error exit, + rem distinct from its deliberate exit 1 "nothing to do here" -- would also satisfy + rem "if errorlevel 2" and get mislabeled as a TOML parse error if checked second. + if errorlevel 3 ( + >> "%LOG%" echo pyproj_deps.py: unexpected internal error, exit 3; falling back to requirements.txt or pipreqs. + ) else if errorlevel 2 ( echo *** [WARN] pyproject.toml could not be parsed as valid TOML; falling back to requirements.txt or pipreqs. call :log "[WARN] pyproject.toml TOML parse error; falling back." ) @@ -4930,7 +4936,7 @@ exit /b %errorlevel% :define_helper_payloads rem Helper payloads are base64-encoded so run_setup.bat stays self-contained. rem Regenerate with python - <<'PY' snippets as noted in README.md (base64 docs: https://docs.python.org/3/library/base64.html). -set "HP_PYPROJ_DEPS=IiIicHlwcm9qX2RlcHMgKEhQX1BZUFJPSl9ERVBTKSAtLSBleHRyYWN0cyBbcHJvamVjdF0uZGVwZW5kZW5jaWVzIGZyb20KcHlwcm9qZWN0LnRvbWwsIHdyaXRpbmcgb25lIGRlcGVuZGVuY3kgcGVyIGxpbmUgdG8gYW4gb3V0cHV0IGZpbGUuCgpSdW4gZnJvbSB0aGUgYXBwbGljYXRpb24gZGlyZWN0b3J5IChyZWFkcyAicHlwcm9qZWN0LnRvbWwiIGZyb20gQ1dEKS4KVXNhZ2U6IHB5dGhvbiBweXByb2pfZGVwcy5weSBbb3V0cHV0X3BhdGhdICAoZGVmYXVsdCAifnJlcXVpcmVtZW50cy5weXByb2plY3QudHh0IikKCkV4aXQgY29kZXM6CiAgMCAtIHN1Y2Nlc3MsIGRlcGVuZGVuY2llcyB3cml0dGVuIHRvIHRoZSBvdXRwdXQgZmlsZQogIDEgLSBub3QtZm91bmQvZXJyb3IgKG5vIHB5cHJvamVjdC50b21sLCBubyBbcHJvamVjdF0uZGVwZW5kZW5jaWVzIGtleSwKICAgICAgb3IgYW4gZW1wdHkgZGVwZW5kZW5jaWVzIGxpc3QpCiAgMiAtIG1hbGZvcm1lZCBUT01MICh0b21sbGliIHJhaXNlZCwgb3IgLS0gd2hlbiB0b21sbGliIGlzIHVuYXZhaWxhYmxlIC0tCiAgICAgIHRoZSByZWdleCBmYWxsYmFjayBmb3VuZCBhbiB1bmNsb3NlZCAiW3Byb2plY3QiIGhlYWRlcikKClByZWZlcnMgc3RkbGliIHRvbWxsaWIgKDMuMTErKSB3aGVuIGF2YWlsYWJsZS4gRmFsbHMgYmFjayB0byBhIHJlZ2V4LWJhc2VkCmV4dHJhY3RvciB3aGVuIHRvbWxsaWIgaXMgbWlzc2luZyBPUiB0aGUgW3Byb2plY3RdIHRhYmxlIGhhcyBubwoiZGVwZW5kZW5jaWVzIiBrZXkgdmlhIHRvbWxsaWIgKHRoZSB0d28gY2FzZXMgYXJlIGluZGlzdGluZ3Vpc2hhYmxlIGZyb20KdG9tbGxpYidzIG93biByZXR1cm4gdmFsdWUsIHNvIHRoZSBmYWxsYmFjayBhbHdheXMgcmUtc2NhbnMgaW4gdGhhdCBjYXNlIC0tCmhhcm1sZXNzLCBzaW5jZSBhIGdlbnVpbmVseS1hYnNlbnQga2V5IGFsc28gZmluZHMgbm90aGluZyB2aWEgcmVnZXgpLiBUaGUKZmFsbGJhY2sncyBkZXBlbmRlbmN5LWFycmF5IHdhbGsgaXMgY2hhci1ieS1jaGFyIG92ZXIgcXVvdGVkIHN0cmluZ3MgKG5vdCBhCm5haXZlIGNvbW1hL25ld2xpbmUgc3BsaXQpIHNvIGl0IHByZXNlcnZlcyBleHRyYXMgKCJwa2dbYWxsXSIpIGFuZAptdWx0aS1jb25zdHJhaW50IHNwZWNpZmllcnMgKCJwa2c+PTQsPDUiKSBpbnRhY3QsIGFuZCBza2lwcyAiIyItdG8tZW5kLW9mLQpsaW5lIGNvbW1lbnRzIChvdXRzaWRlIG9mIHF1b3Rlcykgc28gYSBjb21tZW50IG1lbnRpb25pbmcgYnJhY2tldCBzeW50YXgKY2Fubm90IGJlIG1pc3Rha2VuIGZvciB0aGUgYXJyYXkncyBvd24gY2xvc2luZyAiXSIuCgpUaGlzIGlzIHRoZSBjYW5vbmljYWwgc291cmNlIGZvciB0aGUgSFBfUFlQUk9KX0RFUFMgYmFzZTY0IHBheWxvYWQgZW1iZWRkZWQKaW4gcnVuX3NldHVwLmJhdC4gQWZ0ZXIgZWRpdGluZywgcmUtZW5jb2RlIGFuZCBwYXN0ZSBpdCBpbnRvIHRoZQpgc2V0ICJIUF9QWVBST0pfREVQUz0uLi4iYCBsaW5lOyB0ZXN0cy90ZXN0X3B5cHJval9kZXBzLnB5IGFzc2VydHMgdGhlCmVtYmVkZGVkIHBheWxvYWQgbWF0Y2hlcyB0aGlzIGZpbGUuCiIiIgppbXBvcnQgc3lzLCBwYXRobGliCgp0cnk6CiAgICBpbXBvcnQgdG9tbGxpYgpleGNlcHQgSW1wb3J0RXJyb3I6CiAgICB0b21sbGliID0gTm9uZQoKb3V0ID0gc3lzLmFyZ3ZbMV0gaWYgbGVuKHN5cy5hcmd2KSA+IDEgZWxzZSAnfnJlcXVpcmVtZW50cy5weXByb2plY3QudHh0Jwp0cnk6CiAgICB0eHQgPSBwYXRobGliLlBhdGgoJ3B5cHJvamVjdC50b21sJykucmVhZF90ZXh0KGVuY29kaW5nPSd1dGYtOCcsIGVycm9ycz0ncmVwbGFjZScpCiAgICBkZXBzID0gTm9uZQogICAgaWYgdG9tbGxpYjoKICAgICAgICB0cnk6CiAgICAgICAgICAgIGRhdGEgPSB0b21sbGliLmxvYWRzKHR4dCkKICAgICAgICAgICAgZGVwcyA9IGRhdGEuZ2V0KCdwcm9qZWN0Jywge30pLmdldCgnZGVwZW5kZW5jaWVzJykKICAgICAgICBleGNlcHQgRXhjZXB0aW9uOgogICAgICAgICAgICAjIEV4aXQgMiBzaWduYWxzIGNhbGxlciB0byBlbWl0IFtXQVJOXTogcHlwcm9qZWN0LnRvbWwgaXMgbm90IHZhbGlkIFRPTUwuCiAgICAgICAgICAgIHN5cy5leGl0KDIpCiAgICBpZiBkZXBzIGlzIE5vbmU6CiAgICAgICAgaW1wb3J0IHJlCiAgICAgICAgbSA9IHJlLnNlYXJjaChyJ15cW3Byb2plY3RcXScsIHR4dCwgcmUuTVVMVElMSU5FKQogICAgICAgIGlmIG5vdCBtOgogICAgICAgICAgICAjIGRlcml2ZWQgcmVxdWlyZW1lbnQ6IHdpdGhvdXQgdG9tbGxpYiwgZGV0ZWN0IG9idmlvdXNseSBtYWxmb3JtZWQgW3Byb2plY3QgaGVhZGVyCiAgICAgICAgICAgICMgKG1pc3NpbmcgY2xvc2luZyBicmFja2V0IC0tIGUuZy4gIltwcm9qZWN0XG4iKS4gRXhpdCAyIHNvIGNhbGxlciBlbWl0cyBUT01MIHBhcnNlIHdhcm5pbmcuCiAgICAgICAgICAgIGlmIHJlLnNlYXJjaChyJ15cW3Byb2plY3RccyokJywgdHh0LCByZS5NVUxUSUxJTkUpOgogICAgICAgICAgICAgICAgc3lzLmV4aXQoMikKICAgICAgICAgICAgc3lzLmV4aXQoMSkKICAgICAgICBzZWMgPSB0eHRbbS5lbmQoKTpdCiAgICAgICAgc3RvcCA9IHJlLnNlYXJjaChyJ15cWycsIHNlYywgcmUuTVVMVElMSU5FKQogICAgICAgIGlmIHN0b3A6CiAgICAgICAgICAgIHNlYyA9IHNlY1s6c3RvcC5zdGFydCgpXQogICAgICAgIGRtID0gcmUuc2VhcmNoKHInXlxzKmRlcGVuZGVuY2llc1xzKj1ccypcWycsIHNlYywgcmUuTVVMVElMSU5FKQogICAgICAgIGlmIG5vdCBkbToKICAgICAgICAgICAgc3lzLmV4aXQoMSkKICAgICAgICByZXN0ID0gc2VjW2RtLmVuZCgpOl0KICAgICAgICAjIFdhbGsgY2hhci1ieS1jaGFyOiBjb2xsZWN0IG9ubHkgcXVvdGVkIHN0cmluZ3M7IHN0b3AgYXQgdW5xdW90ZWQgXQogICAgICAgICMgVGhpcyBwcmVzZXJ2ZXMgZnVsbCBkZXAgc3RyaW5ncyBpbmNsdWRpbmcgZXh0cmFzIChbYWxsXSkgYW5kCiAgICAgICAgIyBtdWx0aS1jb25zdHJhaW50IHNwZWNpZmllcnMgKD49NCw8NSkgd2l0aG91dCBuYWl2ZSBjb21tYS9uZXdsaW5lIHNwbGl0cy4KICAgICAgICBkZXBzID0gW10KICAgICAgICBpID0gMAogICAgICAgIHdoaWxlIGkgPCBsZW4ocmVzdCk6CiAgICAgICAgICAgIGMgPSByZXN0W2ldCiAgICAgICAgICAgIGlmIGMgaW4gKCciJywgIiciKToKICAgICAgICAgICAgICAgIHEgPSBjCiAgICAgICAgICAgICAgICBpICs9IDEKICAgICAgICAgICAgICAgIHN0YXJ0ID0gaQogICAgICAgICAgICAgICAgd2hpbGUgaSA8IGxlbihyZXN0KSBhbmQgcmVzdFtpXSAhPSBxOgogICAgICAgICAgICAgICAgICAgIGlmIHJlc3RbaV0gPT0gJ1xcJzoKICAgICAgICAgICAgICAgICAgICAgICAgaSArPSAxCiAgICAgICAgICAgICAgICAgICAgaSArPSAxCiAgICAgICAgICAgICAgICBkZXBzLmFwcGVuZChyZXN0W3N0YXJ0OmldKQogICAgICAgICAgICAgICAgaSArPSAxCiAgICAgICAgICAgIGVsaWYgYyA9PSAnIyc6CiAgICAgICAgICAgICAgICAjIFRPTUwgY29tbWVudHMgcnVuIGZyb20gIyB0byBlbmQgb2YgbGluZSBhbmQgY2Fubm90IGFwcGVhcgogICAgICAgICAgICAgICAgIyBpbnNpZGUgYSBzdHJpbmcgKHRoZSBicmFuY2ggYWJvdmUgYWxyZWFkeSBjb25zdW1lZCBhbnkgIwogICAgICAgICAgICAgICAgIyB0aGF0IHdhcyBxdW90ZWQpLiBXaXRob3V0IHRoaXMsIGEgY29tbWVudCByZWZlcmVuY2luZyBhcnJheQogICAgICAgICAgICAgICAgIyBzeW50YXggLS0gZS5nLiAnInJlcXVlc3RzIiwgICMgc3VwcG9ydHMgWzEsMl0gc3ludGF4JyAtLQogICAgICAgICAgICAgICAgIyB3b3VsZCBoYXZlIGl0cyB1bnF1b3RlZCAiXSIgd3JvbmdseSB0cmVhdGVkIGFzIHRoZSBlbmQgb2YKICAgICAgICAgICAgICAgICMgdGhlIGRlcGVuZGVuY2llcyBhcnJheSwgc2lsZW50bHkgZHJvcHBpbmcgZXZlcnkgZGVwZW5kZW5jeQogICAgICAgICAgICAgICAgIyBsaXN0ZWQgYWZ0ZXIgdGhhdCBsaW5lLgogICAgICAgICAgICAgICAgbmwgPSByZXN0LmZpbmQoJ1xuJywgaSkKICAgICAgICAgICAgICAgIGlmIG5sID09IC0xOgogICAgICAgICAgICAgICAgICAgIGJyZWFrCiAgICAgICAgICAgICAgICBpID0gbmwgKyAxCiAgICAgICAgICAgIGVsaWYgYyA9PSAnXSc6CiAgICAgICAgICAgICAgICBicmVhawogICAgICAgICAgICBlbHNlOgogICAgICAgICAgICAgICAgaSArPSAxCiAgICBpZiBub3QgZGVwczoKICAgICAgICBzeXMuZXhpdCgxKQogICAgcGF0aGxpYi5QYXRoKG91dCkud3JpdGVfdGV4dCgnXG4nLmpvaW4oZGVwcykgKyAnXG4nLCBlbmNvZGluZz0nYXNjaWknLCBlcnJvcnM9J3JlcGxhY2UnKQogICAgc3lzLmV4aXQoMCkKZXhjZXB0IEV4Y2VwdGlvbjoKICAgIHN5cy5leGl0KDEpCg==" +set "HP_PYPROJ_DEPS=IiIicHlwcm9qX2RlcHMgKEhQX1BZUFJPSl9ERVBTKSAtLSBleHRyYWN0cyBbcHJvamVjdF0uZGVwZW5kZW5jaWVzIGZyb20KcHlwcm9qZWN0LnRvbWwsIHdyaXRpbmcgb25lIGRlcGVuZGVuY3kgcGVyIGxpbmUgdG8gYW4gb3V0cHV0IGZpbGUuCgpSdW4gZnJvbSB0aGUgYXBwbGljYXRpb24gZGlyZWN0b3J5IChyZWFkcyAicHlwcm9qZWN0LnRvbWwiIGZyb20gQ1dEKS4KVXNhZ2U6IHB5dGhvbiBweXByb2pfZGVwcy5weSBbb3V0cHV0X3BhdGhdICAoZGVmYXVsdCAifnJlcXVpcmVtZW50cy5weXByb2plY3QudHh0IikKCkV4aXQgY29kZXM6CiAgMCAtIHN1Y2Nlc3MsIGRlcGVuZGVuY2llcyB3cml0dGVuIHRvIHRoZSBvdXRwdXQgZmlsZQogIDEgLSBub3QtZm91bmQvZXJyb3IgKG5vIHB5cHJvamVjdC50b21sLCBubyBbcHJvamVjdF0uZGVwZW5kZW5jaWVzIGtleSwKICAgICAgb3IgYW4gZW1wdHkgZGVwZW5kZW5jaWVzIGxpc3QpCiAgMiAtIG1hbGZvcm1lZCBUT01MICh0b21sbGliIHJhaXNlZCwgb3IgLS0gd2hlbiB0b21sbGliIGlzIHVuYXZhaWxhYmxlIC0tCiAgICAgIHRoZSByZWdleCBmYWxsYmFjayBmb3VuZCBhbiB1bmNsb3NlZCAiW3Byb2plY3QiIGhlYWRlcikKICAzIC0gdW5leHBlY3RlZCBpbnRlcm5hbCBlcnJvciAoZS5nLiBweXByb2plY3QudG9tbCBleGlzdHMgYnV0IGNhbm5vdCBiZQogICAgICByZWFkIGFzIGEgZmlsZSAtLSBhIGRpcmVjdG9yeSwgYSBwZXJtaXNzaW9uIGZhaWx1cmUsIGV0Yy4pLiBEaXN0aW5jdAogICAgICBmcm9tIDEgc28gYSBnZW51aW5lIGJ1ZyBvciB1bnVzdWFsIEkvTyBmYWlsdXJlIG5ldmVyIG1hc3F1ZXJhZGVzIGFzCiAgICAgIHRoZSBiZW5pZ24gIm5vdGhpbmcgdG8gZG8gaGVyZSIgY2FzZSAtLSBzZWUgdGhlIG91dGVyIGV4Y2VwdCBiZWxvdy4KClByZWZlcnMgc3RkbGliIHRvbWxsaWIgKDMuMTErKSB3aGVuIGF2YWlsYWJsZS4gRmFsbHMgYmFjayB0byBhIHJlZ2V4LWJhc2VkCmV4dHJhY3RvciB3aGVuIHRvbWxsaWIgaXMgbWlzc2luZyBPUiB0aGUgW3Byb2plY3RdIHRhYmxlIGhhcyBubwoiZGVwZW5kZW5jaWVzIiBrZXkgdmlhIHRvbWxsaWIgKHRoZSB0d28gY2FzZXMgYXJlIGluZGlzdGluZ3Vpc2hhYmxlIGZyb20KdG9tbGxpYidzIG93biByZXR1cm4gdmFsdWUsIHNvIHRoZSBmYWxsYmFjayBhbHdheXMgcmUtc2NhbnMgaW4gdGhhdCBjYXNlIC0tCmhhcm1sZXNzLCBzaW5jZSBhIGdlbnVpbmVseS1hYnNlbnQga2V5IGFsc28gZmluZHMgbm90aGluZyB2aWEgcmVnZXgpLiBUaGUKZmFsbGJhY2sncyBkZXBlbmRlbmN5LWFycmF5IHdhbGsgaXMgY2hhci1ieS1jaGFyIG92ZXIgcXVvdGVkIHN0cmluZ3MgKG5vdCBhCm5haXZlIGNvbW1hL25ld2xpbmUgc3BsaXQpIHNvIGl0IHByZXNlcnZlcyBleHRyYXMgKCJwa2dbYWxsXSIpIGFuZAptdWx0aS1jb25zdHJhaW50IHNwZWNpZmllcnMgKCJwa2c+PTQsPDUiKSBpbnRhY3QsIGFuZCBza2lwcyAiIyItdG8tZW5kLW9mLQpsaW5lIGNvbW1lbnRzIChvdXRzaWRlIG9mIHF1b3Rlcykgc28gYSBjb21tZW50IG1lbnRpb25pbmcgYnJhY2tldCBzeW50YXgKY2Fubm90IGJlIG1pc3Rha2VuIGZvciB0aGUgYXJyYXkncyBvd24gY2xvc2luZyAiXSIuCgpUaGlzIGlzIHRoZSBjYW5vbmljYWwgc291cmNlIGZvciB0aGUgSFBfUFlQUk9KX0RFUFMgYmFzZTY0IHBheWxvYWQgZW1iZWRkZWQKaW4gcnVuX3NldHVwLmJhdC4gQWZ0ZXIgZWRpdGluZywgcmUtZW5jb2RlIGFuZCBwYXN0ZSBpdCBpbnRvIHRoZQpgc2V0ICJIUF9QWVBST0pfREVQUz0uLi4iYCBsaW5lOyB0ZXN0cy90ZXN0X3B5cHJval9kZXBzLnB5IGFzc2VydHMgdGhlCmVtYmVkZGVkIHBheWxvYWQgbWF0Y2hlcyB0aGlzIGZpbGUuCiIiIgppbXBvcnQgc3lzLCBwYXRobGliCgp0cnk6CiAgICBpbXBvcnQgdG9tbGxpYgpleGNlcHQgSW1wb3J0RXJyb3I6CiAgICB0b21sbGliID0gTm9uZQoKb3V0ID0gc3lzLmFyZ3ZbMV0gaWYgbGVuKHN5cy5hcmd2KSA+IDEgZWxzZSAnfnJlcXVpcmVtZW50cy5weXByb2plY3QudHh0Jwp0cnk6CiAgICBzcmMgPSBwYXRobGliLlBhdGgoJ3B5cHJvamVjdC50b21sJykKICAgIHRyeToKICAgICAgICAjIGRlcml2ZWQgcmVxdWlyZW1lbnQ6IGEgbWlzc2luZyBweXByb2plY3QudG9tbCBpcyB0aGUgZGVsaWJlcmF0ZSwKICAgICAgICAjIGRvY3VtZW50ZWQgZXhpdC0xICJub3QgZm91bmQiIGNhc2UgKGluIHByYWN0aWNlIHJ1bl9zZXR1cC5iYXQgb25seQogICAgICAgICMgZXZlciBpbnZva2VzIHRoaXMgc2NyaXB0IHdoZW4gdGhlIGZpbGUgZXhpc3RzLCBidXQgdGhpcyBzY3JpcHQncwogICAgICAgICMgb3duIGNvbnRyYWN0IHByZWRhdGVzIGFuZCBkb2VzIG5vdCBhc3N1bWUgdGhhdCBjYWxsZXIpLiBDYXVnaHQKICAgICAgICAjIHNwZWNpZmljYWxseSBoZXJlLCBhcm91bmQgdGhlIHJlYWQgaXRzZWxmLCByYXRoZXIgdGhhbiB2aWEgYQogICAgICAgICMgcHJlY2VkaW5nIFBhdGguZXhpc3RzKCkgY2hlY2sgLS0gUHl0aG9uIDMuMTQrIG1hZGUgZXhpc3RzKCkgc3dhbGxvdwogICAgICAgICMgQU5ZIE9TRXJyb3IgKGluY2x1ZGluZyBQZXJtaXNzaW9uRXJyb3IpIGFuZCByZXR1cm4gRmFsc2UgaW5zdGVhZCBvZgogICAgICAgICMgcmFpc2luZywgd2hpY2ggd291bGQgc2lsZW50bHkgbWlzY2xhc3NpZnkgYSBnZW51aW5lIHBlcm1pc3Npb24KICAgICAgICAjIGZhaWx1cmUgYXMgIm5vdCBmb3VuZCIgKDEpIGluc3RlYWQgb2YgdGhlIHJlYWwtZXJyb3IgY2FzZSAoMykKICAgICAgICAjIGJlbG93LiBBbnkgT1RIRVIgZXhjZXB0aW9uIGhlcmUgKGEgZGlyZWN0b3J5LCBwZXJtaXNzaW9uIGRlbmllZCwKICAgICAgICAjIGV0Yy4pIGlzIGEgZ2VudWluZSBidWcvZmFpbHVyZSBhbmQgZmFsbHMgdGhyb3VnaCB0byB0aGUgb3V0ZXIKICAgICAgICAjIGV4Y2VwdCwgdW5jYXVnaHQgYnkgdGhpcyBuYXJyb3dlciBvbmUuCiAgICAgICAgdHh0ID0gc3JjLnJlYWRfdGV4dChlbmNvZGluZz0ndXRmLTgnLCBlcnJvcnM9J3JlcGxhY2UnKQogICAgZXhjZXB0IEZpbGVOb3RGb3VuZEVycm9yOgogICAgICAgIHN5cy5leGl0KDEpCiAgICBkZXBzID0gTm9uZQogICAgaWYgdG9tbGxpYjoKICAgICAgICB0cnk6CiAgICAgICAgICAgIGRhdGEgPSB0b21sbGliLmxvYWRzKHR4dCkKICAgICAgICAgICAgZGVwcyA9IGRhdGEuZ2V0KCdwcm9qZWN0Jywge30pLmdldCgnZGVwZW5kZW5jaWVzJykKICAgICAgICBleGNlcHQgRXhjZXB0aW9uOgogICAgICAgICAgICAjIEV4aXQgMiBzaWduYWxzIGNhbGxlciB0byBlbWl0IFtXQVJOXTogcHlwcm9qZWN0LnRvbWwgaXMgbm90IHZhbGlkIFRPTUwuCiAgICAgICAgICAgIHN5cy5leGl0KDIpCiAgICBpZiBkZXBzIGlzIE5vbmU6CiAgICAgICAgaW1wb3J0IHJlCiAgICAgICAgbSA9IHJlLnNlYXJjaChyJ15cW3Byb2plY3RcXScsIHR4dCwgcmUuTVVMVElMSU5FKQogICAgICAgIGlmIG5vdCBtOgogICAgICAgICAgICAjIGRlcml2ZWQgcmVxdWlyZW1lbnQ6IHdpdGhvdXQgdG9tbGxpYiwgZGV0ZWN0IG9idmlvdXNseSBtYWxmb3JtZWQgW3Byb2plY3QgaGVhZGVyCiAgICAgICAgICAgICMgKG1pc3NpbmcgY2xvc2luZyBicmFja2V0IC0tIGUuZy4gIltwcm9qZWN0XG4iKS4gRXhpdCAyIHNvIGNhbGxlciBlbWl0cyBUT01MIHBhcnNlIHdhcm5pbmcuCiAgICAgICAgICAgIGlmIHJlLnNlYXJjaChyJ15cW3Byb2plY3RccyokJywgdHh0LCByZS5NVUxUSUxJTkUpOgogICAgICAgICAgICAgICAgc3lzLmV4aXQoMikKICAgICAgICAgICAgc3lzLmV4aXQoMSkKICAgICAgICBzZWMgPSB0eHRbbS5lbmQoKTpdCiAgICAgICAgc3RvcCA9IHJlLnNlYXJjaChyJ15cWycsIHNlYywgcmUuTVVMVElMSU5FKQogICAgICAgIGlmIHN0b3A6CiAgICAgICAgICAgIHNlYyA9IHNlY1s6c3RvcC5zdGFydCgpXQogICAgICAgIGRtID0gcmUuc2VhcmNoKHInXlxzKmRlcGVuZGVuY2llc1xzKj1ccypcWycsIHNlYywgcmUuTVVMVElMSU5FKQogICAgICAgIGlmIG5vdCBkbToKICAgICAgICAgICAgc3lzLmV4aXQoMSkKICAgICAgICByZXN0ID0gc2VjW2RtLmVuZCgpOl0KICAgICAgICAjIFdhbGsgY2hhci1ieS1jaGFyOiBjb2xsZWN0IG9ubHkgcXVvdGVkIHN0cmluZ3M7IHN0b3AgYXQgdW5xdW90ZWQgXQogICAgICAgICMgVGhpcyBwcmVzZXJ2ZXMgZnVsbCBkZXAgc3RyaW5ncyBpbmNsdWRpbmcgZXh0cmFzIChbYWxsXSkgYW5kCiAgICAgICAgIyBtdWx0aS1jb25zdHJhaW50IHNwZWNpZmllcnMgKD49NCw8NSkgd2l0aG91dCBuYWl2ZSBjb21tYS9uZXdsaW5lIHNwbGl0cy4KICAgICAgICBkZXBzID0gW10KICAgICAgICBpID0gMAogICAgICAgIHdoaWxlIGkgPCBsZW4ocmVzdCk6CiAgICAgICAgICAgIGMgPSByZXN0W2ldCiAgICAgICAgICAgIGlmIGMgaW4gKCciJywgIiciKToKICAgICAgICAgICAgICAgIHEgPSBjCiAgICAgICAgICAgICAgICBpICs9IDEKICAgICAgICAgICAgICAgIHN0YXJ0ID0gaQogICAgICAgICAgICAgICAgd2hpbGUgaSA8IGxlbihyZXN0KSBhbmQgcmVzdFtpXSAhPSBxOgogICAgICAgICAgICAgICAgICAgIGlmIHJlc3RbaV0gPT0gJ1xcJzoKICAgICAgICAgICAgICAgICAgICAgICAgaSArPSAxCiAgICAgICAgICAgICAgICAgICAgaSArPSAxCiAgICAgICAgICAgICAgICBkZXBzLmFwcGVuZChyZXN0W3N0YXJ0OmldKQogICAgICAgICAgICAgICAgaSArPSAxCiAgICAgICAgICAgIGVsaWYgYyA9PSAnIyc6CiAgICAgICAgICAgICAgICAjIFRPTUwgY29tbWVudHMgcnVuIGZyb20gIyB0byBlbmQgb2YgbGluZSBhbmQgY2Fubm90IGFwcGVhcgogICAgICAgICAgICAgICAgIyBpbnNpZGUgYSBzdHJpbmcgKHRoZSBicmFuY2ggYWJvdmUgYWxyZWFkeSBjb25zdW1lZCBhbnkgIwogICAgICAgICAgICAgICAgIyB0aGF0IHdhcyBxdW90ZWQpLiBXaXRob3V0IHRoaXMsIGEgY29tbWVudCByZWZlcmVuY2luZyBhcnJheQogICAgICAgICAgICAgICAgIyBzeW50YXggLS0gZS5nLiAnInJlcXVlc3RzIiwgICMgc3VwcG9ydHMgWzEsMl0gc3ludGF4JyAtLQogICAgICAgICAgICAgICAgIyB3b3VsZCBoYXZlIGl0cyB1bnF1b3RlZCAiXSIgd3JvbmdseSB0cmVhdGVkIGFzIHRoZSBlbmQgb2YKICAgICAgICAgICAgICAgICMgdGhlIGRlcGVuZGVuY2llcyBhcnJheSwgc2lsZW50bHkgZHJvcHBpbmcgZXZlcnkgZGVwZW5kZW5jeQogICAgICAgICAgICAgICAgIyBsaXN0ZWQgYWZ0ZXIgdGhhdCBsaW5lLgogICAgICAgICAgICAgICAgbmwgPSByZXN0LmZpbmQoJ1xuJywgaSkKICAgICAgICAgICAgICAgIGlmIG5sID09IC0xOgogICAgICAgICAgICAgICAgICAgIGJyZWFrCiAgICAgICAgICAgICAgICBpID0gbmwgKyAxCiAgICAgICAgICAgIGVsaWYgYyA9PSAnXSc6CiAgICAgICAgICAgICAgICBicmVhawogICAgICAgICAgICBlbHNlOgogICAgICAgICAgICAgICAgaSArPSAxCiAgICBpZiBub3QgZGVwczoKICAgICAgICBzeXMuZXhpdCgxKQogICAgcGF0aGxpYi5QYXRoKG91dCkud3JpdGVfdGV4dCgnXG4nLmpvaW4oZGVwcykgKyAnXG4nLCBlbmNvZGluZz0nYXNjaWknLCBlcnJvcnM9J3JlcGxhY2UnKQogICAgc3lzLmV4aXQoMCkKZXhjZXB0IEV4Y2VwdGlvbjoKICAgICMgZGVyaXZlZCByZXF1aXJlbWVudDogYSBnZW51aW5lbHkgdW5leHBlY3RlZCBleGNlcHRpb24gKG5vdCBvbmUgb2YgdGhlCiAgICAjIGRlbGliZXJhdGUgc3lzLmV4aXQoMSkvc3lzLmV4aXQoMikgY2FsbHMgYWJvdmUgLS0gdGhvc2UgcmFpc2UKICAgICMgU3lzdGVtRXhpdCwgd2hpY2ggdGhpcyBFeGNlcHRpb24tb25seSBoYW5kbGVyIGRvZXMgbm90IGNhdGNoKSBtdXN0IG5vdAogICAgIyBleGl0IDEsIG9yIHJ1bl9zZXR1cC5iYXQncyBjYWxsZXIgY2Fubm90IHRlbGwgYSByZWFsIGJ1ZyBhcGFydCBmcm9tIHRoZQogICAgIyBpbnRlbnRpb25hbCAibm8gZGVwZW5kZW5jaWVzIGZvdW5kIiBjYXNlLiBTZWUgZXhpdCBjb2RlIDMgYWJvdmUuCiAgICBzeXMuZXhpdCgzKQo=" set "HP_CONDARC=Y2hhbm5lbHM6CiAgLSBjb25kYS1mb3JnZQpjaGFubmVsX3ByaW9yaXR5OiBzdHJpY3QKc2hvd19jaGFubmVsX3VybHM6IHRydWUK" set "HP_DETECT_PY=IiIiZGV0ZWN0X3B5dGhvbiAoSFBfREVURUNUX1BZKSAtLSBSRVEtMDA0IFRpZXIgMS8yIHJlcXVpcmVzLXB5dGhvbiBkZXRlY3Rvci4KClJ1biBmcm9tIHRoZSBhcHBsaWNhdGlvbiBkaXJlY3Rvcnk7IHByaW50cyBhIG5vcm1hbGl6ZWQgY29uZGEtc3ludGF4CiJweXRob248b3A+PHZlcj5bLC4uLl0iIGNvbnN0cmFpbnQgdG8gc3Rkb3V0LCBvciBhbiBlbXB0eSBsaW5lIGlmIG5laXRoZXIKdGllciBmb3VuZCBhbnl0aGluZyAodGhlIGNhbGxlciB0aGVuIGxldHMgdGhlIHNlbGVjdGVkIHByb3ZpZGVyIHBpY2sgdGhlCmxhdGVzdCBhdmFpbGFibGUgUHl0aG9uIC0tIFJFUS0wMDQgVGllciAzLCBoYW5kbGVkIG91dHNpZGUgdGhpcyBoZWxwZXIpLgpEZXRlY3Rpb24gb3JkZXI6CgogIDEuIHJ1bnRpbWUudHh0IC0tIGEgYmFyZSAicHl0aG9uLVguWVsuWl0iIG9yICJYLllbLlpdIiBsaW5lIHBpbnMgYW4gZXhhY3QKICAgICBtaW5vciB2ZXJzaW9uIChweXRob249WC5ZOyB0aGUgcGF0Y2ggY29tcG9uZW50LCBpZiBwcmVzZW50LCBpcyBub3QKICAgICBmb3J3YXJkZWQgLS0gcHJvdmlkZXJzIHBpbiBieSBtaW5vciBvbmx5KS4KICAyLiBweXByb2plY3QudG9tbCdzIFtwcm9qZWN0XSByZXF1aXJlcy1weXRob24gLS0gYSBQRVAgNDQwIHNwZWNpZmllcgogICAgIChlLmcuICI+PTMuMTAsPDQiLCAifj0zLjExIikgaXMgcGFyc2VkIGFuZCBleHBhbmRlZCBpbnRvIG9uZSBvciBtb3JlCiAgICAgY29uZGEtc3ludGF4IGNsYXVzZXMgdmlhIHBlcDQ0MF90b19jb25kYSgpLgoKcGVwNDQwX3RvX2NvbmRhKCkgYWxzbyBkb3VibGVzIGFzIGEgc3RhbmRhbG9uZSBDTEkgdXRpbGl0eSAoc2VlIG1haW4oKSkgZm9yCnRyYW5zbGF0aW5nIGFyYml0cmFyeSBQRVAgNDQwIHNwZWNpZmllcnMsIGluY2x1ZGluZyB0aGUgIn49IiBjb21wYXRpYmxlLXJlbGVhc2UKb3BlcmF0b3IsIHdoaWNoIGNvbmRhIGhhcyBubyBuYXRpdmUgZXF1aXZhbGVudCBmb3IgYW5kIG11c3QgYmUgZXhwYW5kZWQgaW50bwphbiBleHBsaWNpdCA+PS88IHJhbmdlLgoKVGhpcyBpcyB0aGUgY2Fub25pY2FsIHNvdXJjZSBmb3IgdGhlIEhQX0RFVEVDVF9QWSBiYXNlNjQgcGF5bG9hZCBlbWJlZGRlZCBpbgpydW5fc2V0dXAuYmF0LiBBZnRlciBlZGl0aW5nLCByZS1lbmNvZGUgYW5kIHBhc3RlIGl0IGludG8gdGhlCmBzZXQgIkhQX0RFVEVDVF9QWT0uLi4iYCBsaW5lOyB0ZXN0cy90ZXN0X2RldGVjdF9weXRob24ucHkgYXNzZXJ0cyB0aGUKZW1iZWRkZWQgcGF5bG9hZCBtYXRjaGVzIHRoaXMgZmlsZS4KIiIiCl9fdmVyc2lvbl9fID0gImRldGVjdF9weXRob24gdjIgKDIwMjUtMDktMjQpIgpfX2FsbF9fID0gWyJwZXA0NDBfdG9fY29uZGEiLCAiZGV0ZWN0X3JlcXVpcmVzX3B5dGhvbiIsICJtYWluIl0KT1JERVIgPSB7Ij09IjogMCwgIiE9IjogMSwgIj49IjogMiwgIj4iOiAzLCAiPD0iOiA0LCAiPCI6IDV9CgppbXBvcnQgb3MKaW1wb3J0IHJlCmltcG9ydCBzeXMKCiMgSGVscGVyIGltcGxlbWVudHMgdGhlIFJFQURNRSBib290c3RyYXAgY29udHJhY3QuIFBFUCA0NDAgZGV0YWlsczoKIyBodHRwczovL3BlcHMucHl0aG9uLm9yZy9wZXAtMDQ0MC8KCkNEID0gb3MuZ2V0Y3dkKCkKUlVOVElNRV9QQVRIID0gb3MucGF0aC5qb2luKENELCAicnVudGltZS50eHQiKQpQWVBST0pFQ1RfUEFUSCA9IG9zLnBhdGguam9pbihDRCwgInB5cHJvamVjdC50b21sIikKUFlQUk9KRUNUX1JFID0gcmUuY29tcGlsZSgicmVxdWlyZXMtcHl0aG9uXFxzKj1cXHMqWydcIl0oW14nXCJdKylbJ1wiXSIsIHJlLklHTk9SRUNBU0UpClNQRUNfUEFUVEVSTiA9IHJlLmNvbXBpbGUocicofj18PT18IT18Pj18PD18Pnw8KVxzKihbMC05XSsoPzpcLlswLTldKykqKScpCgoKZGVmIHZlcnNpb25fa2V5KHRleHQ6IHN0cik6CiAgICAiIiJSZXR1cm4gYSB0dXBsZSB1c2FibGUgZm9yIG51bWVyaWMgb3JkZXJpbmcgb2YgZG90dGVkIHZlcnNpb25zLiIiIgogICAgcGFydHMgPSBbXQogICAgZm9yIGNodW5rIGluIHRleHQuc3BsaXQoJy4nKToKICAgICAgICB0cnk6CiAgICAgICAgICAgIHBhcnRzLmFwcGVuZChpbnQoY2h1bmspKQogICAgICAgIGV4Y2VwdCBWYWx1ZUVycm9yOgogICAgICAgICAgICBwYXJ0cy5hcHBlbmQoMCkKICAgIHJldHVybiB0dXBsZShwYXJ0cykKCgpkZWYgYnVtcF9mb3JfY29tcGF0aWJsZSh2ZXJzaW9uOiBzdHIpIC0+IHN0cjoKICAgICIiIlRyYW5zbGF0ZSB0aGUgUEVQIDQ0MCBjb21wYXRpYmxlIHJlbGVhc2UgdXBwZXIgYm91bmQuIiIiCiAgICBwaWVjZXMgPSBbaW50KGl0ZW0pIGZvciBpdGVtIGluIHZlcnNpb24uc3BsaXQoJy4nKSBpZiBpdGVtLmlzZGlnaXQoKV0KICAgIGlmIG5vdCBwaWVjZXM6CiAgICAgICAgcmV0dXJuIHZlcnNpb24KICAgIGlmIGxlbihwaWVjZXMpID49IDM6CiAgICAgICAgcmV0dXJuIGYie3BpZWNlc1swXX0ue3BpZWNlc1sxXSArIDF9IgogICAgaWYgbGVuKHBpZWNlcykgPT0gMjoKICAgICAgICByZXR1cm4gZiJ7cGllY2VzWzBdICsgMX0uMCIKICAgIHJldHVybiBzdHIocGllY2VzWzBdICsgMSkKCgpkZWYgZXhwYW5kX2NsYXVzZShvcDogc3RyLCB2ZXJzaW9uOiBzdHIpOgogICAgaWYgb3AgPT0gIn49IjoKICAgICAgICB1cHBlciA9IGJ1bXBfZm9yX2NvbXBhdGlibGUodmVyc2lvbikKICAgICAgICByZXR1cm4gWygiPj0iLCB2ZXJzaW9uKSwgKCI8IiwgdXBwZXIpXQogICAgcmV0dXJuIFsob3AsIHZlcnNpb24pXQoKCmRlZiBwZXA0NDBfdG9fY29uZGEoc3BlYzogc3RyKSAtPiBzdHI6CiAgICAiIiJSZXR1cm4gInB5dGhvbiIgY29uc3RyYWludHMgZXhwYW5kZWQgZnJvbSBhIHJlcXVpcmVzLXB5dGhvbiBzcGVjLiIiIgogICAgY2xhdXNlcyA9IFtdCiAgICBmb3IgcmF3IGluIHNwZWMuc3BsaXQoJywnKToKICAgICAgICByYXcgPSByYXcuc3RyaXAoKQogICAgICAgIGlmIG5vdCByYXc6CiAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgbWF0Y2ggPSBTUEVDX1BBVFRFUk4ubWF0Y2gocmF3KQogICAgICAgIGlmIG5vdCBtYXRjaDoKICAgICAgICAgICAgY29udGludWUKICAgICAgICBvcCwgdmVyc2lvbiA9IG1hdGNoLmdyb3VwcygpCiAgICAgICAgY2xhdXNlcy5leHRlbmQoZXhwYW5kX2NsYXVzZShvcCwgdmVyc2lvbikpCiAgICBpZiBub3QgY2xhdXNlczoKICAgICAgICByZXR1cm4gIiIKICAgIGRlZHVwID0ge30KICAgIGZvciBvcCwgdmVyc2lvbiBpbiBjbGF1c2VzOgogICAgICAgIGRlZHVwWyhvcCwgdmVyc2lvbildID0gKG9wLCB2ZXJzaW9uKQogICAgb3JkZXJlZCA9IHNvcnRlZChkZWR1cC52YWx1ZXMoKSwga2V5PWxhbWJkYSBpdGVtOiAoT1JERVIuZ2V0KGl0ZW1bMF0sIDk5KSwgdmVyc2lvbl9rZXkoaXRlbVsxXSkpKQogICAgcmV0dXJuICJweXRob24iICsgIiwiLmpvaW4oZiJ7b3B9e3ZlcnNpb259IiBmb3Igb3AsIHZlcnNpb24gaW4gb3JkZXJlZCkKCgpkZWYgcmVhZF9ydW50aW1lX3NwZWMoKSAtPiBzdHI6CiAgICBpZiBub3Qgb3MucGF0aC5leGlzdHMoUlVOVElNRV9QQVRIKToKICAgICAgICByZXR1cm4gIiIKICAgIHdpdGggb3BlbihSVU5USU1FX1BBVEgsICdyJywgZW5jb2Rpbmc9J3V0Zi04JywgZXJyb3JzPSdpZ25vcmUnKSBhcyBoYW5kbGU6CiAgICAgICAgdGV4dCA9IGhhbmRsZS5yZWFkKCkKICAgIG1hdGNoID0gcmUuc2VhcmNoKHInKD86cHl0aG9uWy09XSk/XHMqKFswLTldKyg/OlwuWzAtOV0rKXswLDJ9KScsIHRleHQpCiAgICBpZiBub3QgbWF0Y2g6CiAgICAgICAgcmV0dXJuICIiCiAgICBwYXJ0cyA9IG1hdGNoLmdyb3VwKDEpLnNwbGl0KCcuJykKICAgIG1ham9yX21pbm9yID0gJy4nLmpvaW4ocGFydHNbOjJdKQogICAgcmV0dXJuIGYncHl0aG9uPXttYWpvcl9taW5vcn0nCgoKZGVmIHJlYWRfcHlwcm9qZWN0X3NwZWMoKSAtPiBzdHI6CiAgICBpZiBub3Qgb3MucGF0aC5leGlzdHMoUFlQUk9KRUNUX1BBVEgpOgogICAgICAgIHJldHVybiAiIgogICAgd2l0aCBvcGVuKFBZUFJPSkVDVF9QQVRILCAncicsIGVuY29kaW5nPSd1dGYtOCcsIGVycm9ycz0naWdub3JlJykgYXMgaGFuZGxlOgogICAgICAgIHRleHQgPSBoYW5kbGUucmVhZCgpCiAgICBtYXRjaCA9IFBZUFJPSkVDVF9SRS5zZWFyY2godGV4dCkKICAgIGlmIG5vdCBtYXRjaDoKICAgICAgICByZXR1cm4gIiIKICAgIHJldHVybiBwZXA0NDBfdG9fY29uZGEobWF0Y2guZ3JvdXAoMSkpCgoKZGVmIGRldGVjdF9yZXF1aXJlc19weXRob24oKSAtPiBzdHI6CiAgICAiIiJSZXR1cm4gYmVzdC1lZmZvcnQgcmVxdWlyZXMtcHl0aG9uIGNvbnN0cmFpbnQgZm9yIHRoZSBjdXJyZW50IHByb2plY3QuIiIiCiAgICBydW50aW1lX3NwZWMgPSByZWFkX3J1bnRpbWVfc3BlYygpCiAgICBpZiBydW50aW1lX3NwZWM6CiAgICAgICAgcmV0dXJuIHJ1bnRpbWVfc3BlYwogICAgcmV0dXJuIHJlYWRfcHlwcm9qZWN0X3NwZWMoKQoKCmRlZiBtYWluKGFyZ3Y9Tm9uZSkgLT4gTm9uZToKICAgICIiIkNMSSBlbnRyeSBwb2ludCB0aGF0IHByaW50cyBub3JtYWxpemVkIHJlcXVpcmVzLXB5dGhvbiBjb25zdHJhaW50cy4iIiIKICAgIGFyZ3MgPSBsaXN0KHN5cy5hcmd2WzE6XSBpZiBhcmd2IGlzIE5vbmUgZWxzZSBhcmd2KQogICAgaWYgYXJncyBhbmQgYXJnc1swXSA9PSAiLS1zZWxmLXRlc3QiOgogICAgICAgIGZvciBzYW1wbGUgaW4gKCJ+PTMuMTAiLCAifj0zLjguMSIpOgogICAgICAgICAgICBzeXMuc3Rkb3V0LndyaXRlKHBlcDQ0MF90b19jb25kYShzYW1wbGUpICsgIlxuIikKCiAgICAgICAgcmV0dXJuCiAgICBpZiBhcmdzOgogICAgICAgIGZvciBpdGVtIGluIGFyZ3M6CiAgICAgICAgICAgIHN5cy5zdGRvdXQud3JpdGUocGVwNDQwX3RvX2NvbmRhKGl0ZW0pICsgIlxuIikKCiAgICAgICAgcmV0dXJuCiAgICBzeXMuc3Rkb3V0LndyaXRlKGRldGVjdF9yZXF1aXJlc19weXRob24oKSArICJcbiIpCgoKCmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICBtYWluKCkK" set "HP_PRINT_PYVER=aW1wb3J0IHN5cwoKcHJpbnQoZiJweXRob24te3N5cy52ZXJzaW9uX2luZm9bMF19LntzeXMudmVyc2lvbl9pbmZvWzFdfS57c3lzLnZlcnNpb25faW5mb1syXX0iKQo=" diff --git a/tests/test_check_delimiters_import.py b/tests/test_check_delimiters_import.py index 9df3386e..e2875e93 100644 --- a/tests/test_check_delimiters_import.py +++ b/tests/test_check_delimiters_import.py @@ -124,8 +124,19 @@ def test_paren_split_across_echo_lines_at_top_level_is_not_flagged(tmp_path, cap def test_paren_pair_on_same_echo_line_is_not_flagged(tmp_path, capsys): - # A balanced pair on a single echo line (common, e.g. a parenthetical aside) is - # always safe regardless of block nesting -- only a CROSS-line split is risky. + # A balanced pair on a single plain "echo" line (common, e.g. a parenthetical + # aside), one level of block nesting deep, is not flagged by check_delimiters.py + # today -- this is the checker's CURRENT behavior, not a proven-safe claim for + # every shape. CORRECTION (CLAUDE.md Item 61, PR #445): an earlier version of + # this comment claimed same-line pairs are "always safe regardless of block + # nesting" -- real Windows CI in PR #445 disproved that for a DIFFERENT (but + # related) shape: a redirected ">> file echo ..." statement with a same-line + # pair, nested FOUR levels deep, corrupted cmd.exe's parsing ("falling was + # unexpected at this time."). See test_paren_pair_on_redirected_echo_line_deeply_ + # nested_is_a_known_false_negative below, which documents that proven-unsafe + # shape explicitly. This test's own fixture (plain echo, one level deep) has + # NOT itself been proven unsafe or safe on real cmd.exe -- it only documents + # what the checker currently does, pending Item 61's broader fix. bat = tmp_path / "sample.bat" bat.write_text( "@echo off\r\n" @@ -140,3 +151,41 @@ def test_paren_pair_on_same_echo_line_is_not_flagged(tmp_path, capsys): assert result == 0 assert "No delimiter issues found." in captured.out + + +def test_paren_pair_on_redirected_echo_line_deeply_nested_is_a_known_false_negative(tmp_path, capsys): + # Regression fixture for CLAUDE.md Item 61 / PR #445's second real CI incident: + # a same-line, self-contained "(exit 3)" pair inside a ">> file echo ..." + # redirected statement, nested FOUR levels deep inside real if(...) blocks, + # corrupted cmd.exe's block-closing parser on real Windows CI -- confirmed via + # a downloaded diagnostics artifact showing the identical corruption signature + # as the original PR #408 echo-hazard incident ("falling was unexpected at + # this time."). check_delimiters.py does NOT currently flag this shape (same + # as any same-line pair) -- this test documents that as a KNOWN FALSE NEGATIVE, + # not a safe pattern to imitate. Extending the checker to catch this class + # (same-line pairs nested inside a real open block, not just cross-line ones) + # is part of Item 61's scope, not yet implemented. + bat = tmp_path / "sample.bat" + bat.write_text( + "@echo off\r\n" + "if exist \"x\" (\r\n" + " if not errorlevel 1 (\r\n" + " if errorlevel 1 (\r\n" + " if errorlevel 3 (\r\n" + " >> \"%LOG%\" echo unexpected internal error (exit 3); falling back.\r\n" + " )\r\n" + " )\r\n" + " )\r\n" + ")\r\n", + encoding="ascii", + ) + result = check_delimiters.main([str(bat)]) + captured = capsys.readouterr() + + # KNOWN GAP: the checker currently reports this fixture clean even though the + # identical shape corrupted real cmd.exe parsing in production. If this + # assertion ever starts failing (result == 1), it means Item 61's checker + # extension has landed -- update this test to assert the new flagged behavior + # instead of loosening it. + assert result == 0 + assert "No delimiter issues found." in captured.out diff --git a/tests/test_pyproj_deps.py b/tests/test_pyproj_deps.py index 4d469825..01d8eda4 100644 --- a/tests/test_pyproj_deps.py +++ b/tests/test_pyproj_deps.py @@ -111,6 +111,19 @@ def test_default_output_path_used_when_arg_omitted(self): self.assertEqual(rc, 0) self.assertTrue((Path(d) / "~requirements.pyproject.txt").exists()) + def test_unexpected_exception_exits_3_not_1(self): + # CLAUDE.md Active Backlog item 52: a genuine unexpected exception (here, + # "pyproject.toml" existing as a directory instead of a file -- read_text() + # raises before the tomllib try/except ever runs) must not exit 1, or it is + # indistinguishable from the deliberate "nothing to do here" case. Directory- + # not-file is cross-platform: raises IsADirectoryError on POSIX and + # PermissionError on Windows, either way uncaught until the outer except. + with tempfile.TemporaryDirectory() as d: + (Path(d) / "pyproject.toml").mkdir() + rc, _, _ = _run(d, ["out.txt"]) + self.assertEqual(rc, 3) + self.assertFalse((Path(d) / "out.txt").exists()) + class RegexFallbackPath(unittest.TestCase): """tomllib shadowed to be unavailable -- exercises the char-by-char diff --git a/tools/pyproj_deps.py b/tools/pyproj_deps.py index aca23adb..20c4a3d0 100644 --- a/tools/pyproj_deps.py +++ b/tools/pyproj_deps.py @@ -10,6 +10,10 @@ or an empty dependencies list) 2 - malformed TOML (tomllib raised, or -- when tomllib is unavailable -- the regex fallback found an unclosed "[project" header) + 3 - unexpected internal error (e.g. pyproject.toml exists but cannot be + read as a file -- a directory, a permission failure, etc.). Distinct + from 1 so a genuine bug or unusual I/O failure never masquerades as + the benign "nothing to do here" case -- see the outer except below. Prefers stdlib tomllib (3.11+) when available. Falls back to a regex-based extractor when tomllib is missing OR the [project] table has no @@ -36,7 +40,23 @@ out = sys.argv[1] if len(sys.argv) > 1 else '~requirements.pyproject.txt' try: - txt = pathlib.Path('pyproject.toml').read_text(encoding='utf-8', errors='replace') + src = pathlib.Path('pyproject.toml') + try: + # derived requirement: a missing pyproject.toml is the deliberate, + # documented exit-1 "not found" case (in practice run_setup.bat only + # ever invokes this script when the file exists, but this script's + # own contract predates and does not assume that caller). Caught + # specifically here, around the read itself, rather than via a + # preceding Path.exists() check -- Python 3.14+ made exists() swallow + # ANY OSError (including PermissionError) and return False instead of + # raising, which would silently misclassify a genuine permission + # failure as "not found" (1) instead of the real-error case (3) + # below. Any OTHER exception here (a directory, permission denied, + # etc.) is a genuine bug/failure and falls through to the outer + # except, uncaught by this narrower one. + txt = src.read_text(encoding='utf-8', errors='replace') + except FileNotFoundError: + sys.exit(1) deps = None if tomllib: try: @@ -100,4 +120,9 @@ pathlib.Path(out).write_text('\n'.join(deps) + '\n', encoding='ascii', errors='replace') sys.exit(0) except Exception: - sys.exit(1) + # derived requirement: a genuinely unexpected exception (not one of the + # deliberate sys.exit(1)/sys.exit(2) calls above -- those raise + # SystemExit, which this Exception-only handler does not catch) must not + # exit 1, or run_setup.bat's caller cannot tell a real bug apart from the + # intentional "no dependencies found" case. See exit code 3 above. + sys.exit(3)