From 83a2837b328c58b243f7d97bec12c64ec66681c5 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Mon, 24 Jul 2023 20:49:16 +0200 Subject: [PATCH] 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 --- Lib/test/test_clinic.py | 190 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 176 insertions(+), 14 deletions(-) diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index c24b8cd3899f28..f5271203362263 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -4,11 +4,14 @@ from test import support, test_tools from test.support import os_helper +from test.support import SHORT_TIMEOUT, requires_subprocess +from test.support.os_helper import TESTFN, unlink from textwrap import dedent from unittest import TestCase import collections import inspect import os.path +import subprocess import sys import unittest @@ -1346,31 +1349,190 @@ def test_scaffolding(self): class ClinicExternalTest(TestCase): maxDiff = None + def _do_test(self, *args, expect_success=True): + clinic_py = os.path.join(test_tools.toolsdir, "clinic", "clinic.py") + with subprocess.Popen( + [sys.executable, "-Xutf8", clinic_py, *args], + encoding="utf-8", + bufsize=0, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) as proc: + proc.wait() + if expect_success == bool(proc.returncode): + self.fail("".join(proc.stderr)) + stdout = proc.stdout.read() + stderr = proc.stderr.read() + # Clinic never writes to stderr. + self.assertEqual(stderr, "") + return stdout + + def expect_success(self, *args): + return self._do_test(*args) + + def expect_failure(self, *args): + return self._do_test(*args, expect_success=False) + def test_external(self): CLINIC_TEST = 'clinic.test.c' - # bpo-42398: Test that the destination file is left unchanged if the - # content does not change. Moreover, check also that the file - # modification time does not change in this case. source = support.findfile(CLINIC_TEST) with open(source, 'r', encoding='utf-8') as f: orig_contents = f.read() - with os_helper.temp_dir() as tmp_dir: - testfile = os.path.join(tmp_dir, CLINIC_TEST) - with open(testfile, 'w', encoding='utf-8') as f: - f.write(orig_contents) - old_mtime_ns = os.stat(testfile).st_mtime_ns - - clinic.parse_file(testfile) + # Run clinic CLI and verify that it does not complain. + self.addCleanup(unlink, TESTFN) + out = self.expect_success("-f", "-o", TESTFN, source) + self.assertEqual(out, "") - with open(testfile, 'r', encoding='utf-8') as f: - new_contents = f.read() - new_mtime_ns = os.stat(testfile).st_mtime_ns + with open(TESTFN, 'r', encoding='utf-8') as f: + new_contents = f.read() self.assertEqual(new_contents, orig_contents) + + def test_no_change(self): + # bpo-42398: Test that the destination file is left unchanged if the + # content does not change. Moreover, check also that the file + # modification time does not change in this case. + code = dedent(""" + /*[clinic input] + [clinic start generated code]*/ + /*[clinic end generated code: output=da39a3ee5e6b4b0d input=da39a3ee5e6b4b0d]*/ + """) + with os_helper.temp_dir() as tmp_dir: + fn = os.path.join(tmp_dir, "test.c") + with open(fn, "w", encoding="utf-8") as f: + f.write(code) + pre_mtime = os.stat(fn).st_mtime_ns + self.expect_success(fn) + post_mtime = os.stat(fn).st_mtime_ns # Don't change the file modification time # if the content does not change - self.assertEqual(new_mtime_ns, old_mtime_ns) + self.assertEqual(pre_mtime, post_mtime) + + def test_cli_force(self): + invalid_input = dedent(""" + /*[clinic input] + output preset block + module test + test.fn + a: int + [clinic start generated code]*/ + + const char *hand_edited = "output block is overwritten"; + /*[clinic end generated code: output=bogus input=bogus]*/ + """) + fail_msg = dedent(""" + Checksum mismatch! + Expected: bogus + Computed: 2ed19 + Suggested fix: remove all generated code including the end marker, + or use the '-f' option. + """) + with os_helper.temp_dir() as tmp_dir: + fn = os.path.join(tmp_dir, "test.c") + with open(fn, "w", encoding="utf-8") as f: + f.write(invalid_input) + # First, run the CLI without -f and expect failure. + # Note, we cannot check the entire fail msg, because the path to + # the tmp file will change for every run. + out = self.expect_failure(fn) + self.assertTrue(out.endswith(fail_msg)) + # Then, force regeneration; success expected. + out = self.expect_success("-f", fn) + self.assertEqual(out, "") + # Verify by checking the checksum. + checksum = ( + "/*[clinic end generated code: " + "output=2124c291eb067d76 input=9543a8d2da235301]*/\n" + ) + with open(fn, 'r', encoding='utf-8') as f: + generated = f.read() + self.assertTrue(generated.endswith(checksum)) + + def test_cli_verbose(self): + with os_helper.temp_dir() as tmp_dir: + fn = os.path.join(tmp_dir, "test.c") + with open(fn, "w", encoding="utf-8") as f: + f.write("") + out = self.expect_success("-v", fn) + self.assertEqual(out.strip(), fn) + + def test_cli_help(self): + out = self.expect_success("-h") + self.assertIn("usage: clinic.py", out) + + def test_cli_converters(self): + prelude = dedent(""" + Legacy converters: + B C D L O S U Y Z Z# + b c d f h i l p s s# s* u u# w* y y# y* z z# z* + + Converters: + """) + expected_converters = ( + "bool", + "byte", + "char", + "defining_class", + "double", + "fildes", + "float", + "int", + "long", + "long_long", + "object", + "Py_buffer", + "Py_complex", + "Py_ssize_t", + "Py_UNICODE", + "PyByteArrayObject", + "PyBytesObject", + "self", + "short", + "size_t", + "slice_index", + "str", + "unicode", + "unsigned_char", + "unsigned_int", + "unsigned_long", + "unsigned_long_long", + "unsigned_short", + ) + finale = dedent(""" + Return converters: + bool() + double() + float() + init() + int() + long() + Py_ssize_t() + size_t() + unsigned_int() + unsigned_long() + + All converters also accept (c_default=None, py_default=None, annotation=None). + All return converters also accept (py_default=None). + """) + out = self.expect_success("--converters") + # We cannot simply compare the output, because the repr of the *accept* + # param may change (it's a set, thus unordered). So, let's compare the + # start and end of the expected output, and then assert that the + # converters appear lined up in alphabetical order. + self.assertTrue(out.startswith(prelude), out) + self.assertTrue(out.endswith(finale), out) + + out = out.removeprefix(prelude) + out = out.removesuffix(finale) + lines = out.split("\n") + for converter, line in zip(expected_converters, lines): + line = line.lstrip() + with self.subTest(converter=converter): + self.assertTrue( + line.startswith(converter), + f"expected converter {converter!r}, got {line!r}" + ) try: