|
4 | 4 |
|
5 | 5 | from test import support, test_tools
|
6 | 6 | from test.support import os_helper
|
| 7 | +from test.support import SHORT_TIMEOUT, requires_subprocess |
| 8 | +from test.support.os_helper import TESTFN, unlink |
7 | 9 | from textwrap import dedent
|
8 | 10 | from unittest import TestCase
|
9 | 11 | import collections
|
10 | 12 | import inspect
|
11 | 13 | import os.path
|
| 14 | +import subprocess |
12 | 15 | import sys
|
13 | 16 | import unittest
|
14 | 17 |
|
@@ -1346,31 +1349,190 @@ def test_scaffolding(self):
|
1346 | 1349 | class ClinicExternalTest(TestCase):
|
1347 | 1350 | maxDiff = None
|
1348 | 1351 |
|
| 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 | + |
1349 | 1376 | def test_external(self):
|
1350 | 1377 | 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. |
1354 | 1378 | source = support.findfile(CLINIC_TEST)
|
1355 | 1379 | with open(source, 'r', encoding='utf-8') as f:
|
1356 | 1380 | orig_contents = f.read()
|
1357 | 1381 |
|
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, "") |
1365 | 1386 |
|
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() |
1369 | 1389 |
|
1370 | 1390 | 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 |
1371 | 1408 | # Don't change the file modification time
|
1372 | 1409 | # 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 | + ) |
1374 | 1536 |
|
1375 | 1537 |
|
1376 | 1538 | try:
|
|
0 commit comments