Skip to content

Commit cebe60b

Browse files
miss-islingtonerlend-aaslandsobolevn
authored
[3.11] gh-106368: Increase Argument Clinic CLI test coverage (GH-107156) (#107190)
Instead of hacking into the Clinic class, use the Argument Clinic tool to run the ClinicExternalTest test suite. (cherry picked from commit 83a2837) Co-authored-by: Erlend E. Aasland <[email protected]> Co-authored-by: Nikita Sobolev <[email protected]>
1 parent 55edee2 commit cebe60b

File tree

1 file changed

+177
-14
lines changed

1 file changed

+177
-14
lines changed

Lib/test/test_clinic.py

Lines changed: 177 additions & 14 deletions
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

@@ -1292,31 +1295,191 @@ def test_scaffolding(self):
12921295
class ClinicExternalTest(TestCase):
12931296
maxDiff = None
12941297

1298+
def _do_test(self, *args, expect_success=True):
1299+
clinic_py = os.path.join(test_tools.toolsdir, "clinic", "clinic.py")
1300+
with subprocess.Popen(
1301+
[sys.executable, "-Xutf8", clinic_py, *args],
1302+
encoding="utf-8",
1303+
bufsize=0,
1304+
stdout=subprocess.PIPE,
1305+
stderr=subprocess.PIPE,
1306+
) as proc:
1307+
proc.wait()
1308+
if expect_success == bool(proc.returncode):
1309+
self.fail("".join(proc.stderr))
1310+
stdout = proc.stdout.read()
1311+
stderr = proc.stderr.read()
1312+
# Clinic never writes to stderr.
1313+
self.assertEqual(stderr, "")
1314+
return stdout
1315+
1316+
def expect_success(self, *args):
1317+
return self._do_test(*args)
1318+
1319+
def expect_failure(self, *args):
1320+
return self._do_test(*args, expect_success=False)
1321+
12951322
def test_external(self):
12961323
CLINIC_TEST = 'clinic.test.c'
1297-
# bpo-42398: Test that the destination file is left unchanged if the
1298-
# content does not change. Moreover, check also that the file
1299-
# modification time does not change in this case.
13001324
source = support.findfile(CLINIC_TEST)
13011325
with open(source, 'r', encoding='utf-8') as f:
13021326
orig_contents = f.read()
13031327

1304-
with os_helper.temp_dir() as tmp_dir:
1305-
testfile = os.path.join(tmp_dir, CLINIC_TEST)
1306-
with open(testfile, 'w', encoding='utf-8') as f:
1307-
f.write(orig_contents)
1308-
old_mtime_ns = os.stat(testfile).st_mtime_ns
1309-
1310-
clinic.parse_file(testfile)
1328+
# Run clinic CLI and verify that it does not complain.
1329+
self.addCleanup(unlink, TESTFN)
1330+
out = self.expect_success("-f", "-o", TESTFN, source)
1331+
self.assertEqual(out, "")
13111332

1312-
with open(testfile, 'r', encoding='utf-8') as f:
1313-
new_contents = f.read()
1314-
new_mtime_ns = os.stat(testfile).st_mtime_ns
1333+
with open(TESTFN, 'r', encoding='utf-8') as f:
1334+
new_contents = f.read()
13151335

13161336
self.assertEqual(new_contents, orig_contents)
1337+
1338+
def test_no_change(self):
1339+
# bpo-42398: Test that the destination file is left unchanged if the
1340+
# content does not change. Moreover, check also that the file
1341+
# modification time does not change in this case.
1342+
code = dedent("""
1343+
/*[clinic input]
1344+
[clinic start generated code]*/
1345+
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=da39a3ee5e6b4b0d]*/
1346+
""")
1347+
with os_helper.temp_dir() as tmp_dir:
1348+
fn = os.path.join(tmp_dir, "test.c")
1349+
with open(fn, "w", encoding="utf-8") as f:
1350+
f.write(code)
1351+
pre_mtime = os.stat(fn).st_mtime_ns
1352+
self.expect_success(fn)
1353+
post_mtime = os.stat(fn).st_mtime_ns
13171354
# Don't change the file modification time
13181355
# if the content does not change
1319-
self.assertEqual(new_mtime_ns, old_mtime_ns)
1356+
self.assertEqual(pre_mtime, post_mtime)
1357+
1358+
def test_cli_force(self):
1359+
invalid_input = dedent("""
1360+
/*[clinic input]
1361+
output preset block
1362+
module test
1363+
test.fn
1364+
a: int
1365+
[clinic start generated code]*/
1366+
1367+
const char *hand_edited = "output block is overwritten";
1368+
/*[clinic end generated code: output=bogus input=bogus]*/
1369+
""")
1370+
fail_msg = dedent("""
1371+
Checksum mismatch!
1372+
Expected: bogus
1373+
Computed: 2ed19
1374+
Suggested fix: remove all generated code including the end marker,
1375+
or use the '-f' option.
1376+
""")
1377+
with os_helper.temp_dir() as tmp_dir:
1378+
fn = os.path.join(tmp_dir, "test.c")
1379+
with open(fn, "w", encoding="utf-8") as f:
1380+
f.write(invalid_input)
1381+
# First, run the CLI without -f and expect failure.
1382+
# Note, we cannot check the entire fail msg, because the path to
1383+
# the tmp file will change for every run.
1384+
out = self.expect_failure(fn)
1385+
self.assertTrue(out.endswith(fail_msg))
1386+
# Then, force regeneration; success expected.
1387+
out = self.expect_success("-f", fn)
1388+
self.assertEqual(out, "")
1389+
# Verify by checking the checksum.
1390+
checksum = (
1391+
"/*[clinic end generated code: "
1392+
"output=6c2289b73f32bc19 input=9543a8d2da235301]*/\n"
1393+
)
1394+
with open(fn, 'r', encoding='utf-8') as f:
1395+
generated = f.read()
1396+
self.assertTrue(generated.endswith(checksum))
1397+
1398+
def test_cli_verbose(self):
1399+
with os_helper.temp_dir() as tmp_dir:
1400+
fn = os.path.join(tmp_dir, "test.c")
1401+
with open(fn, "w", encoding="utf-8") as f:
1402+
f.write("")
1403+
out = self.expect_success("-v", fn)
1404+
self.assertEqual(out.strip(), fn)
1405+
1406+
def test_cli_help(self):
1407+
out = self.expect_success("-h")
1408+
self.assertIn("usage: clinic.py", out)
1409+
1410+
def test_cli_converters(self):
1411+
prelude = dedent("""
1412+
Legacy converters:
1413+
B C D L O S U Y Z Z#
1414+
b c d f h i l p s s# s* u u# w* y y# y* z z# z*
1415+
1416+
Converters:
1417+
""")
1418+
expected_converters = (
1419+
"bool",
1420+
"byte",
1421+
"char",
1422+
"defining_class",
1423+
"double",
1424+
"fildes",
1425+
"float",
1426+
"int",
1427+
"long",
1428+
"long_long",
1429+
"object",
1430+
"Py_buffer",
1431+
"Py_complex",
1432+
"Py_ssize_t",
1433+
"Py_UNICODE",
1434+
"PyByteArrayObject",
1435+
"PyBytesObject",
1436+
"self",
1437+
"short",
1438+
"size_t",
1439+
"slice_index",
1440+
"str",
1441+
"unicode",
1442+
"unsigned_char",
1443+
"unsigned_int",
1444+
"unsigned_long",
1445+
"unsigned_long_long",
1446+
"unsigned_short",
1447+
)
1448+
finale = dedent("""
1449+
Return converters:
1450+
bool()
1451+
double()
1452+
float()
1453+
init()
1454+
int()
1455+
long()
1456+
NoneType()
1457+
Py_ssize_t()
1458+
size_t()
1459+
unsigned_int()
1460+
unsigned_long()
1461+
1462+
All converters also accept (c_default=None, py_default=None, annotation=None).
1463+
All return converters also accept (py_default=None).
1464+
""")
1465+
out = self.expect_success("--converters")
1466+
# We cannot simply compare the output, because the repr of the *accept*
1467+
# param may change (it's a set, thus unordered). So, let's compare the
1468+
# start and end of the expected output, and then assert that the
1469+
# converters appear lined up in alphabetical order.
1470+
self.assertTrue(out.startswith(prelude), out)
1471+
self.assertTrue(out.endswith(finale), out)
1472+
1473+
out = out.removeprefix(prelude)
1474+
out = out.removesuffix(finale)
1475+
lines = out.split("\n")
1476+
for converter, line in zip(expected_converters, lines):
1477+
line = line.lstrip()
1478+
with self.subTest(converter=converter):
1479+
self.assertTrue(
1480+
line.startswith(converter),
1481+
f"expected converter {converter!r}, got {line!r}"
1482+
)
13201483

13211484

13221485
try:

0 commit comments

Comments
 (0)