mirror of https://github.com/torvalds/linux.git
561 lines
16 KiB
Python
Executable File
561 lines
16 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-2.0
|
|
# Copyright(c) 2026: Mauro Carvalho Chehab <mchehab@kernel.org>.
|
|
#
|
|
# pylint: disable=C0200,C0413,W0102,R0914
|
|
|
|
"""
|
|
Unit tests for kernel-doc parser.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
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_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 TestUnits
|
|
|
|
|
|
#
|
|
# Test file
|
|
#
|
|
TEST_FILE = os.path.join(SRC_DIR, "kdoc-test.yaml")
|
|
|
|
env = {
|
|
"yaml_file": TEST_FILE
|
|
}
|
|
|
|
#
|
|
# Ancillary logic to clean whitespaces
|
|
#
|
|
#: Regex to help cleaning whitespaces
|
|
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):
|
|
"""
|
|
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 = val.strip()
|
|
if relax_whitespace:
|
|
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):
|
|
val = {k: clean_whitespc(v, relax_whitespace) for k, v in val.items()}
|
|
return val
|
|
|
|
#
|
|
# Helper classes to help mocking with logger and config
|
|
#
|
|
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
|
|
|
|
def run_test(self, source, __expected_list, exports={}, fname="test.c",
|
|
relax_whitespace=False):
|
|
"""
|
|
Stores expected values and patch the test to use source as
|
|
a "file" input.
|
|
"""
|
|
debug_level = int(os.getenv("VERBOSE", "0"))
|
|
source = dedent(source)
|
|
|
|
# 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():
|
|
new_e[key] = value
|
|
|
|
expected_list.append(new_e)
|
|
|
|
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()
|
|
|
|
self.assertEqual(export_table, exports)
|
|
self.assertEqual(len(entries), len(expected_list))
|
|
|
|
for i in range(0, len(entries)):
|
|
|
|
entry = entries[i]
|
|
expected = expected_list[i]
|
|
self.assertNotEqual(expected, None)
|
|
self.assertNotEqual(expected, {})
|
|
self.assertIsInstance(entry, KdocItem)
|
|
|
|
d = vars(entry)
|
|
|
|
other_stuff = d.get("other_stuff", {})
|
|
if "source" in other_stuff:
|
|
del other_stuff["source"]
|
|
|
|
for key, value in expected.items():
|
|
if key == "other_stuff":
|
|
if "source" in value:
|
|
del value["source"]
|
|
|
|
result = clean_whitespc(d[key], relax_whitespace)
|
|
value = clean_whitespc(value, relax_whitespace)
|
|
|
|
if debug_level > 1:
|
|
sys.stderr.write(f"{key}: assert('{result}' == '{value}')\n")
|
|
|
|
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
|
|
|
|
#
|
|
# Classes to be used by dynamic test generation from YAML
|
|
#
|
|
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 GenerateKdocItem.run_test() is working.
|
|
"""
|
|
|
|
SOURCE = """
|
|
/**
|
|
* function3: Exported function
|
|
* @arg1: @arg1 does nothing
|
|
*
|
|
* Does nothing
|
|
*
|
|
* return:
|
|
* always return 0.
|
|
*/
|
|
int function3(char *arg1) { return 0; };
|
|
EXPORT_SYMBOL(function3);
|
|
"""
|
|
|
|
EXPECTED = [{
|
|
'name': 'function3',
|
|
'type': 'function',
|
|
'declaration_start_line': 2,
|
|
|
|
'sections_start_lines': {
|
|
'Description': 4,
|
|
'Return': 7,
|
|
},
|
|
'sections': {
|
|
'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
|
|
},
|
|
}]
|
|
|
|
EXPORTS = {"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)
|
|
|
|
#
|
|
# 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.
|
|
"""
|
|
|
|
test_file = os.environ.get("yaml_file", TEST_FILE)
|
|
|
|
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
|
|
#
|
|
if __name__ == "__main__":
|
|
runner = TestUnits()
|
|
parser = runner.parse_args()
|
|
parser.add_argument("-y", "--yaml-file", "--yaml",
|
|
help='Name of the yaml file to load')
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.yaml_file:
|
|
env["yaml_file"] = os.path.expanduser(args.yaml_file)
|
|
|
|
# Run tests with customized arguments
|
|
runner.run(__file__, parser=parser, args=args, env=env)
|