From nobody Sun Feb 8 14:34:47 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-1.web.codeaurora.org [10.30.226.201]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id D9FB93ACF09; Tue, 3 Feb 2026 14:55:55 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=10.30.226.201 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1770130555; cv=none; b=R4qsRAQ5xy90G6nLk4EHpTpM5+NXMvjBP8RCSVF4xUnbiifMMWEXYJKqMFRm45W94tyv9+3d/TlSBIFjsRLn/+JI4xakkiO1RHKLAwqljsrUVcs5gWMqi7/FqBGFb8vLj0S5YVglfzQACT+NhKZqoMrjt1XQmE2SJ7b/LYYVU4g= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1770130555; c=relaxed/simple; bh=XjB7oEIvMEoEv4pw8CCs+7NJYgNKFnhj4FnMlBDzF5c=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=EApG77i05VOYHnUn7H/PO4sMT8z7GobBBlbMPzgwwuK0bj7TnJ8KnupEJIgMNoj2jmgAjqX9+xO77xLXwPYr/Gj0j/dEVSsWOo7/mSlAxgWoM1KejgWwM75vJflNr/MiKDyQbMdSiS+esaKLOIP8/LtsdzXauf/kqsY9+YzgaRc= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=RDQK+h51; arc=none smtp.client-ip=10.30.226.201 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="RDQK+h51" Received: by smtp.kernel.org (Postfix) with ESMTPSA id E4EAEC2BCB5; Tue, 3 Feb 2026 14:55:54 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1770130555; bh=XjB7oEIvMEoEv4pw8CCs+7NJYgNKFnhj4FnMlBDzF5c=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=RDQK+h51OR9ODBJJXaZQ4Oc0O5L07u0fvpNbPAgwL1iyHzNuB+CkPqtdgwGNyplGs Q750kc/KI0HnLB8hS5MF7mWBMy9Yby6q5bpQGIK/v1OTL8Ohs4/gwNwgRAZC7uQtW8 4xfqeBptznBRSNBRscaivalBqiMYnmmXLLuBg9kcjv3jz7buIbnGYtLD4w8VjZjG8j ATkghxo561fS9dIa01LRjctXKyIL/PTr+M99oCoWWSu2hTK+l45TyvSROP3X5bfnfW VZt/MyynHW6Mh01jydwKOWIeF+FC1fUc4Il23OOVn9mGNIF5uK2eyvuzJDIV+3nRiq O2FEHC0st2sKQ== Received: from mchehab by mail.kernel.org with local (Exim 4.99.1) (envelope-from ) id 1vnHon-000000027Uq-0SWE; Tue, 03 Feb 2026 15:55:53 +0100 From: Mauro Carvalho Chehab To: Jonathan Corbet , Linux Doc Mailing List Cc: Mauro Carvalho Chehab , linux-kernel@vger.kernel.org, Jani Nikula , Mauro Carvalho Chehab Subject: [PATCH 12/15] docs: test_kdoc_parser: add support for dynamic test creation Date: Tue, 3 Feb 2026 15:55:40 +0100 Message-ID: <5518d264bd3e273911d09ba9b07cca20b7cc4bc1.1770128540.git.mchehab+huawei@kernel.org> X-Mailer: git-send-email 2.52.0 In-Reply-To: References: Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Sender: Mauro Carvalho Chehab Use the content of kdoc-test.yaml to generate unittests to verify that kernel-doc internal methods are parsing C code and generating output the expected way. Depending on what is written at the parser file at kdoc-test.yaml, up to 5 tests can be generated from a single test entry inside the YAML file: 1. from source to kdoc_item: test KernelDoc class; 2. from kdoc_item to man: test ManOutput class; 3. from kdoc_item to rst: test RestOutput class; 4. from source to man without checking expected KdocItem; 5. from source to rst without checking expected KdocItem. Signed-off-by: Mauro Carvalho Chehab --- tools/unittests/test_kdoc_parser.py | 342 ++++++++++++++++++++++++++-- 1 file changed, 325 insertions(+), 17 deletions(-) diff --git a/tools/unittests/test_kdoc_parser.py b/tools/unittests/test_kdo= c_parser.py index f3ff750ac0e9..dd3d576e1b93 100755 --- a/tools/unittests/test_kdoc_parser.py +++ b/tools/unittests/test_kdoc_parser.py @@ -8,24 +8,40 @@ Unit tests for kernel-doc parser. """ =20 +import logging import os -import unittest import re import sys +import unittest =20 from textwrap import dedent from unittest.mock import patch, MagicMock, mock_open =20 +import yaml + SRC_DIR =3D os.path.dirname(os.path.realpath(__file__)) sys.path.insert(0, os.path.join(SRC_DIR, "../lib/python")) =20 -from kdoc.kdoc_parser import KernelDoc +from kdoc.kdoc_files import KdocConfig from kdoc.kdoc_item import KdocItem +from kdoc.kdoc_parser import KernelDoc +from kdoc.kdoc_output import RestFormat, ManFormat + from kdoc.xforms_lists import CTransforms + from unittest_helper import run_unittest =20 + +# +# Test file +# +TEST_FILE =3D os.path.join(SRC_DIR, "kdoc-test.yaml") + +# +# Ancillary logic to clean whitespaces +# #: Regex to help cleaning whitespaces -RE_WHITESPC =3D re.compile(r"\s++") +RE_WHITESPC =3D re.compile(r"([ \t]|\n)++") =20 def clean_whitespc(val, relax_whitespace=3DFalse): """ @@ -46,22 +62,59 @@ def clean_whitespc(val, relax_whitespace=3DFalse): return val =20 # -# Helper class to help mocking with +# Helper classes to help mocking with logger and config # -class KdocParser(unittest.TestCase): +class MockLogging(logging.Handler): + """ + Simple class to store everything on a list + """ + + def __init__(self, level=3Dlogging.NOTSET): + super().__init__(level) + self.messages =3D [] + self.formatter =3D logging.Formatter() + + def emit(self, record: logging.LogRecord) -> None: + """ + Append a formatted record to self.messages. + """ + try: + # The `format` method uses the handler's formatter. + message =3D self.format(record) + self.messages.append(message) + except Exception: + self.handleError(record) + +class MockKdocConfig(KdocConfig): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.log =3D logging.getLogger(__file__) + self.handler =3D MockLogging() + self.log.addHandler(self.handler) + + def warning(self, msg): + """Ancillary routine to output a warning and increment error count= .""" + + self.log.warning(msg) + +# +# Helper class to generate KdocItem and validate its contents +# +# TODO: check self.config.handler.messages content +# +class GenerateKdocItem(unittest.TestCase): """ Base class to run KernelDoc parser class """ =20 DEFAULT =3D vars(KdocItem("", "", "", 0)) =20 + config =3D MockKdocConfig() + xforms =3D CTransforms() + def setUp(self): self.maxDiff =3D None - self.config =3D MagicMock() - self.config.log =3D MagicMock() - self.config.log.debug =3D MagicMock() - self.xforms =3D CTransforms() - =20 def run_test(self, source, __expected_list, exports=3D{}, fname=3D"tes= t.c", relax_whitespace=3DFalse): @@ -75,6 +128,9 @@ class KdocParser(unittest.TestCase): # Ensure that default values will be there expected_list =3D [] for e in __expected_list: + if not isinstance(e, dict): + e =3D vars(e) + new_e =3D self.DEFAULT.copy() new_e["fname"] =3D fname for key, value in e.items(): @@ -111,13 +167,136 @@ class KdocParser(unittest.TestCase): =20 self.assertEqual(result, value, msg=3Df"at {key}") =20 +# +# Ancillary function that replicates kdoc_files way to generate output +# +def gen_output(fname, out_style, symbols, expected, + config=3DNone, relax_whitespace=3DFalse): + """ + Use the output class to return an output content from KdocItem symbols. + """ + + if not config: + config =3D MockKdocConfig() + + out_style.set_config(config) + + msg =3D out_style.output_symbols(fname, symbols) + + result =3D clean_whitespc(msg, relax_whitespace) + expected =3D clean_whitespc(expected, relax_whitespace) + + return result, expected + +# +# Classes to be used by dynamic test generation from YAML +# +class CToKdocItem(GenerateKdocItem): + def setUp(self): + self.maxDiff =3D None + + def run_parser_test(self, source, symbols, exports, fname): + if isinstance(symbols, dict): + symbols =3D [symbols] + + if isinstance(exports, str): + exports=3Dset([exports]) + elif isinstance(exports, list): + exports=3Dset(exports) + + self.run_test(source, symbols, exports=3Dexports, + fname=3Dfname, relax_whitespace=3DTrue) + +class KdocItemToMan(unittest.TestCase): + out_style =3D ManFormat() + + def setUp(self): + self.maxDiff =3D None + + def run_out_test(self, fname, symbols, expected): + """ + Generate output using out_style, + """ + result, expected =3D gen_output(fname, self.out_style, + symbols, expected) + + self.assertEqual(result, expected) + +class KdocItemToRest(unittest.TestCase): + out_style =3D RestFormat() + + def setUp(self): + self.maxDiff =3D None + + def run_out_test(self, fname, symbols, expected): + """ + Generate output using out_style, + """ + result, expected =3D gen_output(fname, self.out_style, symbols, + expected, relax_whitespace=3DTrue) + + self.assertEqual(result, expected) + + +class CToMan(unittest.TestCase): + out_style =3D ManFormat() + config =3D MockKdocConfig() + xforms =3D CTransforms() + + def setUp(self): + self.maxDiff =3D None + + def run_out_test(self, fname, source, expected): + """ + Generate output using out_style, + """ + patcher =3D patch('builtins.open', + new_callable=3Dmock_open, read_data=3Dsource) + + kernel_doc =3D KernelDoc(self.config, fname, self.xforms) + + with patcher: + export_table, entries =3D kernel_doc.parse_kdoc() + + result, expected =3D gen_output(fname, self.out_style, + entries, expected, config=3Dself.con= fig) + + self.assertEqual(result, expected) + + +class CToRest(unittest.TestCase): + out_style =3D RestFormat() + config =3D MockKdocConfig() + xforms =3D CTransforms() + + def setUp(self): + self.maxDiff =3D None + + def run_out_test(self, fname, source, expected): + """ + Generate output using out_style, + """ + patcher =3D patch('builtins.open', + new_callable=3Dmock_open, read_data=3Dsource) + + kernel_doc =3D KernelDoc(self.config, fname, self.xforms) + + with patcher: + export_table, entries =3D kernel_doc.parse_kdoc() + + result, expected =3D gen_output(fname, self.out_style, entries, + expected, relax_whitespace=3DTrue, + config=3Dself.config) + + self.assertEqual(result, expected) + =20 # -# Selttest class +# Selftest class # -class TestSelfValidate(KdocParser): +class TestSelfValidate(GenerateKdocItem): """ - Tests to check if logic inside KdocParser.run_test() is working. + Tests to check if logic inside GenerateKdocItem.run_test() is working. """ =20 SOURCE =3D """ @@ -143,16 +322,23 @@ class TestSelfValidate(KdocParser): 'Description': 'Does nothing\n\n', 'Return': '\nalways return 0.\n' }, + + 'sections_start_lines': { + 'Description': 4, + 'Return': 7, + }, + + 'parameterdescs': {'arg1': '@arg1 does nothing\n'}, + 'parameterlist': ['arg1'], + 'parameterdesc_start_lines': {'arg1': 3}, + 'parametertypes': {'arg1': 'char *arg1'}, + 'other_stuff': { 'func_macro': False, 'functiontype': 'int', 'purpose': 'Exported function', 'typedef': False }, - 'parameterdescs': {'arg1': '@arg1 does nothing\n'}, - 'parameterlist': ['arg1'], - 'parameterdesc_start_lines': {'arg1': 3}, - 'parametertypes': {'arg1': 'char *arg1'}, }] =20 EXPORTS =3D {"function3"} @@ -191,6 +377,128 @@ class TestSelfValidate(KdocParser): """ self.run_test(self.SOURCE, [self.DEFAULT.copy()], self.EXPORTS) =20 +# +# Class and logic to create dynamic tests from YAML +# + +class KernelDocDynamicTests(): + """ + Dynamically create a set of tests from a YAML file. + """ + + @classmethod + def create_parser_test(cls, name, fname, source, symbols, exports): + """ + Return a function that will be attached to the test class. + """ + def test_method(self): + """Lambda-like function to run tests with provided vars""" + self.run_parser_test(source, symbols, exports, fname) + + test_method.__name__ =3D f"test_gen_{name}" + + setattr(CToKdocItem, test_method.__name__, test_method) + + @classmethod + def create_out_test(cls, name, fname, symbols, out_type, data): + """ + Return a function that will be attached to the test class. + """ + def test_method(self): + """Lambda-like function to run tests with provided vars""" + self.run_out_test(fname, symbols, data) + + test_method.__name__ =3D f"test_{out_type}_{name}" + + if out_type =3D=3D "man": + setattr(KdocItemToMan, test_method.__name__, test_method) + else: + setattr(KdocItemToRest, test_method.__name__, test_method) + + @classmethod + def create_src2out_test(cls, name, fname, source, out_type, data): + """ + Return a function that will be attached to the test class. + """ + def test_method(self): + """Lambda-like function to run tests with provided vars""" + self.run_out_test(fname, source, data) + + test_method.__name__ =3D f"test_{out_type}_{name}" + + if out_type =3D=3D "man": + setattr(CToMan, test_method.__name__, test_method) + else: + setattr(CToRest, test_method.__name__, test_method) + + @classmethod + def create_tests(cls): + """ + Iterate over all scenarios and add a method to the class for each. + + The logic in this function assumes a valid test that are compliant + with kdoc-test-schema.yaml. There is an unit test to check that. + As such, it picks mandatory values directly, and uses get() for the + optional ones. + """ + + with open(TEST_FILE, encoding=3D"utf-8") as fp: + testset =3D yaml.safe_load(fp) + + tests =3D testset["tests"] + + for idx, test in enumerate(tests): + name =3D test["name"] + fname =3D test["fname"] + source =3D test["source"] + expected_list =3D test["expected"] + + exports =3D test.get("exports", []) + + # + # The logic below allows setting up to 5 types of test: + # 1. from source to kdoc_item: test KernelDoc class; + # 2. from kdoc_item to man: test ManOutput class; + # 3. from kdoc_item to rst: test RestOutput class; + # 4. from source to man without checking expected KdocItem; + # 5. from source to rst without checking expected KdocItem. + # + for expected in expected_list: + kdoc_item =3D expected.get("kdoc_item") + man =3D expected.get("man", []) + rst =3D expected.get("rst", []) + + if kdoc_item: + if isinstance(kdoc_item, dict): + kdoc_item =3D [kdoc_item] + + symbols =3D [] + + for arg in kdoc_item: + arg["fname"] =3D fname + arg["start_line"] =3D 1 + + symbols.append(KdocItem.from_dict(arg)) + + if source: + cls.create_parser_test(name, fname, source, + symbols, exports) + + if man: + cls.create_out_test(name, fname, symbols, "man", m= an) + + if rst: + cls.create_out_test(name, fname, symbols, "rst", r= st) + + elif source: + if man: + cls.create_src2out_test(name, fname, source, "man"= , man) + + if rst: + cls.create_src2out_test(name, fname, source, "rst"= , rst) + +KernelDocDynamicTests.create_tests() + # # Run all tests # --=20 2.52.0