Skip to content

Commit 6a62d11

Browse files
committed
pythongh-121188: Escape XML characters in regrtest
When creating the JUnit XML file, regrtest now escapes characters which are invalid in XML such as the chr(27) control character used in ANSI escape sequences.
1 parent bd473aa commit 6a62d11

File tree

4 files changed

+72
-5
lines changed

4 files changed

+72
-5
lines changed

Lib/test/libregrtest/testresult.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import traceback
1010
import unittest
1111
from test import support
12+
from test.libregrtest.utils import escape_xml
1213

1314
class RegressionTestResult(unittest.TextTestResult):
1415
USE_XML = False
@@ -65,23 +66,24 @@ def _add_result(self, test, capture=False, **args):
6566
if capture:
6667
if self._stdout_buffer is not None:
6768
stdout = self._stdout_buffer.getvalue().rstrip()
68-
ET.SubElement(e, 'system-out').text = stdout
69+
ET.SubElement(e, 'system-out').text = escape_xml(stdout)
6970
if self._stderr_buffer is not None:
7071
stderr = self._stderr_buffer.getvalue().rstrip()
71-
ET.SubElement(e, 'system-err').text = stderr
72+
ET.SubElement(e, 'system-err').text = escape_xml(stderr)
7273

7374
for k, v in args.items():
7475
if not k or not v:
7576
continue
77+
7678
e2 = ET.SubElement(e, k)
7779
if hasattr(v, 'items'):
7880
for k2, v2 in v.items():
7981
if k2:
80-
e2.set(k2, str(v2))
82+
e2.set(k2, escape_xml(str(v2)))
8183
else:
82-
e2.text = str(v2)
84+
e2.text = escape_xml(str(v2))
8385
else:
84-
e2.text = str(v)
86+
e2.text = escape_xml(str(v))
8587

8688
@classmethod
8789
def __makeErrorDict(cls, err_type, err_value, err_tb):

Lib/test/libregrtest/utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os.path
66
import platform
77
import random
8+
import re
89
import shlex
910
import signal
1011
import subprocess
@@ -712,3 +713,19 @@ def get_signal_name(exitcode):
712713
pass
713714

714715
return None
716+
717+
718+
ILLEGAL_XML_CHARS_RE = re.compile(
719+
'['
720+
'\x00-\x1F' # ASCII control characters
721+
'\uD800-\uDFFF' # surrogate characters
722+
'\uFFFE'
723+
'\uFFFF'
724+
']')
725+
726+
def _escape_xml_replace(regs):
727+
code_point = ord(regs[0])
728+
return f"&#{code_point};"
729+
730+
def escape_xml(text):
731+
return ILLEGAL_XML_CHARS_RE.sub(_escape_xml_replace, text)

Lib/test/test_regrtest.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import tempfile
2222
import textwrap
2323
import unittest
24+
from xml.etree import ElementTree
25+
2426
from test import support
2527
from test.support import import_helper
2628
from test.support import os_helper
@@ -2254,6 +2256,44 @@ def test_pass(self):
22542256
self.check_executed_tests(output, testname, stats=1, parallel=True)
22552257
self.assertNotIn('SPAM SPAM SPAM', output)
22562258

2259+
def test_xml(self):
2260+
code = textwrap.dedent(r"""
2261+
import unittest
2262+
from test import support
2263+
2264+
class VerboseTests(unittest.TestCase):
2265+
def test_failed(self):
2266+
print("abc \x1b def")
2267+
self.fail()
2268+
""")
2269+
testname = self.create_test(code=code)
2270+
2271+
# Run sequentially
2272+
filename = os_helper.TESTFN
2273+
self.addCleanup(os_helper.unlink, filename)
2274+
2275+
output = self.run_tests(testname, "--junit-xml", filename,
2276+
exitcode=EXITCODE_BAD_TEST)
2277+
self.check_executed_tests(output, testname,
2278+
failed=testname,
2279+
stats=TestStats(1, 1, 0))
2280+
2281+
# Test generated XML
2282+
with open(filename, encoding="utf8") as fp:
2283+
content = fp.read()
2284+
2285+
testsuite = ElementTree.fromstring(content)
2286+
self.assertEqual(int(testsuite.get('tests')), 1)
2287+
self.assertEqual(int(testsuite.get('errors')), 0)
2288+
self.assertEqual(int(testsuite.get('failures')), 1)
2289+
2290+
testcase = testsuite[0][0]
2291+
self.assertEqual(testcase.get('status'), 'run')
2292+
self.assertEqual(testcase.get('result'), 'completed')
2293+
self.assertGreater(float(testcase.get('time')), 0)
2294+
for out in testcase.iter('system-out'):
2295+
self.assertEqual(out.text, "abc  def")
2296+
22572297

22582298
class TestUtils(unittest.TestCase):
22592299
def test_format_duration(self):
@@ -2437,6 +2477,11 @@ def id(self):
24372477
self.assertTrue(match_test(test_chdir))
24382478
self.assertFalse(match_test(test_copy))
24392479

2480+
def test_escape_xml(self):
2481+
escape_xml = utils.escape_xml
2482+
self.assertEqual(escape_xml('abc \x1b def'),
2483+
'abc  def')
2484+
24402485

24412486
if __name__ == '__main__':
24422487
unittest.main()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
When creating the JUnit XML file, regrtest now escapes characters which are
2+
invalid in XML such as the chr(27) control character used in ANSI escape
3+
sequences. Patch by Victor Stinner.

0 commit comments

Comments
 (0)