mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-05-16 09:02:21 -04:00
docs: test_kdoc_parser: add support for dynamic test creation
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 <mchehab+huawei@kernel.org> Signed-off-by: Jonathan Corbet <corbet@lwn.net> Message-ID: <7ec2593c5b19de3e3b1d8de92675f6b751d3fa21.1773823995.git.mchehab+huawei@kernel.org>
This commit is contained in:
committed by
Jonathan Corbet
parent
8b69f5225a
commit
eea0d807db
@@ -8,24 +8,43 @@
|
||||
Unit tests for kernel-doc parser.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import unittest
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from textwrap import dedent
|
||||
from unittest.mock import patch, MagicMock, mock_open
|
||||
|
||||
import yaml
|
||||
|
||||
SRC_DIR = 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_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
|
||||
|
||||
|
||||
#
|
||||
# Test file
|
||||
#
|
||||
TEST_FILE = os.path.join(SRC_DIR, "kdoc-test.yaml")
|
||||
|
||||
#
|
||||
# Ancillary logic to clean whitespaces
|
||||
#
|
||||
#: Regex to help cleaning whitespaces
|
||||
RE_WHITESPC = re.compile(r"\s++")
|
||||
RE_WHITESPC = re.compile(r"[ \t]++")
|
||||
RE_BEGINSPC = re.compile(r"^\s+", re.MULTILINE)
|
||||
RE_ENDSPC = re.compile(r"\s+$", re.MULTILINE)
|
||||
|
||||
def clean_whitespc(val, relax_whitespace=False):
|
||||
"""
|
||||
@@ -38,7 +57,9 @@ def clean_whitespc(val, relax_whitespace=False):
|
||||
if isinstance(val, str):
|
||||
val = val.strip()
|
||||
if relax_whitespace:
|
||||
val = RE_WHITESPC.sub("", val)
|
||||
val = RE_WHITESPC.sub(" ", val)
|
||||
val = RE_BEGINSPC.sub("", val)
|
||||
val = RE_ENDSPC.sub("", val)
|
||||
elif isinstance(val, list):
|
||||
val = [clean_whitespc(item, relax_whitespace) for item in val]
|
||||
elif isinstance(val, dict):
|
||||
@@ -46,22 +67,59 @@ def clean_whitespc(val, relax_whitespace=False):
|
||||
return val
|
||||
|
||||
#
|
||||
# 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=logging.NOTSET):
|
||||
super().__init__(level)
|
||||
self.messages = []
|
||||
self.formatter = 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 = 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 = logging.getLogger(__file__)
|
||||
self.handler = 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
|
||||
"""
|
||||
|
||||
DEFAULT = vars(KdocItem("", "", "", 0))
|
||||
|
||||
config = MockKdocConfig()
|
||||
xforms = CTransforms()
|
||||
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
self.config = MagicMock()
|
||||
self.config.log = MagicMock()
|
||||
self.config.log.debug = MagicMock()
|
||||
self.xforms = CTransforms()
|
||||
|
||||
|
||||
def run_test(self, source, __expected_list, exports={}, fname="test.c",
|
||||
relax_whitespace=False):
|
||||
@@ -75,6 +133,9 @@ class KdocParser(unittest.TestCase):
|
||||
# Ensure that default values will be there
|
||||
expected_list = []
|
||||
for e in __expected_list:
|
||||
if not isinstance(e, dict):
|
||||
e = vars(e)
|
||||
|
||||
new_e = self.DEFAULT.copy()
|
||||
new_e["fname"] = fname
|
||||
for key, value in e.items():
|
||||
@@ -111,13 +172,155 @@ class KdocParser(unittest.TestCase):
|
||||
|
||||
self.assertEqual(result, value, msg=f"at {key}")
|
||||
|
||||
#
|
||||
# Ancillary function that replicates kdoc_files way to generate output
|
||||
#
|
||||
def cleanup_timestamp(text):
|
||||
lines = text.split("\n")
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if not line.startswith('.TH'):
|
||||
continue
|
||||
|
||||
parts = shlex.split(line)
|
||||
if len(parts) > 3:
|
||||
parts[3] = ""
|
||||
|
||||
lines[i] = " ".join(parts)
|
||||
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def gen_output(fname, out_style, symbols, expected,
|
||||
config=None, relax_whitespace=False):
|
||||
"""
|
||||
Use the output class to return an output content from KdocItem symbols.
|
||||
"""
|
||||
|
||||
if not config:
|
||||
config = MockKdocConfig()
|
||||
|
||||
out_style.set_config(config)
|
||||
|
||||
msg = out_style.output_symbols(fname, symbols)
|
||||
|
||||
result = clean_whitespc(msg, relax_whitespace)
|
||||
result = cleanup_timestamp(result)
|
||||
|
||||
expected = clean_whitespc(expected, relax_whitespace)
|
||||
expected = cleanup_timestamp(expected)
|
||||
|
||||
return result, expected
|
||||
|
||||
#
|
||||
# Selttest class
|
||||
# Classes to be used by dynamic test generation from YAML
|
||||
#
|
||||
class TestSelfValidate(KdocParser):
|
||||
class CToKdocItem(GenerateKdocItem):
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
|
||||
def run_parser_test(self, source, symbols, exports, fname):
|
||||
if isinstance(symbols, dict):
|
||||
symbols = [symbols]
|
||||
|
||||
if isinstance(exports, str):
|
||||
exports=set([exports])
|
||||
elif isinstance(exports, list):
|
||||
exports=set(exports)
|
||||
|
||||
self.run_test(source, symbols, exports=exports,
|
||||
fname=fname, relax_whitespace=True)
|
||||
|
||||
class KdocItemToMan(unittest.TestCase):
|
||||
out_style = ManFormat()
|
||||
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
|
||||
def run_out_test(self, fname, symbols, expected):
|
||||
"""
|
||||
Generate output using out_style,
|
||||
"""
|
||||
result, expected = gen_output(fname, self.out_style,
|
||||
symbols, expected)
|
||||
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
class KdocItemToRest(unittest.TestCase):
|
||||
out_style = RestFormat()
|
||||
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
|
||||
def run_out_test(self, fname, symbols, expected):
|
||||
"""
|
||||
Generate output using out_style,
|
||||
"""
|
||||
result, expected = gen_output(fname, self.out_style, symbols,
|
||||
expected, relax_whitespace=True)
|
||||
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
|
||||
class CToMan(unittest.TestCase):
|
||||
out_style = ManFormat()
|
||||
config = MockKdocConfig()
|
||||
xforms = CTransforms()
|
||||
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
|
||||
def run_out_test(self, fname, source, expected):
|
||||
"""
|
||||
Generate output using out_style,
|
||||
"""
|
||||
patcher = patch('builtins.open',
|
||||
new_callable=mock_open, read_data=source)
|
||||
|
||||
kernel_doc = KernelDoc(self.config, fname, self.xforms)
|
||||
|
||||
with patcher:
|
||||
export_table, entries = kernel_doc.parse_kdoc()
|
||||
|
||||
result, expected = gen_output(fname, self.out_style,
|
||||
entries, expected, config=self.config)
|
||||
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
|
||||
class CToRest(unittest.TestCase):
|
||||
out_style = RestFormat()
|
||||
config = MockKdocConfig()
|
||||
xforms = CTransforms()
|
||||
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
|
||||
def run_out_test(self, fname, source, expected):
|
||||
"""
|
||||
Generate output using out_style,
|
||||
"""
|
||||
patcher = patch('builtins.open',
|
||||
new_callable=mock_open, read_data=source)
|
||||
|
||||
kernel_doc = KernelDoc(self.config, fname, self.xforms)
|
||||
|
||||
with patcher:
|
||||
export_table, entries = kernel_doc.parse_kdoc()
|
||||
|
||||
result, expected = gen_output(fname, self.out_style, entries,
|
||||
expected, relax_whitespace=True,
|
||||
config=self.config)
|
||||
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
|
||||
#
|
||||
# Selftest class
|
||||
#
|
||||
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.
|
||||
"""
|
||||
|
||||
SOURCE = """
|
||||
@@ -147,16 +350,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'},
|
||||
}]
|
||||
|
||||
EXPORTS = {"function3"}
|
||||
@@ -195,6 +405,128 @@ class TestSelfValidate(KdocParser):
|
||||
"""
|
||||
self.run_test(self.SOURCE, [self.DEFAULT.copy()], self.EXPORTS)
|
||||
|
||||
#
|
||||
# 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__ = 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__ = f"test_{out_type}_{name}"
|
||||
|
||||
if out_type == "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__ = f"test_{out_type}_{name}"
|
||||
|
||||
if out_type == "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="utf-8") as fp:
|
||||
testset = yaml.safe_load(fp)
|
||||
|
||||
tests = testset["tests"]
|
||||
|
||||
for idx, test in enumerate(tests):
|
||||
name = test["name"]
|
||||
fname = test["fname"]
|
||||
source = test["source"]
|
||||
expected_list = test["expected"]
|
||||
|
||||
exports = 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 = expected.get("kdoc_item")
|
||||
man = expected.get("man", [])
|
||||
rst = expected.get("rst", [])
|
||||
|
||||
if kdoc_item:
|
||||
if isinstance(kdoc_item, dict):
|
||||
kdoc_item = [kdoc_item]
|
||||
|
||||
symbols = []
|
||||
|
||||
for arg in kdoc_item:
|
||||
arg["fname"] = fname
|
||||
arg["start_line"] = 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", man)
|
||||
|
||||
if rst:
|
||||
cls.create_out_test(name, fname, symbols, "rst", rst)
|
||||
|
||||
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
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user