fix(issue-26): review - an unreadable env file is an error, NUL lines are skipped, integers are ASCII in every locale, empty flags are refused
Merge-workspace review of PR #43 (codex R1-R4, claude R1/R2).
- R1: envfile.parse raises EnvFileError when the file exists but cannot be
read or decoded as a whole; `--shell` prints the cause and exits 1, so the
wrapper stops (rc 1, as before #26) instead of running on defaults, and
mqtt_common's entry points stop on the same error. A bad single line is
still a warning. The wrapper's message no longer claims a read failure
when the cause is elsewhere ("cannot load ... see the line above").
- R2: lib.sh mam_int spells out the ASCII blanks and digits instead of
[[:space:]]/[0-9], which follow the locale: U+2003 30 U+2003 was 30 in the
wrapper and invalid in the runner under en_US.UTF-8. The test oracle
_expected uses the same ASCII rule.
- R3: an integer flag given explicitly with an empty value is refused like
any other invalid value; only an absent flag keeps its default.
- R4: a line holding a NUL byte is skipped with a warning by both loaders;
before, the wrapper read "leftright" and the Python loader died.
- .mam.env.example states the format: no expansion, `export` and bash's own
variables skipped, a trailing `# ...` is part of the value, the environment
wins, an unreadable file is an error.
Tests: unreadable (0000) and invalid-UTF-8 files stop the wrapper and the
Python loader; a NUL line is skipped by both; Unicode blanks/digits are
invalid for mam_int, _positive_int, the wrapper and the runner under C and
en_US.UTF-8; explicit empty values for the five integer flags are refused.
All of them fail on b58edac.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
1 parent
b58edace11
commit
0eec795cd1
7 files changed
+128
-19
No files matched your search
@@ -86,8 +86,8 @@ NOT_ARITHMETIC = {
|
||||
|
||||
|
||||
def _expected(value: str, minimum: int):
|
||||
s = value.strip()
|
||||
if not s.isdigit():
|
||||
s = value.strip(" \t\n\r\v\f") # ASCII blanks only, in every locale (review R2)
|
||||
if not s.isascii() or not s.isdigit():
|
||||
return None
|
||||
s = s.lstrip("0") or "0"
|
||||
if len(s) > 9:
|
||||
@@ -182,6 +182,28 @@ def test_b1_mam_int_and_positive_int_are_one_rule(tmp_path, minimum):
|
||||
assert want[INPUTS.index("0700")] == 700
|
||||
|
||||
|
||||
# Review R2: bash's [[:space:]] and [0-9] follow the locale; the rule is ASCII.
|
||||
UNICODE_INPUTS = ["\u200330\u2003", "\u00a030", "30\u3000", "\u0663\u0660", "\uff13\uff10"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("locale", ["C", "en_US.UTF-8"])
|
||||
@pytest.mark.parametrize("value", UNICODE_INPUTS, ids=lambda v: v.encode("unicode_escape").decode())
|
||||
def test_b1_unicode_blanks_and_digits_are_invalid_in_every_locale(tmp_path, locale, value):
|
||||
if str(SKILLS) not in sys.path:
|
||||
sys.path.insert(0, str(SKILLS))
|
||||
from lib_py.loop.runner import _positive_int
|
||||
|
||||
loc = {"LC_ALL": locale, "LANG": locale}
|
||||
res = _bash('mam_int "$PROBE" 1 || echo invalid', tmp_path, {**loc, "PROBE": value})
|
||||
assert res.stdout.strip() == "invalid", (res.stdout, res.stderr)
|
||||
assert _positive_int(value) is None and _expected(value, 1) is None
|
||||
rec = _record(tmp_path, _dry_submit(tmp_path, {**loc, "MAM_JOB_TIMEOUT_S": value}))
|
||||
rres = _runner(tmp_path, {**loc, "MAM_JOB_TIMEOUT_S": value})
|
||||
assert rres.returncode == 0, rres.stderr[-500:]
|
||||
got = json.loads(rres.stdout.strip().splitlines()[-1])
|
||||
assert rec["timeout_sec"] == got["budget"] == 3600, (rec["timeout_sec"], got)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("knob", ["SKS_DIALOG_TIMEOUT", "SKS_BUSY_TIMEOUT", "SKS_DELIVERY_GRACE_S"])
|
||||
def test_b1_sks_knobs_keep_zero_and_read_decimal(tmp_path, knob):
|
||||
minimum, default = INT_KNOBS[knob]
|
||||
@@ -204,7 +226,7 @@ def test_b2_wrapper_flags_are_decimal_or_refused(tmp_path):
|
||||
ok = _dry_submit(tmp_path, None, "--timeout", "30")
|
||||
assert _record(tmp_path, ok)["timeout_sec"] == 30
|
||||
for flag in ("--timeout", "--idle-timeout", "--heartbeat", "--heartbeat-misses", "--ack-timeout"):
|
||||
for bad in ("abc", "0", "-5", "9999999999", "9" * 5000):
|
||||
for bad in ("abc", "0", "-5", "9999999999", "9" * 5000, ""): # "" given explicitly (review R3)
|
||||
res = _dry_submit(tmp_path, None, flag, bad)
|
||||
assert res.returncode == 1, (flag, bad[:20], res.stdout, res.stderr[-500:])
|
||||
assert f"ERROR: {flag} {bad}: not a positive decimal integer" in res.stderr, res.stderr[-500:]
|
||||
|
||||
Reference in new issue
Block a user