Skip to content

Commit 83a2837

Browse files
gh-106368: Increase Argument Clinic CLI test coverage (#107156)
Instead of hacking into the Clinic class, use the Argument Clinic tool to run the ClinicExternalTest test suite. Co-authored-by: Nikita Sobolev <[email protected]>
1 parent 3071867 commit 83a2837

File tree

1 file changed

+176
-14
lines changed

1 file changed

+176
-14
lines changed

Lib/test/test_clinic.py

+176-14
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44

55
from test import support, test_tools
66
from test.support import os_helper
7+
from test.support import SHORT_TIMEOUT, requires_subprocess
8+
from test.support.os_helper import TESTFN, unlink
79
from textwrap import dedent
810
from unittest import TestCase
911
import collections
1012
import inspect
1113
import os.path
14+
import subprocess
1215
import sys
1316
import unittest
1417

@@ -1346,31 +1349,190 @@ def test_scaffolding(self):
13461349
class ClinicExternalTest(TestCase):
13471350
maxDiff = None
13481351

1352+
def _do_test(self, *args, expect_success=True):
1353+
clinic_py = os.path.join(test_tools.toolsdir, "clinic", "clinic.py")
1354+
with subprocess.Popen(
1355+
[sys.executable, "-Xutf8", clinic_py, *args],
1356+
encoding="utf-8",
1357+
bufsize=0,
1358+
stdout=subprocess.PIPE,
1359+
stderr=subprocess.PIPE,
1360+
) as proc:
1361+
proc.wait()
1362+
if expect_success == bool(proc.returncode):
1363+
self.fail("".join(proc.stderr))
1364+
stdout = proc.stdout.read()
1365+
stderr = proc.stderr.read()
1366+
# Clinic never writes to stderr.
1367+
self.assertEqual(stderr, "")
1368+
return stdout
1369+
1370+
def expect_success(self, *args):
1371+
return self._do_test(*args)
1372+
1373+
def expect_failure(self, *args):
1374+
return self._do_test(*args, expect_success=False)
1375+
13491376
def test_external(self):
13501377
CLINIC_TEST = 'clinic.test.c'
1351-
# bpo-42398: Test that the destination file is left unchanged if the
1352-
# content does not change. Moreover, check also that the file
1353-
# modification time does not change in this case.
13541378
source = support.findfile(CLINIC_TEST)
13551379
with open(source, 'r', encoding='utf-8') as f:
13561380
orig_contents = f.read()
13571381

1358-
with os_helper.temp_dir() as tmp_dir:
1359-
testfile = os.path.join(tmp_dir, CLINIC_TEST)
1360-
with open(testfile, 'w', encoding='utf-8') as f:
1361-
f.write(orig_contents)
1362-
old_mtime_ns = os.stat(testfile).st_mtime_ns
1363-
1364-
clinic.parse_file(testfile)
1382+
# Run clinic CLI and verify that it does not complain.
1383+
self.addCleanup(unlink, TESTFN)
1384+
out = self.expect_success("-f", "-o", TESTFN, source)
1385+
self.assertEqual(out, "")
13651386

1366-
with open(testfile, 'r', encoding='utf-8') as f:
1367-
new_contents = f.read()
1368-
new_mtime_ns = os.stat(testfile).st_mtime_ns
1387+
with open(TESTFN, 'r', encoding='utf-8') as f:
1388+
new_contents = f.read()
13691389

13701390
self.assertEqual(new_contents, orig_contents)
1391+
1392+
def test_no_change(self):
1393+
# bpo-42398: Test that the destination file is left unchanged if the
1394+
# content does not change. Moreover, check also that the file
1395+
# modification time does not change in this case.
1396+
code = dedent("""
1397+
/*[clinic input]
1398+
[clinic start generated code]*/
1399+
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=da39a3ee5e6b4b0d]*/
1400+
""")
1401+
with os_helper.temp_dir() as tmp_dir:
1402+
fn = os.path.join(tmp_dir, "test.c")
1403+
with open(fn, "w", encoding="utf-8") as f:
1404+
f.write(code)
1405+
pre_mtime = os.stat(fn).st_mtime_ns
1406+
self.expect_success(fn)
1407+
post_mtime = os.stat(fn).st_mtime_ns
13711408
# Don't change the file modification time
13721409
# if the content does not change
1373-
self.assertEqual(new_mtime_ns, old_mtime_ns)
1410+
self.assertEqual(pre_mtime, post_mtime)
1411+
1412+
def test_cli_force(self):
1413+
invalid_input = dedent("""
1414+
/*[clinic input]
1415+
output preset block
1416+
module test
1417+
test.fn
1418+
a: int
1419+
[clinic start generated code]*/
1420+
1421+
const char *hand_edited = "output block is overwritten";
1422+
/*[clinic end generated code: output=bogus input=bogus]*/
1423+
""")
1424+
fail_msg = dedent("""
1425+
Checksum mismatch!
1426+
Expected: bogus
1427+
Computed: 2ed19
1428+
Suggested fix: remove all generated code including the end marker,
1429+
or use the '-f' option.
1430+
""")
1431+
with os_helper.temp_dir() as tmp_dir:
1432+
fn = os.path.join(tmp_dir, "test.c")
1433+
with open(fn, "w", encoding="utf-8") as f:
1434+
f.write(invalid_input)
1435+
# First, run the CLI without -f and expect failure.
1436+
# Note, we cannot check the entire fail msg, because the path to
1437+
# the tmp file will change for every run.
1438+
out = self.expect_failure(fn)
1439+
self.assertTrue(out.endswith(fail_msg))
1440+
# Then, force regeneration; success expected.
1441+
out = self.expect_success("-f", fn)
1442+
self.assertEqual(out, "")
1443+
# Verify by checking the checksum.
1444+
checksum = (
1445+
"/*[clinic end generated code: "
1446+
"output=2124c291eb067d76 input=9543a8d2da235301]*/\n"
1447+
)
1448+
with open(fn, 'r', encoding='utf-8') as f:
1449+
generated = f.read()
1450+
self.assertTrue(generated.endswith(checksum))
1451+
1452+
def test_cli_verbose(self):
1453+
with os_helper.temp_dir() as tmp_dir:
1454+
fn = os.path.join(tmp_dir, "test.c")
1455+
with open(fn, "w", encoding="utf-8") as f:
1456+
f.write("")
1457+
out = self.expect_success("-v", fn)
1458+
self.assertEqual(out.strip(), fn)
1459+
1460+
def test_cli_help(self):
1461+
out = self.expect_success("-h")
1462+
self.assertIn("usage: clinic.py", out)
1463+
1464+
def test_cli_converters(self):
1465+
prelude = dedent("""
1466+
Legacy converters:
1467+
B C D L O S U Y Z Z#
1468+
b c d f h i l p s s# s* u u# w* y y# y* z z# z*
1469+
1470+
Converters:
1471+
""")
1472+
expected_converters = (
1473+
"bool",
1474+
"byte",
1475+
"char",
1476+
"defining_class",
1477+
"double",
1478+
"fildes",
1479+
"float",
1480+
"int",
1481+
"long",
1482+
"long_long",
1483+
"object",
1484+
"Py_buffer",
1485+
"Py_complex",
1486+
"Py_ssize_t",
1487+
"Py_UNICODE",
1488+
"PyByteArrayObject",
1489+
"PyBytesObject",
1490+
"self",
1491+
"short",
1492+
"size_t",
1493+
"slice_index",
1494+
"str",
1495+
"unicode",
1496+
"unsigned_char",
1497+
"unsigned_int",
1498+
"unsigned_long",
1499+
"unsigned_long_long",
1500+
"unsigned_short",
1501+
)
1502+
finale = dedent("""
1503+
Return converters:
1504+
bool()
1505+
double()
1506+
float()
1507+
init()
1508+
int()
1509+
long()
1510+
Py_ssize_t()
1511+
size_t()
1512+
unsigned_int()
1513+
unsigned_long()
1514+
1515+
All converters also accept (c_default=None, py_default=None, annotation=None).
1516+
All return converters also accept (py_default=None).
1517+
""")
1518+
out = self.expect_success("--converters")
1519+
# We cannot simply compare the output, because the repr of the *accept*
1520+
# param may change (it's a set, thus unordered). So, let's compare the
1521+
# start and end of the expected output, and then assert that the
1522+
# converters appear lined up in alphabetical order.
1523+
self.assertTrue(out.startswith(prelude), out)
1524+
self.assertTrue(out.endswith(finale), out)
1525+
1526+
out = out.removeprefix(prelude)
1527+
out = out.removesuffix(finale)
1528+
lines = out.split("\n")
1529+
for converter, line in zip(expected_converters, lines):
1530+
line = line.lstrip()
1531+
with self.subTest(converter=converter):
1532+
self.assertTrue(
1533+
line.startswith(converter),
1534+
f"expected converter {converter!r}, got {line!r}"
1535+
)
13741536

13751537

13761538
try:

0 commit comments

Comments
 (0)