From 9dae210f7fec9820f29e03b4e9d9a4999231fd76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Dausseur?= Date: Sat, 6 Jun 2026 21:39:36 +0200 Subject: [PATCH] fix(windows): UTF-8 console + self-sufficient validation wrapper Make the suite run cleanly on Windows. Product code: - __init__.py: force UTF-8 on stdout/stderr. The Windows console code page (cp1252) cannot encode the box-drawing/accented characters the runner prints, which crashed the parent capture_stdout thread. Only the stream encoders are reconfigured; the locale default used to read cp1252 config files is left untouched. - report_export_junit/html: open the report file with encoding="utf-8" (XML/HTML are UTF-8) instead of the platform default, matching the txt/json exporters. Validation: - run.bat: source mode now sets up its own venv and runs testium from src\ directly instead of delegating to the project run.bat (which launches the GUI and drops its arguments). Installs the fake_exporter entry-point plugin (report_plugin) and the [lsp] extra, and runs the same lsp_check.py pre-flight as run.sh. - jsonrpc/test.tum: launch the echo server via "$(python_bin)" instead of "python3" (the Microsoft Store stub on Windows). - post_execution.py: write the JUnit XML with encoding="utf-8". - restore items/run/sub_pass.tum and sub_fail.tum, deleted by mistake in d97d00c "removed test logs". --- src/testium/__init__.py | 10 ++++ .../test_report/report_export_html.py | 2 +- .../test_report/report_export_junit.py | 2 +- test/validation/items/jsonrpc/test.tum | 2 +- test/validation/items/run/sub_fail.tum | 7 +++ test/validation/items/run/sub_pass.tum | 7 +++ test/validation/post_execution.py | 2 +- test/validation/run.bat | 56 +++++++++++++++++-- 8 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 test/validation/items/run/sub_fail.tum create mode 100644 test/validation/items/run/sub_pass.tum diff --git a/src/testium/__init__.py b/src/testium/__init__.py index 30a7c84..daebb43 100755 --- a/src/testium/__init__.py +++ b/src/testium/__init__.py @@ -11,6 +11,16 @@ sys.path.append(os.path.abspath(ourpath.parent)) import interpreter.utils.constants as cst def main(): + # Force UTF-8 on stdout/stderr so the runner's output survives a legacy + # console code page (Windows cp1252 can't encode box-drawing/accented + # chars). Only the stream encoders change; the locale default used for + # config files is untouched. + for _stream in (sys.stdout, sys.stderr): + try: + _stream.reconfigure(encoding="utf-8") + except (AttributeError, ValueError): + pass # no stdout (frozen GUI) or non-reconfigurable stream + # Subcommand dispatch (must run *before* argparse so neither 'schema' nor # 'lsp' has to share the GUI/batch flag surface). The subcommands also # skip the multiprocessing 'spawn' setup which is only meaningful for the diff --git a/src/testium/interpreter/test_report/report_export_html.py b/src/testium/interpreter/test_report/report_export_html.py index 023ec58..bd4f9ce 100644 --- a/src/testium/interpreter/test_report/report_export_html.py +++ b/src/testium/interpreter/test_report/report_export_html.py @@ -14,7 +14,7 @@ class ReportExportHTML(rpe.ReportExport): self.prepareFile() self.create_base() self.process_tests() - with open(self._file_name, 'w') as f: + with open(self._file_name, 'w', encoding="utf-8") as f: f.write(lxml.html.tostring(self.root, pretty_print=True).decode()) def testsIterate(self, row): diff --git a/src/testium/interpreter/test_report/report_export_junit.py b/src/testium/interpreter/test_report/report_export_junit.py index 5f61d91..b9cb007 100644 --- a/src/testium/interpreter/test_report/report_export_junit.py +++ b/src/testium/interpreter/test_report/report_export_junit.py @@ -20,7 +20,7 @@ class ReportExportJUnit(rpe.ReportExport): ts = TestSuite(repname, test_cases=self.test_cases, hostname=tm.gd('host_ip')) - with open(self._file_name, 'w') as f: + with open(self._file_name, 'w', encoding="utf-8") as f: TestSuite.to_file(f, [ts]) def testsIterate(self, row): diff --git a/test/validation/items/jsonrpc/test.tum b/test/validation/items/jsonrpc/test.tum index dfe0d60..0ea39de 100644 --- a/test/validation/items/jsonrpc/test.tum +++ b/test/validation/items/jsonrpc/test.tum @@ -20,7 +20,7 @@ console_name: jrpces key: $(test)_PASS steps: - - writeln: python3 {{include_directory}}/jrpc_echo_server.py -c {{include_directory}}/jrpces.ini + - writeln: '"$(python_bin)" {{include_directory}}/jrpc_echo_server.py -c {{include_directory}}/jrpces.ini' - read_until: {expected: ready, timeout: 5} - console: diff --git a/test/validation/items/run/sub_fail.tum b/test/validation/items/run/sub_fail.tum new file mode 100644 index 0000000..c86b417 --- /dev/null +++ b/test/validation/items/run/sub_fail.tum @@ -0,0 +1,7 @@ +main: + name: run sub-test (always fail) + steps: + - check: + name: fail + values: + - false diff --git a/test/validation/items/run/sub_pass.tum b/test/validation/items/run/sub_pass.tum new file mode 100644 index 0000000..072658f --- /dev/null +++ b/test/validation/items/run/sub_pass.tum @@ -0,0 +1,7 @@ +main: + name: run sub-test (always pass) + steps: + - check: + name: pass + values: + - true diff --git a/test/validation/post_execution.py b/test/validation/post_execution.py index b0ae4df..bb86d4f 100644 --- a/test/validation/post_execution.py +++ b/test/validation/post_execution.py @@ -89,7 +89,7 @@ def exec(): junit_report = report.replace(".sqlite", f"-{test}.xml") print(junit_report) _prepare_file_to_save(junit_report) - with open(junit_report, "w") as f: + with open(junit_report, "w", encoding="utf-8") as f: f.write(TestSuite.to_xml_string([ts])) # cleanup diff --git a/test/validation/run.bat b/test/validation/run.bat index f540504..33a1bf5 100644 --- a/test/validation/run.bat +++ b/test/validation/run.bat @@ -89,6 +89,13 @@ REM Reports are stamped with the mode so successive runs don't clobber each othe SET "TAIL=-b -d "python_bin=%VENV_PYTHON%" -d "validation_report_file=validation-%MODE%" -- "%SCRIPT_DIR%\main.tum"%EXTRA%" +REM The report-exporter plugin (items\report_plugin) is a pip entry-point +REM package. It must live in the *testium* environment, so it is installed into +REM the source/wheel venvs below. A frozen PyInstaller binary cannot see +REM externally-installed plugins, so report_plugin is expected to be skipped +REM there (same as Linux pyinstaller mode). +SET "FAKE_EXPORTER=%SCRIPT_DIR%\fake_exporter" + REM ---------- per-mode launcher ---------------------------------------------- echo -- validation mode: %MODE% @@ -100,8 +107,25 @@ echo ERROR: unknown --mode '%MODE%'. Expected: source ^| wheel ^| pyinstaller. exit /b 1 :MODE_SOURCE -call "%PROJECT_DIR%\run.bat" %TAIL% -exit /b %ERRORLEVEL% +REM Run testium from src\ in a dedicated venv set up here. We do NOT delegate to +REM the project's run.bat: that one launches the GUI and does not forward its +REM arguments, so the suite would never run head-less. +SET "TESTIUM_VENV=%PROJECT_DIR%\test\tmp\testium_venv" +IF NOT EXIST "%TESTIUM_VENV%" ( + echo Creating testium venv at %TESTIUM_VENV% + %PYTHON_EXE% -m venv "%TESTIUM_VENV%" + IF !ERRORLEVEL! NEQ 0 ( + echo ERROR while creating the testium venv. + exit /b 1 + ) + call "%TESTIUM_VENV%\Scripts\pip" install --quiet --upgrade pip + call "%TESTIUM_VENV%\Scripts\pip" install --quiet -r "%PROJECT_DIR%\src\requirements.txt" + REM language-server extra so `testium lsp` works from source (lsp_check.py) + call "%TESTIUM_VENV%\Scripts\pip" install --quiet "pygls>=1.3" +) +call "%TESTIUM_VENV%\Scripts\pip" install --quiet -e "%FAKE_EXPORTER%" +SET CMD="%TESTIUM_VENV%\Scripts\python.exe" "%PROJECT_DIR%\src\testium" +GOTO LAUNCH :MODE_WHEEL SET "WHEEL=%PROJECT_DIR%\dist\testium-%VERSION%-py3-none-any.whl" @@ -115,10 +139,13 @@ IF NOT EXIST "%WHEEL_VENV%" ( echo Creating wheel venv at %WHEEL_VENV% %PYTHON_EXE% -m venv --system-site-packages "%WHEEL_VENV%" call "%WHEEL_VENV%\Scripts\pip" install --quiet --upgrade pip - call "%WHEEL_VENV%\Scripts\pip" install --quiet "%WHEEL%" + REM install with the [lsp] extra so the wheel channel is validated in its + REM language-server-capable form (pulls pygls), matching `pip install testium[lsp]`. + call "%WHEEL_VENV%\Scripts\pip" install --quiet "%WHEEL%[lsp]" ) -"%WHEEL_VENV%\Scripts\python.exe" -m testium %TAIL% -exit /b %ERRORLEVEL% +call "%WHEEL_VENV%\Scripts\pip" install --quiet -e "%FAKE_EXPORTER%" +SET CMD="%WHEEL_VENV%\Scripts\python.exe" -m testium +GOTO LAUNCH :MODE_PYI SET "PYI_BIN=%PROJECT_DIR%\dist\testium-%VERSION%.exe" @@ -127,5 +154,22 @@ IF NOT EXIST "%PYI_BIN%" ( echo ERROR: PyInstaller binary not found in %PROJECT_DIR%\dist -- run build_all.sh first. exit /b 1 ) -"%PYI_BIN%" %TAIL% +SET CMD="%PYI_BIN%" +GOTO LAUNCH + +REM ---------- launch ---------------------------------------------------------- + +:LAUNCH +echo -- launch: %CMD% + +REM LSP check (this exact channel): `schema` must keep its nested actions and +REM `lsp` must answer initialize. Mirrors run.sh; aborts the run on failure. +echo -- LSP check (%MODE%) +"%VENV_PYTHON%" "%SCRIPT_DIR%\lsp_check.py" %CMD% +IF !ERRORLEVEL! NEQ 0 ( + echo ERROR: LSP check failed for mode %MODE%. + exit /b 1 +) + +%CMD% %TAIL% exit /b %ERRORLEVEL%