Skip to content

Commit 9342e4a

Browse files
committed
fix: 2 more Windows test-portability gaps from tonight's CI-red survey
Python UnicodeEncodeError: instance_deform_cache_test.py and large_scene_generated_test.py (backs the _generated/_500/_1000 CTest names) printed "!=" as unicode "≠" and ">=" as "≥" in PASS messages. Windows' default console codepage (cp1252) can't encode those characters, crashing the test harness itself rather than the product under test. Replaced with plain ASCII. Write-protection test assumption: mc3togltf_no_partial_output (STAB-0546b) and mc3tomcb_error_handling (STAB-0538) simulated an unwritable output directory via os.chmod(dir, ...) with POSIX mode bits. On Windows, os.chmod() can only toggle FILE_ATTRIBUTE_READONLY, which Windows ignores for directories, so it never actually blocked writes there -- the CLI succeeded (exit 0) where the test expected rejection. Replaced with a directory-permission-independent mechanism: point the output path at a parent directory that is simply never created, which fails identically ("no such file or directory") on every platform and exercises the same downstream "does a write failure leave partial output" code path without depending on any OS permission model. Verified via a MinGW+Wine repro of both fixes: built mc3togltf.exe/ mc3tomcb.exe for Windows and ran the actual Python test scripts against them through Wine (via a small wrapper script, since this sandbox has no binfmt_misc handler for .exe) -- all 4 sub-cases (STAB-0546a/b, STAB-0537/0538) pass. Also reconfirmed the full mc3togltf_*/mc3tomcb_* suite on Linux (77/80, same 3 already-known Blender/numpy failures, zero regressions).
1 parent db1c6dd commit 9342e4a

5 files changed

Lines changed: 70 additions & 49 deletions

File tree

NEXT.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,34 @@ known (not new) failures, not yet fixed:
333333
any of tonight's work), a CI-runner apt-package gap unrelated to anything
334334
fixed this session.
335335

336+
User asked to keep going on the remaining 4 items. Fixed 2 more:
337+
- **Python `UnicodeEncodeError`**: 2 files (`instance_deform_cache_test.py`,
338+
`large_scene_generated_test.py` — the latter backs all 3 `_generated`/
339+
`_500`/`_1000` CTest names) printed ``/`` in PASS messages; Windows'
340+
default console codepage (cp1252) can't encode those. Replaced with
341+
`!=`/`>=`. Verified unchanged behavior on Linux (still 100% pass);
342+
correctness of the fix itself doesn't depend on Windows-specific
343+
verification since removing non-ASCII output is unconditionally safe on
344+
any codepage.
345+
- **Windows write-protection test assumption**: `mc3togltf_no_partial_output`
346+
(STAB-0546b) and `mc3tomcb_error_handling` (STAB-0538) simulated an
347+
unwritable output directory via `os.chmod(dir, ...)` with POSIX mode
348+
bits — on Windows, `os.chmod()` can only toggle `FILE_ATTRIBUTE_READONLY`,
349+
which Windows ignores for directories, so it never actually blocked
350+
writes there. Replaced with a directory-permission-independent
351+
mechanism: point the output path at a parent directory that's simply
352+
never created. This fails identically on every platform (the exporter's
353+
file-open-for-write call gets "no such file or directory") and tests the
354+
exact same downstream code path ("does a write failure leave partial
355+
output or a non-zero exit"), without relying on OS permission-model
356+
differences at all. Verified via the same MinGW+Wine repro technique as
357+
the `mc3_roundtrip` fix above — built `mc3togltf.exe`/`mc3tomcb.exe` for
358+
Windows and ran both Python test scripts against them through Wine (via
359+
a small wrapper script, since this sandbox has no binfmt_misc handler
360+
for `.exe`): both STAB-0546a/b and STAB-0537/0538 cases pass. Also
361+
reconfirmed the full `mc3togltf_*`/`mc3tomcb_*` suite on Linux (77/80,
362+
same 3 already-known Blender/`numpy` failures, zero regressions).
363+
336364
## Known release blockers and decisions
337365

338366
| Area | Live state |

mc3togltf/test/instance_deform_cache_test.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,19 +77,19 @@ def run(cmd):
7777
f"Undeformed block (mesh {block_a_mesh}) and wide-deformed block "
7878
f"(mesh {wide_a_mesh}) must NOT share a mesh"
7979
)
80-
print(f"No-deform mesh ({block_a_mesh}) wide-deform mesh ({wide_a_mesh}) — PASS")
80+
print(f"No-deform mesh ({block_a_mesh}) != wide-deform mesh ({wide_a_mesh}) — PASS")
8181

