Commit e2d55227730 for nodejs

commit e2d55227730b77802c142519b7ef04487604cff3
Author: Filip Skokan <panva.ip@gmail.com>
Date:   Sun Oct 18 09:18:16 2026 +0200

    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 <panva.ip@gmail.com>
    Assisted-by: Codex
    PR-URL: https://github.com/nodejs/node/pull/66783
    Reviewed-By: Richard Lau <richard.lau@ibm.com>
    Reviewed-By: Ulises Gascón <ulisesgascongonzalez@gmail.com>

diff --git a/test/tools/test_testrunner_output.py b/test/tools/test_testrunner_output.py
new file mode 100644
index 00000000000..087967752eb
--- /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 fee5172b41b..58217f1d11c 100755
--- a/tools/test.py
+++ b/tools/test.py
@@ -836,8 +836,10 @@ def Execute(args, context, timeout=None, env=None, disable_core_files=False,
   )
   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)