From 5926aca90bd14dfae9366a124c2ae1ac46c81696 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Fri, 16 Oct 2026 08:56:03 +0200 Subject: [PATCH] tools: tolerate non-UTF-8 test output Non-UTF-8 bytes in captured test output cause Execute() to raise UnicodeDecodeError. CI reports can then attribute the failure to an earlier, expected "Error: uncaught" message from a passing cctest. Replace malformed UTF-8 in stdout and stderr so the runner reports test results while preserving the child process exit status. Refs: https://github.com/nodejs/reliability/blob/main/reports/2026-10-16.md Signed-off-by: Filip Skokan Assisted-by: Codex --- test/tools/test_testrunner_output.py | 36 ++++++++++++++++++++++++++++ tools/test.py | 6 +++-- 2 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 test/tools/test_testrunner_output.py diff --git a/test/tools/test_testrunner_output.py b/test/tools/test_testrunner_output.py new file mode 100644 index 000000000000..087967752ebe --- /dev/null +++ b/test/tools/test_testrunner_output.py @@ -0,0 +1,36 @@ +# Copyright Node.js contributors. All rights reserved. +# Use of this source code is governed by an MIT-style license that can be +# found in the LICENSE file. + +import os +import sys +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import patch + +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.insert(0, os.path.join(ROOT, 'tools')) +import test as runner + + +class TestRunnerOutputTest(unittest.TestCase): + def test_invalid_utf8(self): + context = SimpleNamespace(verbose=False, suppress_dialogs=False, + abort_on_timeout=False) + with tempfile.TemporaryDirectory() as directory: + with patch.object(runner.tempfile, 'tempdir', directory): + for stream, fd in [('stdout', 1), ('stderr', 2)]: + for exit_code in [0, 3]: + with self.subTest(stream=stream, exit_code=exit_code): + script = (f"import os; os.write({fd}, b'valid \\xc3\\xa9\\ninvalid \\xff\\r\\n'); " + f"raise SystemExit({exit_code})") + result = runner.Execute([sys.executable, '-c', script], context, timeout=10) + self.assertEqual(getattr(result, stream), 'valid é\ninvalid \ufffd\n') + self.assertEqual(result.exit_code, exit_code) + self.assertFalse(result.timed_out) + self.assertEqual(os.listdir(directory), []) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/test.py b/tools/test.py index fee5172b41bb..58217f1d11c5 100755 --- a/tools/test.py +++ b/tools/test.py @@ -836,8 +836,10 @@ def setResourceLimits(): ) os.close(fd_out) os.close(fd_err) - output = open(outname, encoding='utf8').read() - errors = open(errname, encoding='utf8').read() + with open(outname, encoding='utf8', errors='replace') as output_file: + output = output_file.read() + with open(errname, encoding='utf8', errors='replace') as error_file: + errors = error_file.read() CheckedUnlink(outname) CheckedUnlink(errname)