8282
assert wide_a_mesh != tall_mesh, (
8383
f"Wide-deformed block (mesh {wide_a_mesh}) and tall-deformed block "
8484
f"(mesh {tall_mesh}) must NOT share a mesh"
8585
)
86-
print(f"Wide-deform mesh ({wide_a_mesh}) tall-deform mesh ({tall_mesh}) — PASS")
86+
print(f"Wide-deform mesh ({wide_a_mesh}) != tall-deform mesh ({tall_mesh}) — PASS")
8787

8888
assert block_a_mesh != tall_mesh, (
8989
f"Undeformed block (mesh {block_a_mesh}) and tall-deformed block "
9090
f"(mesh {tall_mesh}) must NOT share a mesh"
9191
)
92-
print(f"No-deform mesh ({block_a_mesh}) tall-deform mesh ({tall_mesh}) — PASS")
92+
print(f"No-deform mesh ({block_a_mesh}) != tall-deform mesh ({tall_mesh}) — PASS")
9393

9494
# Total unique meshes: 3 (undeformed, wide, tall)
9595
assert len(meshes) == 3, (

mc3togltf/test/large_scene_generated_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def run(cmd):
116116
assert len(nodes) >= total, (
117117
f"Expected at least {total} glTF nodes, got {len(nodes)}"
118118
)
119-
print(f"Node count: {len(nodes)} (expected {total}) — PASS")
119+
print(f"Node count: {len(nodes)} (expected >= {total}) — PASS")
120120

121121
# ----------------------------------------------------------------
122122
# Mesh reuse: 3 unique mesh geometries for 200 nodes.
@@ -127,7 +127,7 @@ def run(cmd):
127127
f"got {len(meshes)} — geometry reuse is not working"
128128
)
129129
assert len(nodes) >= len(meshes) * 10, (
130-
f"Expected nodes ({len(nodes)}) ≥ 10× meshes ({len(meshes)})"
130+
f"Expected nodes ({len(nodes)}) >= 10x meshes ({len(meshes)})"
131131
)
132132
print(f"Unique meshes: {len(meshes)} vs {len(nodes)} nodes — PASS")
133133

mc3togltf/test/no_partial_output_test.py

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@
99
1010
1. Malformed input XML (Mc3Document::loadFromFile() throws before
1111
exportDocument() is even called).
12-
2. A write-protected output directory (WriteGltfSceneToFile() fails at
13-
the file-open step; verified empirically to create zero bytes on
14-
disk, not a truncated/partial file).
12+
2. An unwritable output path -- its parent directory is never created,
13+
so WriteGltfSceneToFile() fails at the file-open step; verified
14+
empirically to create zero bytes on disk, not a truncated/partial
15+
file. (Not a chmod'd read-only directory: Windows' os.chmod() can
16+
only toggle FILE_ATTRIBUTE_READONLY, which Windows ignores for
17+
directories, so that wouldn't actually block writes there.)
1518
1619
Usage: no_partial_output_test.py <mc3togltf> <fixture.mc3.xml>
1720
"""
1821
import os
19-
import stat
2022
import subprocess
2123
import sys
2224
import tempfile
@@ -52,23 +54,17 @@ def run(cmd):
5254
)
5355
print(f"PASS (STAB-0546a): malformed input rejected -- exit {r.returncode}, no output file")
5456

55-
# STAB-0546b: write-protected output directory.
57+
# STAB-0546b: unwritable output path (parent directory never created).
5658
with tempfile.TemporaryDirectory() as tmpdir:
57-
readonly_dir = os.path.join(tmpdir, "readonly")
58-
os.mkdir(readonly_dir)
59-
os.chmod(readonly_dir, stat.S_IRUSR | stat.S_IXUSR) # r-x, no write
60-
try:
61-
out_glb = os.path.join(readonly_dir, "out.glb")
62-
r = run([mc3togltf, fixture, out_glb])
63-
assert r.returncode != 0, (
64-
f"expected a non-zero exit for a write-protected output path, got 0.\n"
65-
f"stdout={r.stdout}\nstderr={r.stderr}"
66-
)
67-
assert r.stderr.strip(), "expected an error message on stderr for a write-protected output path"
68-
assert not os.path.exists(out_glb), (
69-
"expected no output file for a write-protected output path, but one was created "
70-
"(even a zero-length/partial file)"
71-
)
72-
print(f"PASS (STAB-0546b): write-protected output rejected -- exit {r.returncode}, no output file")
73-
finally:
74-
os.chmod(readonly_dir, stat.S_IRWXU) # restore so TemporaryDirectory cleanup can remove it
59+
out_glb = os.path.join(tmpdir, "does_not_exist", "out.glb")
60+
r = run([mc3togltf, fixture, out_glb])
61+
assert r.returncode != 0, (
62+
f"expected a non-zero exit for an unwritable output path, got 0.\n"
63+
f"stdout={r.stdout}\nstderr={r.stderr}"
64+
)
65+
assert r.stderr.strip(), "expected an error message on stderr for an unwritable output path"
66+
assert not os.path.exists(out_glb), (
67+
"expected no output file for an unwritable output path, but one was created "
68+
"(even a zero-length/partial file)"
69+
)
70+
print(f"PASS (STAB-0546b): unwritable output path rejected -- exit {r.returncode}, no output file")

mc3tomcb/test/mc3tomcb_error_test.py

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,19 @@
77
88
STAB-0537: a missing input file (Mc3Document::loadFromFile() throws
99
tinyxml2's XML_ERROR_FILE_NOT_FOUND).
10-
STAB-0538: a write-protected output directory (Mcb::saveToFile() /
10+
STAB-0538: an unwritable output path (Mcb::saveToFile() /
1111
Mc3Document::saveToFile() throw "Cannot open for writing").
12-
Uses a fresh temp dir with its write bit stripped (chmod
13-
0o555) rather than a hardcoded root-owned path, so this
14-
test doesn't depend on the runner's privilege level.
12+
Uses a fresh temp dir with a never-created subdirectory
13+
component in the output path, rather than a hardcoded
14+
root-owned path or a chmod'd read-only directory -- the
15+
former depends on the runner's privilege level, and the
16+
latter doesn't work on Windows (os.chmod() there can only
17+
toggle FILE_ATTRIBUTE_READONLY, which Windows ignores for
18+
directories, so it wouldn't actually block writes).
1519
1620
Usage: mc3tomcb_error_test.py <mc3tomcb-exe> <fixture.mc3.xml>
1721
"""
1822
import os
19-
import stat
2023
import subprocess
2124
import sys
2225
import tempfile
@@ -41,23 +44,17 @@ def main():
4144
assert not os.path.exists(out_mcb), "expected no output file for a missing input file"
4245
print(f"PASS (STAB-0537): missing input rejected -- exit {r.returncode}, stderr: {r.stderr.strip()[:80]}")
4346

44-
# STAB-0538: write-protected output directory.
47+
# STAB-0538: unwritable output path (parent directory never created).
4548
with tempfile.TemporaryDirectory() as tmpdir:
46-
readonly_dir = os.path.join(tmpdir, "readonly")
47-
os.mkdir(readonly_dir)
48-
os.chmod(readonly_dir, stat.S_IRUSR | stat.S_IXUSR) # r-x, no write
49-
try:
50-
out_mcb = os.path.join(readonly_dir, "out.mcb")
51-
r = subprocess.run([exe, fixture, out_mcb], capture_output=True, text=True)
52-
assert r.returncode != 0, (
53-
f"expected a non-zero exit for a write-protected output path, got 0.\n"
54-
f"stdout={r.stdout}\nstderr={r.stderr}"
55-
)
56-
assert r.stderr.strip(), "expected an error message on stderr for a write-protected output path"
57-
assert not os.path.exists(out_mcb), "expected no output file for a write-protected output path"
58-
print(f"PASS (STAB-0538): write-protected output rejected -- exit {r.returncode}, stderr: {r.stderr.strip()[:80]}")
59-
finally:
60-
os.chmod(readonly_dir, stat.S_IRWXU) # restore so TemporaryDirectory cleanup can remove it
49+
out_mcb = os.path.join(tmpdir, "does_not_exist", "out.mcb")
50+
r = subprocess.run([exe, fixture, out_mcb], capture_output=True, text=True)
51+
assert r.returncode != 0, (
52+
f"expected a non-zero exit for an unwritable output path, got 0.\n"
53+
f"stdout={r.stdout}\nstderr={r.stderr}"
54+
)
55+
assert r.stderr.strip(), "expected an error message on stderr for an unwritable output path"
56+
assert not os.path.exists(out_mcb), "expected no output file for an unwritable output path"
57+
print(f"PASS (STAB-0538): unwritable output path rejected -- exit {r.returncode}, stderr: {r.stderr.strip()[:80]}")
6158

6259

6360
if __name__ == "__main__":

0 commit comments

Comments
 (0)