From nobody Mon Feb 9 01:22:35 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 9B0F23ACEFA; 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=fdKqQjMiKq3+phNmijydZq8xcwIG5xdSl2AAq7wqdNe5PlaU5OqYumsvx7o2nT6wQ1JWPXHnxUu7p+FhjedXl5Yi5P8bQWw6tB7wapmSOBX9LZFxYWf8EbR6y5YH/4C7Aj+Al4I3L4USnWWTY1N43MMKWuwTmzCVl0/+kUuxHpQ= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1770130555; c=relaxed/simple; bh=NG3dg7pu+frczz+Zr/hP/7FD6w4QMq2rfMP2u6GHZtM=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=UKOY+sLlMDFL/EaD4oEG73I+M+Ms+McPQ26ZWFQXgXJW+UBuu6LrN15OVAeWUeMIc0fKjsECZiuqdWvo4rSZ2/1wFBgNLAnq+RyATKF4NKHrANVcfUnwECOSS6PqumRStZgRgvwbmlg4Er8mknAo2ihXtBv1WVTi6/MyNQV0M0c= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=ZxIKqZOW; 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="ZxIKqZOW" Received: by smtp.kernel.org (Postfix) with ESMTPSA id DC6ECC2BC87; 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=1770130554; bh=NG3dg7pu+frczz+Zr/hP/7FD6w4QMq2rfMP2u6GHZtM=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=ZxIKqZOW1G/9R7qtIn+1wUtTOSdXgPWo/du678TCWD7usQXpG5/VQmaywbEeDB3hB GDeb5PPwco9ZYcUV0DsLApsVqiSpPMYuMLK/u+shHf7oAr/n0pSx1S7988u912W7Go 700G9XRZdQfNLcZrJeGGYOeQr6y/rOBiBC/8Cb/oF8T1VLgpFPSm2qOWMgyUmtgOC7 ddvnnX5/QkQBYcqHxD0bAkCdfFHwnkYeiTDp91cfAtl52dxamyAHglE9WYdTrEftk2 +j54mwwMEdV+hcZeHj/nbhMaPpwbi+XX6E7/WTnpWY0oIXmDQ9GEqysjbmgB8V/oHI w0oATmsxvpazw== Received: from mchehab by mail.kernel.org with local (Exim 4.99.1) (envelope-from ) id 1vnHon-000000027Ue-07jM; 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 09/15] docs: unittests: add a parser to test kernel-doc parser logic Date: Tue, 3 Feb 2026 15:55:37 +0100 Message-ID: <89897c6344749de5cbf2b59a2a1ae24ff58154c1.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 Validating that kernel-doc is parsing data properly is tricky. Add an unittest skeleton that alllows passing a source code and check if the corresponding values of export_table and entries returned by the parser are properly filled. It works by mocking a file input with the contents of a source string, an comparing if: - exports set matches; - expected KernelItem entries match. Create a new TestSelfValidate meant to check if the logic inside KdocParser.run_test() does its job of checking for differences inside KdocItem. Signed-off-by: Mauro Carvalho Chehab --- tools/unittests/test_kdoc_parser.py | 198 ++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100755 tools/unittests/test_kdoc_parser.py diff --git a/tools/unittests/test_kdoc_parser.py b/tools/unittests/test_kdo= c_parser.py new file mode 100755 index 000000000000..f3ff750ac0e9 --- /dev/null +++ b/tools/unittests/test_kdoc_parser.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +# Copyright(c) 2026: Mauro Carvalho Chehab . +# +# pylint: disable=3DC0200,C0413,W0102,R0914 + +""" +Unit tests for kernel-doc parser. +""" + +import os +import unittest +import re +import sys + +from textwrap import dedent +from unittest.mock import patch, MagicMock, mock_open + +SRC_DIR =3D os.path.dirname(os.path.realpath(__file__)) +sys.path.insert(0, os.path.join(SRC_DIR, "../lib/python")) + +from kdoc.kdoc_parser import KernelDoc +from kdoc.kdoc_item import KdocItem +from kdoc.xforms_lists import CTransforms +from unittest_helper import run_unittest + +#: Regex to help cleaning whitespaces +RE_WHITESPC =3D re.compile(r"\s++") + +def clean_whitespc(val, relax_whitespace=3DFalse): + """ + Cleanup whitespaces to avoid false positives. + + By default, strip only bein/end whitespaces, but, when relax_whitespace + is true, also replace multiple whitespaces in the middle. + """ + + if isinstance(val, str): + val =3D val.strip() + if relax_whitespace: + val =3D RE_WHITESPC.sub("", val) + elif isinstance(val, list): + val =3D [clean_whitespc(item, relax_whitespace) for item in val] + elif isinstance(val, dict): + val =3D {k: clean_whitespc(v, relax_whitespace) for k, v in val.it= ems()} + return val + +# +# Helper class to help mocking with +# +class KdocParser(unittest.TestCase): + """ + Base class to run KernelDoc parser class + """ + + DEFAULT =3D vars(KdocItem("", "", "", 0)) + + 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() + + + def run_test(self, source, __expected_list, exports=3D{}, fname=3D"tes= t.c", + relax_whitespace=3DFalse): + """ + Stores expected values and patch the test to use source as + a "file" input. + """ + debug_level =3D int(os.getenv("VERBOSE", "0")) + source =3D dedent(source) + + # Ensure that default values will be there + expected_list =3D [] + for e in __expected_list: + new_e =3D self.DEFAULT.copy() + new_e["fname"] =3D fname + for key, value in e.items(): + new_e[key] =3D value + + expected_list.append(new_e) + + 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() + + self.assertEqual(export_table, exports) + self.assertEqual(len(entries), len(expected_list)) + + for i in range(0, len(entries)): + + entry =3D entries[i] + expected =3D expected_list[i] + self.assertNotEqual(expected, None) + self.assertNotEqual(expected, {}) + self.assertIsInstance(entry, KdocItem) + + d =3D vars(entry) + for key, value in expected.items(): + result =3D clean_whitespc(d[key], relax_whitespace) + value =3D clean_whitespc(value, relax_whitespace) + + if debug_level > 1: + sys.stderr.write(f"{key}: assert('{result}' =3D=3D= '{value}')\n") + + self.assertEqual(result, value, msg=3Df"at {key}") + + +# +# Selttest class +# +class TestSelfValidate(KdocParser): + """ + Tests to check if logic inside KdocParser.run_test() is working. + """ + + SOURCE =3D """ + /** + * function3: Exported function + * @arg1: @arg1 does nothing + * + * Does nothing + * + * return: + * always return 0. + */ + int function3(char *arg1) { return 0; }; + EXPORT_SYMBOL(function3); + """ + + EXPECTED =3D [{ + 'name': 'function3', + 'type': 'function', + 'declaration_start_line': 2, + + 'sections': { + 'Description': 'Does nothing\n\n', + 'Return': '\nalways return 0.\n' + }, + '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'}, + }] + + EXPORTS =3D {"function3"} + + def test_parse_pass(self): + """ + Test if export_symbol is properly handled. + """ + self.run_test(self.SOURCE, self.EXPECTED, self.EXPORTS) + + @unittest.expectedFailure + def test_no_exports(self): + """ + Test if export_symbol is properly handled. + """ + self.run_test(self.SOURCE, [], {}) + + @unittest.expectedFailure + def test_with_empty_expected(self): + """ + Test if export_symbol is properly handled. + """ + self.run_test(self.SOURCE, [], self.EXPORTS) + + @unittest.expectedFailure + def test_with_unfilled_expected(self): + """ + Test if export_symbol is properly handled. + """ + self.run_test(self.SOURCE, [{}], self.EXPORTS) + + @unittest.expectedFailure + def test_with_default_expected(self): + """ + Test if export_symbol is properly handled. + """ + self.run_test(self.SOURCE, [self.DEFAULT.copy()], self.EXPORTS) + +# +# Run all tests +# +if __name__ =3D=3D "__main__": + run_unittest(__file__) --=20 2.52.0