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 <mail@sobolevn.me>
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

Lines changed: 176 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

@@ -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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy