Skip to content

Commit

Permalink
[3.11] pythongh-106368: Increase Argument Clinic CLI test coverage (p…
Browse files Browse the repository at this point in the history
…ythonGH-107156) (python#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 <erlend@python.org>
Co-authored-by: Nikita Sobolev <mail@sobolevn.me>
  • Loading branch information
3 people authored Jul 26, 2023
1 parent 55edee2 commit cebe60b
Showing 1 changed file with 177 additions and 14 deletions.
191 changes: 177 additions & 14 deletions Lib/test/test_clinic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -1292,31 +1295,191 @@ 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=6c2289b73f32bc19 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()
NoneType()
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:
Expand Down

0 comments on commit cebe60b

Please sign in to comment.