mirror of
https://github.com/torvalds/linux.git
synced 2026-07-29 10:41:49 +02:00
Merge branch 'mauro' into docs-mw
Mauro says:
This patch series change how kdoc parser handles macro replacements.
Instead of heavily relying on regular expressions that can sometimes
be very complex, it uses a C lexical tokenizer. This ensures that
BEGIN/END blocks on functions and structs are properly handled,
even when nested.
Checking before/after the patch series, for both man pages and
rst only had:
- whitespace differences;
- struct_group macros now are shown as inner anonimous structs
as it should be.
Also, I didn't notice any relevant change on the documentation build
time. With that regards, right now, every time a CMatch replacement
rule takes in place, it does:
for each transform:
- tokenizes the source code;
- handle CMatch;
- convert tokens back to a string.
A possible optimization would be to do, instead:
- tokenizes source code;
- for each transform handle CMatch;
- convert tokens back to a string.
For now, I opted not do do it, because:
- too much changes on a single row;
- docs build time is taking ~3:30 minutes, which is
about the same time it ws taken before the changes;
- there is a very dirty hack inside function_xforms:
(KernRe(r"_noprof"), ""). This is meant to change
function prototypes instead of function arguments.
So, if ok for you, I would prefer to merge this one first. We can later
optimize kdoc_parser to avoid multiple token <-> string conversions.
-
One important aspect of this series is that it introduces unittests
for kernel-doc. I used it a lot during the development of this series,
to ensure that the changes I was doing were producing the expected
results. Tests are on two separate files that can be executed directly.
Alternatively, there is a run.py script that runs all of them (and
any other python script named tools/unittests/test_*.py"):
$ tools/unittests/run.py
test_cmatch:
TestSearch:
test_search_acquires_multiple: OK
test_search_acquires_nested_paren: OK
test_search_acquires_simple: OK
test_search_must_hold: OK
test_search_must_hold_shared: OK
test_search_no_false_positive: OK
test_search_no_function: OK
test_search_no_macro_remains: OK
TestSubMultipleMacros:
test_acquires_multiple: OK
test_acquires_nested_paren: OK
test_acquires_simple: OK
test_mixed_macros: OK
test_must_hold: OK
test_must_hold_shared: OK
test_no_false_positive: OK
test_no_function: OK
test_no_macro_remains: OK
TestSubSimple:
test_rise_early_greedy: OK
test_rise_multiple_greedy: OK
test_strip_multiple_acquires: OK
test_sub_count_parameter: OK
test_sub_mixed_placeholders: OK
test_sub_multiple_placeholders: OK
test_sub_no_placeholder: OK
test_sub_single_placeholder: OK
test_sub_with_capture: OK
test_sub_zero_placeholder: OK
TestSubWithLocalXforms:
test_functions_with_acquires_and_releases: OK
test_raw_struct_group: OK
test_raw_struct_group_tagged: OK
test_struct_group: OK
test_struct_group_attr: OK
test_struct_group_tagged_with_private: OK
test_struct_kcov: OK
test_vars_stackdepot: OK
test_tokenizer:
TestPublicPrivate:
test_balanced_inner_private: OK
test_balanced_non_greddy_private: OK
test_balanced_private: OK
test_no private: OK
test_unbalanced_inner_private: OK
test_unbalanced_private: OK
test_unbalanced_struct_group_tagged_with_private: OK
test_unbalanced_two_struct_group_tagged_first_with_private: OK
test_unbalanced_without_end_of_line: OK
TestTokenizer:
test_basic_tokens: OK
test_depth_counters: OK
test_mismatch_error: OK
Ran 47 tests
This commit is contained in:
commit
781171bec0
|
|
@ -213,6 +213,10 @@ The ``private:`` and ``public:`` tags must begin immediately following a
|
|||
``/*`` comment marker. They may optionally include comments between the
|
||||
``:`` and the ending ``*/`` marker.
|
||||
|
||||
When ``private:`` is used on nested structs, it propagates only to inner
|
||||
structs/unions.
|
||||
|
||||
|
||||
Example::
|
||||
|
||||
/**
|
||||
|
|
@ -256,8 +260,10 @@ It is possible to document nested structs and unions, like::
|
|||
union {
|
||||
struct {
|
||||
int memb1;
|
||||
/* private: hides memb2 from documentation */
|
||||
int memb2;
|
||||
};
|
||||
/* Everything here is public again, as private scope finished */
|
||||
struct {
|
||||
void *memb3;
|
||||
int memb4;
|
||||
|
|
|
|||
|
|
@ -11,3 +11,5 @@ Python libraries
|
|||
feat
|
||||
kdoc
|
||||
kabi
|
||||
|
||||
unittest
|
||||
|
|
|
|||
24
Documentation/tools/unittest.rst
Normal file
24
Documentation/tools/unittest.rst
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
.. SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
===============
|
||||
Python unittest
|
||||
===============
|
||||
|
||||
Checking consistency of python modules can be complex. Sometimes, it is
|
||||
useful to define a set of unit tests to help checking them.
|
||||
|
||||
While the actual test implementation is usecase dependent, Python already
|
||||
provides a standard way to add unit tests by using ``import unittest``.
|
||||
|
||||
Using such class, requires setting up a test suite. Also, the default format
|
||||
is a little bit ackward. To improve it and provide a more uniform way to
|
||||
report errors, some unittest classes and functions are defined.
|
||||
|
||||
|
||||
Unittest helper module
|
||||
======================
|
||||
|
||||
.. automodule:: lib.python.unittest_helper
|
||||
:members:
|
||||
:show-inheritance:
|
||||
:undoc-members:
|
||||
655
tools/lib/python/kdoc/c_lex.py
Normal file
655
tools/lib/python/kdoc/c_lex.py
Normal file
|
|
@ -0,0 +1,655 @@
|
|||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
# Copyright(c) 2025: Mauro Carvalho Chehab <mchehab@kernel.org>.
|
||||
|
||||
"""
|
||||
Regular expression ancillary classes.
|
||||
|
||||
Those help caching regular expressions and do matching for kernel-doc.
|
||||
|
||||
Please notice that the code here may rise exceptions to indicate bad
|
||||
usage inside kdoc to indicate problems at the replace pattern.
|
||||
|
||||
Other errors are logged via log instance.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
from copy import copy
|
||||
|
||||
from .kdoc_re import KernRe
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def tokenizer_set_log(logger, prefix = ""):
|
||||
"""
|
||||
Replace the module‑level logger with a LoggerAdapter that
|
||||
prepends *prefix* to every message.
|
||||
"""
|
||||
global log
|
||||
|
||||
class PrefixAdapter(logging.LoggerAdapter):
|
||||
"""
|
||||
Ancillary class to set prefix on all message logs.
|
||||
"""
|
||||
def process(self, msg, kwargs):
|
||||
return f"{prefix}{msg}", kwargs
|
||||
|
||||
# Wrap the provided logger in our adapter
|
||||
log = PrefixAdapter(logger, {"prefix": prefix})
|
||||
|
||||
class CToken():
|
||||
"""
|
||||
Data class to define a C token.
|
||||
"""
|
||||
|
||||
# Tokens that can be used by the parser. Works like an C enum.
|
||||
|
||||
COMMENT = 0 #: A standard C or C99 comment, including delimiter.
|
||||
STRING = 1 #: A string, including quotation marks.
|
||||
CHAR = 2 #: A character, including apostophes.
|
||||
NUMBER = 3 #: A number.
|
||||
PUNC = 4 #: A puntuation mark: / ``,`` / ``.``.
|
||||
BEGIN = 5 #: A begin character: ``{`` / ``[`` / ``(``.
|
||||
END = 6 #: A end character: ``}`` / ``]`` / ``)``.
|
||||
CPP = 7 #: A preprocessor macro.
|
||||
HASH = 8 #: The hash character - useful to handle other macros.
|
||||
OP = 9 #: A C operator (add, subtract, ...).
|
||||
STRUCT = 10 #: A ``struct`` keyword.
|
||||
UNION = 11 #: An ``union`` keyword.
|
||||
ENUM = 12 #: A ``struct`` keyword.
|
||||
TYPEDEF = 13 #: A ``typedef`` keyword.
|
||||
NAME = 14 #: A name. Can be an ID or a type.
|
||||
SPACE = 15 #: Any space characters, including new lines
|
||||
ENDSTMT = 16 #: End of an statement (``;``).
|
||||
|
||||
BACKREF = 17 #: Not a valid C sequence, but used at sub regex patterns.
|
||||
|
||||
MISMATCH = 255 #: an error indicator: should never happen in practice.
|
||||
|
||||
# Dict to convert from an enum interger into a string.
|
||||
_name_by_val = {v: k for k, v in dict(vars()).items() if isinstance(v, int)}
|
||||
|
||||
# Dict to convert from string to an enum-like integer value.
|
||||
_name_to_val = {k: v for v, k in _name_by_val.items()}
|
||||
|
||||
@staticmethod
|
||||
def to_name(val):
|
||||
"""Convert from an integer value from CToken enum into a string"""
|
||||
|
||||
return CToken._name_by_val.get(val, f"UNKNOWN({val})")
|
||||
|
||||
@staticmethod
|
||||
def from_name(name):
|
||||
"""Convert a string into a CToken enum value"""
|
||||
if name in CToken._name_to_val:
|
||||
return CToken._name_to_val[name]
|
||||
|
||||
return CToken.MISMATCH
|
||||
|
||||
|
||||
def __init__(self, kind, value=None, pos=0,
|
||||
brace_level=0, paren_level=0, bracket_level=0):
|
||||
self.kind = kind
|
||||
self.value = value
|
||||
self.pos = pos
|
||||
self.level = (bracket_level, paren_level, brace_level)
|
||||
|
||||
def __repr__(self):
|
||||
name = self.to_name(self.kind)
|
||||
if isinstance(self.value, str):
|
||||
value = '"' + self.value + '"'
|
||||
else:
|
||||
value = self.value
|
||||
|
||||
return f"CToken(CToken.{name}, {value}, {self.pos}, {self.level})"
|
||||
|
||||
#: Regexes to parse C code, transforming it into tokens.
|
||||
RE_SCANNER_LIST = [
|
||||
#
|
||||
# Note that \s\S is different than .*, as it also catches \n
|
||||
#
|
||||
(CToken.COMMENT, r"//[^\n]*|/\*[\s\S]*?\*/"),
|
||||
|
||||
(CToken.STRING, r'"(?:\\.|[^"\\])*"'),
|
||||
(CToken.CHAR, r"'(?:\\.|[^'\\])'"),
|
||||
|
||||
(CToken.NUMBER, r"0[xX][\da-fA-F]+[uUlL]*|0[0-7]+[uUlL]*|"
|
||||
r"\d+(?:\.\d*)?(?:[eE][+-]?\d+)?[fFlL]*"),
|
||||
|
||||
(CToken.ENDSTMT, r"(?:\s+;|;)"),
|
||||
|
||||
(CToken.PUNC, r"[,\.]"),
|
||||
|
||||
(CToken.BEGIN, r"[\[\(\{]"),
|
||||
|
||||
(CToken.END, r"[\]\)\}]"),
|
||||
|
||||
(CToken.CPP, r"#\s*(?:define|include|ifdef|ifndef|if|else|elif|endif|undef|pragma)\b"),
|
||||
|
||||
(CToken.HASH, r"#"),
|
||||
|
||||
(CToken.OP, r"\+\+|\-\-|\->|==|\!=|<=|>=|&&|\|\||<<|>>|\+=|\-=|\*=|/=|%="
|
||||
r"|&=|\|=|\^=|[=\+\-\*/%<>&\|\^~!\?\:]"),
|
||||
|
||||
(CToken.STRUCT, r"\bstruct\b"),
|
||||
(CToken.UNION, r"\bunion\b"),
|
||||
(CToken.ENUM, r"\benum\b"),
|
||||
(CToken.TYPEDEF, r"\btypedef\b"),
|
||||
|
||||
(CToken.NAME, r"[A-Za-z_]\w*"),
|
||||
|
||||
(CToken.SPACE, r"\s+"),
|
||||
|
||||
(CToken.BACKREF, r"\\\d+"),
|
||||
|
||||
(CToken.MISMATCH,r"."),
|
||||
]
|
||||
|
||||
def fill_re_scanner(token_list):
|
||||
"""Ancillary routine to convert RE_SCANNER_LIST into a finditer regex"""
|
||||
re_tokens = []
|
||||
|
||||
for kind, pattern in token_list:
|
||||
name = CToken.to_name(kind)
|
||||
re_tokens.append(f"(?P<{name}>{pattern})")
|
||||
|
||||
return KernRe("|".join(re_tokens), re.MULTILINE | re.DOTALL)
|
||||
|
||||
#: Handle C continuation lines.
|
||||
RE_CONT = KernRe(r"\\\n")
|
||||
|
||||
RE_COMMENT_START = KernRe(r'/\*\s*')
|
||||
|
||||
#: tokenizer regex. Will be filled at the first CTokenizer usage.
|
||||
RE_SCANNER = fill_re_scanner(RE_SCANNER_LIST)
|
||||
|
||||
|
||||
class CTokenizer():
|
||||
"""
|
||||
Scan C statements and definitions and produce tokens.
|
||||
|
||||
When converted to string, it drops comments and handle public/private
|
||||
values, respecting depth.
|
||||
"""
|
||||
|
||||
# This class is inspired and follows the basic concepts of:
|
||||
# https://docs.python.org/3/library/re.html#writing-a-tokenizer
|
||||
|
||||
def __init__(self, source=None, log=None):
|
||||
"""
|
||||
Create a regular expression to handle RE_SCANNER_LIST.
|
||||
|
||||
While I generally don't like using regex group naming via:
|
||||
(?P<name>...)
|
||||
|
||||
in this particular case, it makes sense, as we can pick the name
|
||||
when matching a code via RE_SCANNER.
|
||||
"""
|
||||
|
||||
self.tokens = []
|
||||
|
||||
if not source:
|
||||
return
|
||||
|
||||
if isinstance(source, list):
|
||||
self.tokens = source
|
||||
return
|
||||
|
||||
#
|
||||
# While we could just use _tokenize directly via interator,
|
||||
# As we'll need to use the tokenizer several times inside kernel-doc
|
||||
# to handle macro transforms, cache the results on a list, as
|
||||
# re-using it is cheaper than having to parse everytime.
|
||||
#
|
||||
for tok in self._tokenize(source):
|
||||
self.tokens.append(tok)
|
||||
|
||||
def _tokenize(self, source):
|
||||
"""
|
||||
Iterator that parses ``source``, splitting it into tokens, as defined
|
||||
at ``self.RE_SCANNER_LIST``.
|
||||
|
||||
The interactor returns a CToken class object.
|
||||
"""
|
||||
|
||||
# Handle continuation lines. Note that kdoc_parser already has a
|
||||
# logic to do that. Still, let's keep it for completeness, as we might
|
||||
# end re-using this tokenizer outsize kernel-doc some day - or we may
|
||||
# eventually remove from there as a future cleanup.
|
||||
source = RE_CONT.sub("", source)
|
||||
|
||||
brace_level = 0
|
||||
paren_level = 0
|
||||
bracket_level = 0
|
||||
|
||||
for match in RE_SCANNER.finditer(source):
|
||||
kind = CToken.from_name(match.lastgroup)
|
||||
pos = match.start()
|
||||
value = match.group()
|
||||
|
||||
if kind == CToken.MISMATCH:
|
||||
log.error(f"Unexpected token '{value}' on pos {pos}:\n\t'{source}'")
|
||||
elif kind == CToken.BEGIN:
|
||||
if value == '(':
|
||||
paren_level += 1
|
||||
elif value == '[':
|
||||
bracket_level += 1
|
||||
else: # value == '{'
|
||||
brace_level += 1
|
||||
|
||||
elif kind == CToken.END:
|
||||
if value == ')' and paren_level > 0:
|
||||
paren_level -= 1
|
||||
elif value == ']' and bracket_level > 0:
|
||||
bracket_level -= 1
|
||||
elif brace_level > 0: # value == '}'
|
||||
brace_level -= 1
|
||||
|
||||
yield CToken(kind, value, pos,
|
||||
brace_level, paren_level, bracket_level)
|
||||
|
||||
def __str__(self):
|
||||
out=""
|
||||
show_stack = [True]
|
||||
|
||||
for i, tok in enumerate(self.tokens):
|
||||
if tok.kind == CToken.BEGIN:
|
||||
show_stack.append(show_stack[-1])
|
||||
|
||||
elif tok.kind == CToken.END:
|
||||
prev = show_stack[-1]
|
||||
if len(show_stack) > 1:
|
||||
show_stack.pop()
|
||||
|
||||
if not prev and show_stack[-1]:
|
||||
#
|
||||
# Try to preserve indent
|
||||
#
|
||||
out += "\t" * (len(show_stack) - 1)
|
||||
|
||||
out += str(tok.value)
|
||||
continue
|
||||
|
||||
elif tok.kind == CToken.COMMENT:
|
||||
comment = RE_COMMENT_START.sub("", tok.value)
|
||||
|
||||
if comment.startswith("private:"):
|
||||
show_stack[-1] = False
|
||||
show = False
|
||||
elif comment.startswith("public:"):
|
||||
show_stack[-1] = True
|
||||
|
||||
continue
|
||||
|
||||
if not show_stack[-1]:
|
||||
continue
|
||||
|
||||
if i < len(self.tokens) - 1:
|
||||
next_tok = self.tokens[i + 1]
|
||||
|
||||
# Do some cleanups before ";"
|
||||
|
||||
if tok.kind == CToken.SPACE and next_tok.kind == CToken.ENDSTMT:
|
||||
continue
|
||||
|
||||
if tok.kind == CToken.ENDSTMT and next_tok.kind == tok.kind:
|
||||
continue
|
||||
|
||||
out += str(tok.value)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
class CTokenArgs:
|
||||
"""
|
||||
Ancillary class to help using backrefs from sub matches.
|
||||
|
||||
If the highest backref contain a "+" at the last element,
|
||||
the logic will be greedy, picking all other delims.
|
||||
|
||||
This is needed to parse struct_group macros with end with ``MEMBERS...``.
|
||||
"""
|
||||
def __init__(self, sub_str):
|
||||
self.sub_groups = set()
|
||||
self.max_group = -1
|
||||
self.greedy = None
|
||||
|
||||
for m in KernRe(r'\\(\d+)([+]?)').finditer(sub_str):
|
||||
group = int(m.group(1))
|
||||
if m.group(2) == "+":
|
||||
if self.greedy and self.greedy != group:
|
||||
raise ValueError("There are multiple greedy patterns!")
|
||||
self.greedy = group
|
||||
|
||||
self.sub_groups.add(group)
|
||||
self.max_group = max(self.max_group, group)
|
||||
|
||||
if self.greedy:
|
||||
if self.greedy != self.max_group:
|
||||
raise ValueError("Greedy pattern is not the last one!")
|
||||
|
||||
sub_str = KernRe(r'(\\\d+)[+]').sub(r"\1", sub_str)
|
||||
|
||||
self.sub_str = sub_str
|
||||
self.sub_tokeninzer = CTokenizer(sub_str)
|
||||
|
||||
def groups(self, new_tokenizer):
|
||||
"""
|
||||
Create replacement arguments for backrefs like:
|
||||
|
||||
``\0``, ``\1``, ``\2``, ...``\n``
|
||||
|
||||
It also accepts a ``+`` character to the highest backref. When used,
|
||||
it means in practice to ignore delimins after it, being greedy.
|
||||
|
||||
The logic is smart enough to only go up to the maximum required
|
||||
argument, even if there are more.
|
||||
|
||||
If there is a backref for an argument above the limit, it will
|
||||
raise an exception. Please notice that, on C, square brackets
|
||||
don't have any separator on it. Trying to use ``\1``..``\n`` for
|
||||
brackets also raise an exception.
|
||||
"""
|
||||
|
||||
level = (0, 0, 0)
|
||||
|
||||
if self.max_group < 0:
|
||||
return level, []
|
||||
|
||||
tokens = new_tokenizer.tokens
|
||||
|
||||
#
|
||||
# Fill \0 with the full token contents
|
||||
#
|
||||
groups_list = [ [] ]
|
||||
|
||||
if 0 in self.sub_groups:
|
||||
inner_level = 0
|
||||
|
||||
for i in range(0, len(tokens)):
|
||||
tok = tokens[i]
|
||||
|
||||
if tok.kind == CToken.BEGIN:
|
||||
inner_level += 1
|
||||
|
||||
#
|
||||
# Discard first begin
|
||||
#
|
||||
if not groups_list[0]:
|
||||
continue
|
||||
elif tok.kind == CToken.END:
|
||||
inner_level -= 1
|
||||
if inner_level < 0:
|
||||
break
|
||||
|
||||
if inner_level:
|
||||
groups_list[0].append(tok)
|
||||
|
||||
if not self.max_group:
|
||||
return level, groups_list
|
||||
|
||||
delim = None
|
||||
|
||||
#
|
||||
# Ignore everything before BEGIN. The value of begin gives the
|
||||
# delimiter to be used for the matches
|
||||
#
|
||||
for i in range(0, len(tokens)):
|
||||
tok = tokens[i]
|
||||
if tok.kind == CToken.BEGIN:
|
||||
if tok.value == "{":
|
||||
delim = ";"
|
||||
elif tok.value == "(":
|
||||
delim = ","
|
||||
else:
|
||||
self.log.error(fr"Can't handle \1..\n on {sub_str}")
|
||||
|
||||
level = tok.level
|
||||
break
|
||||
|
||||
pos = 1
|
||||
groups_list.append([])
|
||||
|
||||
inner_level = 0
|
||||
for i in range(i + 1, len(tokens)):
|
||||
tok = tokens[i]
|
||||
|
||||
if tok.kind == CToken.BEGIN:
|
||||
inner_level += 1
|
||||
if tok.kind == CToken.END:
|
||||
inner_level -= 1
|
||||
if inner_level < 0:
|
||||
break
|
||||
|
||||
if tok.kind in [CToken.PUNC, CToken.ENDSTMT] and delim == tok.value:
|
||||
pos += 1
|
||||
if self.greedy and pos > self.max_group:
|
||||
pos -= 1
|
||||
else:
|
||||
groups_list.append([])
|
||||
|
||||
if pos > self.max_group:
|
||||
break
|
||||
|
||||
continue
|
||||
|
||||
groups_list[pos].append(tok)
|
||||
|
||||
if pos < self.max_group:
|
||||
log.error(fr"{self.sub_str} groups are up to {pos} instead of {self.max_group}")
|
||||
|
||||
return level, groups_list
|
||||
|
||||
def tokens(self, new_tokenizer):
|
||||
level, groups = self.groups(new_tokenizer)
|
||||
|
||||
new = CTokenizer()
|
||||
|
||||
for tok in self.sub_tokeninzer.tokens:
|
||||
if tok.kind == CToken.BACKREF:
|
||||
group = int(tok.value[1:])
|
||||
|
||||
for group_tok in groups[group]:
|
||||
new_tok = copy(group_tok)
|
||||
|
||||
new_level = [0, 0, 0]
|
||||
|
||||
for i in range(0, len(level)):
|
||||
new_level[i] = new_tok.level[i] + level[i]
|
||||
|
||||
new_tok.level = tuple(new_level)
|
||||
|
||||
new.tokens += [ new_tok ]
|
||||
else:
|
||||
new.tokens += [ tok ]
|
||||
|
||||
return new.tokens
|
||||
|
||||
|
||||
class CMatch:
|
||||
"""
|
||||
Finding nested delimiters is hard with regular expressions. It is
|
||||
even harder on Python with its normal re module, as there are several
|
||||
advanced regular expressions that are missing.
|
||||
|
||||
This is the case of this pattern::
|
||||
|
||||
'\\bSTRUCT_GROUP(\\(((?:(?>[^)(]+)|(?1))*)\\))[^;]*;'
|
||||
|
||||
which is used to properly match open/close parentheses of the
|
||||
string search STRUCT_GROUP(),
|
||||
|
||||
Add a class that counts pairs of delimiters, using it to match and
|
||||
replace nested expressions.
|
||||
|
||||
The original approach was suggested by:
|
||||
|
||||
https://stackoverflow.com/questions/5454322/python-how-to-match-nested-parentheses-with-regex
|
||||
|
||||
Although I re-implemented it to make it more generic and match 3 types
|
||||
of delimiters. The logic checks if delimiters are paired. If not, it
|
||||
will ignore the search string.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, regex, delim="("):
|
||||
self.regex = KernRe("^" + regex + r"\b")
|
||||
self.start_delim = delim
|
||||
|
||||
def _search(self, tokenizer):
|
||||
"""
|
||||
Finds paired blocks for a regex that ends with a delimiter.
|
||||
|
||||
The suggestion of using finditer to match pairs came from:
|
||||
https://stackoverflow.com/questions/5454322/python-how-to-match-nested-parentheses-with-regex
|
||||
but I ended using a different implementation to align all three types
|
||||
of delimiters and seek for an initial regular expression.
|
||||
|
||||
The algorithm seeks for open/close paired delimiters and places them
|
||||
into a stack, yielding a start/stop position of each match when the
|
||||
stack is zeroed.
|
||||
|
||||
The algorithm should work fine for properly paired lines, but will
|
||||
silently ignore end delimiters that precede a start delimiter.
|
||||
This should be OK for kernel-doc parser, as unaligned delimiters
|
||||
would cause compilation errors. So, we don't need to raise exceptions
|
||||
to cover such issues.
|
||||
"""
|
||||
|
||||
start = None
|
||||
started = False
|
||||
|
||||
import sys
|
||||
|
||||
stack = []
|
||||
|
||||
for i, tok in enumerate(tokenizer.tokens):
|
||||
if start is None:
|
||||
if tok.kind == CToken.NAME and self.regex.match(tok.value):
|
||||
start = i
|
||||
stack.append((start, tok.level))
|
||||
started = False
|
||||
|
||||
continue
|
||||
|
||||
if not started:
|
||||
if tok.kind == CToken.SPACE:
|
||||
continue
|
||||
|
||||
if tok.kind == CToken.BEGIN and tok.value == self.start_delim:
|
||||
started = True
|
||||
continue
|
||||
|
||||
# Name only token without BEGIN/END
|
||||
if i > start:
|
||||
i -= 1
|
||||
yield start, i
|
||||
start = None
|
||||
|
||||
if tok.kind == CToken.END and tok.level == stack[-1][1]:
|
||||
start, level = stack.pop()
|
||||
|
||||
yield start, i
|
||||
start = None
|
||||
|
||||
#
|
||||
# If an END zeroing levels is not there, return remaining stuff
|
||||
# This is meant to solve cases where the caller logic might be
|
||||
# picking an incomplete block.
|
||||
#
|
||||
if start and stack:
|
||||
if started:
|
||||
s = str(tokenizer)
|
||||
log.warning(f"can't find a final end at {s}")
|
||||
|
||||
yield start, len(tokenizer.tokens)
|
||||
|
||||
def search(self, source):
|
||||
"""
|
||||
This is similar to re.search:
|
||||
|
||||
It matches a regex that it is followed by a delimiter,
|
||||
returning occurrences only if all delimiters are paired.
|
||||
"""
|
||||
|
||||
if isinstance(source, CTokenizer):
|
||||
tokenizer = source
|
||||
is_token = True
|
||||
else:
|
||||
tokenizer = CTokenizer(source)
|
||||
is_token = False
|
||||
|
||||
for start, end in self._search(tokenizer):
|
||||
new_tokenizer = CTokenizer(tokenizer.tokens[start:end + 1])
|
||||
|
||||
if is_token:
|
||||
yield new_tokenizer
|
||||
else:
|
||||
yield str(new_tokenizer)
|
||||
|
||||
def sub(self, sub_str, source, count=0):
|
||||
"""
|
||||
This is similar to re.sub:
|
||||
|
||||
It matches a regex that it is followed by a delimiter,
|
||||
replacing occurrences only if all delimiters are paired.
|
||||
|
||||
if the sub argument contains::
|
||||
|
||||
r'\0'
|
||||
|
||||
it will work just like re: it places there the matched paired data
|
||||
with the delimiter stripped.
|
||||
|
||||
If count is different than zero, it will replace at most count
|
||||
items.
|
||||
"""
|
||||
if isinstance(source, CTokenizer):
|
||||
is_token = True
|
||||
tokenizer = source
|
||||
else:
|
||||
is_token = False
|
||||
tokenizer = CTokenizer(source)
|
||||
|
||||
# Detect if sub_str contains sub arguments
|
||||
|
||||
args_match = CTokenArgs(sub_str)
|
||||
|
||||
new_tokenizer = CTokenizer()
|
||||
pos = 0
|
||||
n = 0
|
||||
|
||||
#
|
||||
# NOTE: the code below doesn't consider overlays at sub.
|
||||
# We may need to add some extra unit tests to check if those
|
||||
# would cause problems. When replacing by "", this should not
|
||||
# be a problem, but other transformations could be problematic
|
||||
#
|
||||
for start, end in self._search(tokenizer):
|
||||
new_tokenizer.tokens += tokenizer.tokens[pos:start]
|
||||
|
||||
new = CTokenizer(tokenizer.tokens[start:end + 1])
|
||||
|
||||
new_tokenizer.tokens += args_match.tokens(new)
|
||||
|
||||
pos = end + 1
|
||||
|
||||
n += 1
|
||||
if count and n >= count:
|
||||
break
|
||||
|
||||
new_tokenizer.tokens += tokenizer.tokens[pos:]
|
||||
|
||||
if not is_token:
|
||||
return str(new_tokenizer)
|
||||
|
||||
return new_tokenizer
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Returns a displayable version of the class init.
|
||||
"""
|
||||
|
||||
return f'CMatch("{self.regex.regex.pattern}")'
|
||||
|
|
@ -13,7 +13,8 @@ import sys
|
|||
import re
|
||||
from pprint import pformat
|
||||
|
||||
from kdoc.kdoc_re import NestedMatch, KernRe
|
||||
from kdoc.c_lex import CTokenizer, tokenizer_set_log
|
||||
from kdoc.kdoc_re import KernRe
|
||||
from kdoc.kdoc_item import KdocItem
|
||||
|
||||
#
|
||||
|
|
@ -84,15 +85,9 @@ def trim_private_members(text):
|
|||
"""
|
||||
Remove ``struct``/``enum`` members that have been marked "private".
|
||||
"""
|
||||
# First look for a "public:" block that ends a private region, then
|
||||
# handle the "private until the end" case.
|
||||
#
|
||||
text = KernRe(r'/\*\s*private:.*?/\*\s*public:.*?\*/', flags=re.S).sub('', text)
|
||||
text = KernRe(r'/\*\s*private:.*', flags=re.S).sub('', text)
|
||||
#
|
||||
# We needed the comments to do the above, but now we can take them out.
|
||||
#
|
||||
return KernRe(r'\s*/\*.*?\*/\s*', flags=re.S).sub('', text).strip()
|
||||
|
||||
tokens = CTokenizer(text)
|
||||
return str(tokens)
|
||||
|
||||
class state:
|
||||
"""
|
||||
|
|
@ -258,6 +253,8 @@ class KernelDoc:
|
|||
self.config = config
|
||||
self.xforms = xforms
|
||||
|
||||
tokenizer_set_log(self.config.log, f"{self.fname}: CMatch: ")
|
||||
|
||||
# Initial state for the state machines
|
||||
self.state = state.NORMAL
|
||||
|
||||
|
|
@ -726,6 +723,7 @@ class KernelDoc:
|
|||
#
|
||||
# Do the basic parse to get the pieces of the declaration.
|
||||
#
|
||||
proto = trim_private_members(proto)
|
||||
struct_parts = self.split_struct_proto(proto)
|
||||
if not struct_parts:
|
||||
self.emit_msg(ln, f"{proto} error: Cannot parse struct or union!")
|
||||
|
|
@ -739,7 +737,6 @@ class KernelDoc:
|
|||
#
|
||||
# Go through the list of members applying all of our transformations.
|
||||
#
|
||||
members = trim_private_members(members)
|
||||
members = self.xforms.apply("struct", members)
|
||||
|
||||
#
|
||||
|
|
@ -766,6 +763,7 @@ class KernelDoc:
|
|||
# Strip preprocessor directives. Note that this depends on the
|
||||
# trailing semicolon we added in process_proto_type().
|
||||
#
|
||||
proto = trim_private_members(proto)
|
||||
proto = KernRe(r'#\s*((define|ifdef|if)\s+|endif)[^;]*;', flags=re.S).sub('', proto)
|
||||
#
|
||||
# Parse out the name and members of the enum. Typedef form first.
|
||||
|
|
@ -773,7 +771,7 @@ class KernelDoc:
|
|||
r = KernRe(r'typedef\s+enum\s*\{(.*)\}\s*(\w*)\s*;')
|
||||
if r.search(proto):
|
||||
declaration_name = r.group(2)
|
||||
members = trim_private_members(r.group(1))
|
||||
members = r.group(1)
|
||||
#
|
||||
# Failing that, look for a straight enum
|
||||
#
|
||||
|
|
@ -781,7 +779,7 @@ class KernelDoc:
|
|||
r = KernRe(r'enum\s+(\w*)\s*\{(.*)\}')
|
||||
if r.match(proto):
|
||||
declaration_name = r.group(1)
|
||||
members = trim_private_members(r.group(2))
|
||||
members = r.group(2)
|
||||
#
|
||||
# OK, this isn't going to work.
|
||||
#
|
||||
|
|
@ -810,9 +808,10 @@ class KernelDoc:
|
|||
member_set = set()
|
||||
members = KernRe(r'\([^;)]*\)').sub('', members)
|
||||
for arg in members.split(','):
|
||||
if not arg:
|
||||
continue
|
||||
arg = KernRe(r'^\s*(\w+).*').sub(r'\1', arg)
|
||||
if not arg.strip():
|
||||
continue
|
||||
|
||||
self.entry.parameterlist.append(arg)
|
||||
if arg not in self.entry.parameterdescs:
|
||||
self.entry.parameterdescs[arg] = self.undescribed
|
||||
|
|
@ -1355,6 +1354,12 @@ class KernelDoc:
|
|||
elif doc_content.search(line):
|
||||
self.emit_msg(ln, f"Incorrect use of kernel-doc format: {line}")
|
||||
self.state = state.PROTO
|
||||
|
||||
#
|
||||
# Don't let it add partial comments at the code, as breaks the
|
||||
# logic meant to remove comments from prototypes.
|
||||
#
|
||||
self.process_proto_type(ln, "/**\n" + line)
|
||||
# else ... ??
|
||||
|
||||
def process_inline_text(self, ln, line):
|
||||
|
|
|
|||
|
|
@ -140,204 +140,3 @@ class KernRe:
|
|||
"""
|
||||
|
||||
return self.last_match.groups()
|
||||
|
||||
#: Nested delimited pairs (brackets and parenthesis)
|
||||
DELIMITER_PAIRS = {
|
||||
'{': '}',
|
||||
'(': ')',
|
||||
'[': ']',
|
||||
}
|
||||
|
||||
#: compiled delimiters
|
||||
RE_DELIM = KernRe(r'[\{\}\[\]\(\)]')
|
||||
|
||||
|
||||
class NestedMatch:
|
||||
"""
|
||||
Finding nested delimiters is hard with regular expressions. It is
|
||||
even harder on Python with its normal re module, as there are several
|
||||
advanced regular expressions that are missing.
|
||||
|
||||
This is the case of this pattern::
|
||||
|
||||
'\\bSTRUCT_GROUP(\\(((?:(?>[^)(]+)|(?1))*)\\))[^;]*;'
|
||||
|
||||
which is used to properly match open/close parentheses of the
|
||||
string search STRUCT_GROUP(),
|
||||
|
||||
Add a class that counts pairs of delimiters, using it to match and
|
||||
replace nested expressions.
|
||||
|
||||
The original approach was suggested by:
|
||||
|
||||
https://stackoverflow.com/questions/5454322/python-how-to-match-nested-parentheses-with-regex
|
||||
|
||||
Although I re-implemented it to make it more generic and match 3 types
|
||||
of delimiters. The logic checks if delimiters are paired. If not, it
|
||||
will ignore the search string.
|
||||
"""
|
||||
|
||||
# TODO: make NestedMatch handle multiple match groups
|
||||
#
|
||||
# Right now, regular expressions to match it are defined only up to
|
||||
# the start delimiter, e.g.:
|
||||
#
|
||||
# \bSTRUCT_GROUP\(
|
||||
#
|
||||
# is similar to: STRUCT_GROUP\((.*)\)
|
||||
# except that the content inside the match group is delimiter-aligned.
|
||||
#
|
||||
# The content inside parentheses is converted into a single replace
|
||||
# group (e.g. r`\0').
|
||||
#
|
||||
# It would be nice to change such definition to support multiple
|
||||
# match groups, allowing a regex equivalent to:
|
||||
#
|
||||
# FOO\((.*), (.*), (.*)\)
|
||||
#
|
||||
# it is probably easier to define it not as a regular expression, but
|
||||
# with some lexical definition like:
|
||||
#
|
||||
# FOO(arg1, arg2, arg3)
|
||||
|
||||
def __init__(self, regex):
|
||||
self.regex = KernRe(regex)
|
||||
|
||||
def _search(self, line):
|
||||
"""
|
||||
Finds paired blocks for a regex that ends with a delimiter.
|
||||
|
||||
The suggestion of using finditer to match pairs came from:
|
||||
https://stackoverflow.com/questions/5454322/python-how-to-match-nested-parentheses-with-regex
|
||||
but I ended using a different implementation to align all three types
|
||||
of delimiters and seek for an initial regular expression.
|
||||
|
||||
The algorithm seeks for open/close paired delimiters and places them
|
||||
into a stack, yielding a start/stop position of each match when the
|
||||
stack is zeroed.
|
||||
|
||||
The algorithm should work fine for properly paired lines, but will
|
||||
silently ignore end delimiters that precede a start delimiter.
|
||||
This should be OK for kernel-doc parser, as unaligned delimiters
|
||||
would cause compilation errors. So, we don't need to raise exceptions
|
||||
to cover such issues.
|
||||
"""
|
||||
|
||||
stack = []
|
||||
|
||||
for match_re in self.regex.finditer(line):
|
||||
start = match_re.start()
|
||||
offset = match_re.end()
|
||||
string_char = None
|
||||
escape = False
|
||||
|
||||
d = line[offset - 1]
|
||||
if d not in DELIMITER_PAIRS:
|
||||
continue
|
||||
|
||||
end = DELIMITER_PAIRS[d]
|
||||
stack.append(end)
|
||||
|
||||
for match in RE_DELIM.finditer(line[offset:]):
|
||||
pos = match.start() + offset
|
||||
|
||||
d = line[pos]
|
||||
|
||||
if escape:
|
||||
escape = False
|
||||
continue
|
||||
|
||||
if string_char:
|
||||
if d == '\\':
|
||||
escape = True
|
||||
elif d == string_char:
|
||||
string_char = None
|
||||
|
||||
continue
|
||||
|
||||
if d in ('"', "'"):
|
||||
string_char = d
|
||||
continue
|
||||
|
||||
if d in DELIMITER_PAIRS:
|
||||
end = DELIMITER_PAIRS[d]
|
||||
|
||||
stack.append(end)
|
||||
continue
|
||||
|
||||
# Does the end delimiter match what is expected?
|
||||
if stack and d == stack[-1]:
|
||||
stack.pop()
|
||||
|
||||
if not stack:
|
||||
yield start, offset, pos + 1
|
||||
break
|
||||
|
||||
def search(self, line):
|
||||
"""
|
||||
This is similar to re.search:
|
||||
|
||||
It matches a regex that it is followed by a delimiter,
|
||||
returning occurrences only if all delimiters are paired.
|
||||
"""
|
||||
|
||||
for t in self._search(line):
|
||||
|
||||
yield line[t[0]:t[2]]
|
||||
|
||||
def sub(self, sub, line, count=0):
|
||||
"""
|
||||
This is similar to re.sub:
|
||||
|
||||
It matches a regex that it is followed by a delimiter,
|
||||
replacing occurrences only if all delimiters are paired.
|
||||
|
||||
if the sub argument contains::
|
||||
|
||||
r'\0'
|
||||
|
||||
it will work just like re: it places there the matched paired data
|
||||
with the delimiter stripped.
|
||||
|
||||
If count is different than zero, it will replace at most count
|
||||
items.
|
||||
"""
|
||||
out = ""
|
||||
|
||||
cur_pos = 0
|
||||
n = 0
|
||||
|
||||
for start, end, pos in self._search(line):
|
||||
out += line[cur_pos:start]
|
||||
|
||||
# Value, ignoring start/end delimiters
|
||||
value = line[end:pos - 1]
|
||||
|
||||
# replaces \0 at the sub string, if \0 is used there
|
||||
new_sub = sub
|
||||
new_sub = new_sub.replace(r'\0', value)
|
||||
|
||||
out += new_sub
|
||||
|
||||
# Drop end ';' if any
|
||||
if pos < len(line) and line[pos] == ';':
|
||||
pos += 1
|
||||
|
||||
cur_pos = pos
|
||||
n += 1
|
||||
|
||||
if count and count >= n:
|
||||
break
|
||||
|
||||
# Append the remaining string
|
||||
l = len(line)
|
||||
out += line[cur_pos:l]
|
||||
|
||||
return out
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Returns a displayable version of the class init.
|
||||
"""
|
||||
|
||||
return f'NestedMatch("{self.regex.regex.pattern}")'
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@
|
|||
|
||||
import re
|
||||
|
||||
from kdoc.kdoc_re import KernRe, NestedMatch
|
||||
from kdoc.kdoc_re import KernRe
|
||||
from kdoc.c_lex import CMatch, CTokenizer
|
||||
|
||||
struct_args_pattern = r"([^,)]+)"
|
||||
|
||||
struct_args_pattern = r'([^,)]+)'
|
||||
|
||||
class CTransforms:
|
||||
"""
|
||||
|
|
@ -15,137 +17,106 @@ class CTransforms:
|
|||
into something we can parse and generate kdoc for.
|
||||
"""
|
||||
|
||||
#
|
||||
# NOTE:
|
||||
# Due to performance reasons, place CMatch rules before KernRe,
|
||||
# as this avoids running the C parser every time.
|
||||
#
|
||||
|
||||
#: Transforms for structs and unions.
|
||||
struct_xforms = [
|
||||
# Strip attributes
|
||||
(KernRe(r"__attribute__\s*\(\([a-z0-9,_\*\s\(\)]*\)\)", flags=re.I | re.S, cache=False), ' '),
|
||||
(KernRe(r'\s*__aligned\s*\([^;]*\)', re.S), ' '),
|
||||
(KernRe(r'\s*__counted_by\s*\([^;]*\)', re.S), ' '),
|
||||
(KernRe(r'\s*__counted_by_(le|be)\s*\([^;]*\)', re.S), ' '),
|
||||
(KernRe(r'\s*__guarded_by\s*\([^\)]*\)', re.S), ' '),
|
||||
(KernRe(r'\s*__pt_guarded_by\s*\([^\)]*\)', re.S), ' '),
|
||||
(KernRe(r'\s*__packed\s*', re.S), ' '),
|
||||
(KernRe(r'\s*CRYPTO_MINALIGN_ATTR', re.S), ' '),
|
||||
(KernRe(r'\s*__private', re.S), ' '),
|
||||
(KernRe(r'\s*__rcu', re.S), ' '),
|
||||
(KernRe(r'\s*____cacheline_aligned_in_smp', re.S), ' '),
|
||||
(KernRe(r'\s*____cacheline_aligned', re.S), ' '),
|
||||
(KernRe(r'\s*__cacheline_group_(begin|end)\([^\)]+\);'), ''),
|
||||
#
|
||||
# Unwrap struct_group macros based on this definition:
|
||||
# __struct_group(TAG, NAME, ATTRS, MEMBERS...)
|
||||
# which has variants like: struct_group(NAME, MEMBERS...)
|
||||
# Only MEMBERS arguments require documentation.
|
||||
#
|
||||
# Parsing them happens on two steps:
|
||||
#
|
||||
# 1. drop struct group arguments that aren't at MEMBERS,
|
||||
# storing them as STRUCT_GROUP(MEMBERS)
|
||||
#
|
||||
# 2. remove STRUCT_GROUP() ancillary macro.
|
||||
#
|
||||
# The original logic used to remove STRUCT_GROUP() using an
|
||||
# advanced regex:
|
||||
#
|
||||
# \bSTRUCT_GROUP(\(((?:(?>[^)(]+)|(?1))*)\))[^;]*;
|
||||
#
|
||||
# with two patterns that are incompatible with
|
||||
# Python re module, as it has:
|
||||
#
|
||||
# - a recursive pattern: (?1)
|
||||
# - an atomic grouping: (?>...)
|
||||
#
|
||||
# I tried a simpler version: but it didn't work either:
|
||||
# \bSTRUCT_GROUP\(([^\)]+)\)[^;]*;
|
||||
#
|
||||
# As it doesn't properly match the end parenthesis on some cases.
|
||||
#
|
||||
# So, a better solution was crafted: there's now a NestedMatch
|
||||
# class that ensures that delimiters after a search are properly
|
||||
# matched. So, the implementation to drop STRUCT_GROUP() will be
|
||||
# handled in separate.
|
||||
#
|
||||
(KernRe(r'\bstruct_group\s*\(([^,]*,)', re.S), r'STRUCT_GROUP('),
|
||||
(KernRe(r'\bstruct_group_attr\s*\(([^,]*,){2}', re.S), r'STRUCT_GROUP('),
|
||||
(KernRe(r'\bstruct_group_tagged\s*\(([^,]*),([^,]*),', re.S), r'struct \1 \2; STRUCT_GROUP('),
|
||||
(KernRe(r'\b__struct_group\s*\(([^,]*,){3}', re.S), r'STRUCT_GROUP('),
|
||||
#
|
||||
# Replace macros
|
||||
#
|
||||
# TODO: use NestedMatch for FOO($1, $2, ...) matches
|
||||
#
|
||||
# it is better to also move those to the NestedMatch logic,
|
||||
# to ensure that parentheses will be properly matched.
|
||||
#
|
||||
(KernRe(r'__ETHTOOL_DECLARE_LINK_MODE_MASK\s*\(([^\)]+)\)', re.S),
|
||||
r'DECLARE_BITMAP(\1, __ETHTOOL_LINK_MODE_MASK_NBITS)'),
|
||||
(KernRe(r'DECLARE_PHY_INTERFACE_MASK\s*\(([^\)]+)\)', re.S),
|
||||
r'DECLARE_BITMAP(\1, PHY_INTERFACE_MODE_MAX)'),
|
||||
(KernRe(r'DECLARE_BITMAP\s*\(' + struct_args_pattern + r',\s*' + struct_args_pattern + r'\)',
|
||||
re.S), r'unsigned long \1[BITS_TO_LONGS(\2)]'),
|
||||
(KernRe(r'DECLARE_HASHTABLE\s*\(' + struct_args_pattern + r',\s*' + struct_args_pattern + r'\)',
|
||||
re.S), r'unsigned long \1[1 << ((\2) - 1)]'),
|
||||
(KernRe(r'DECLARE_KFIFO\s*\(' + struct_args_pattern + r',\s*' + struct_args_pattern +
|
||||
r',\s*' + struct_args_pattern + r'\)', re.S), r'\2 *\1'),
|
||||
(KernRe(r'DECLARE_KFIFO_PTR\s*\(' + struct_args_pattern + r',\s*' +
|
||||
struct_args_pattern + r'\)', re.S), r'\2 *\1'),
|
||||
(KernRe(r'(?:__)?DECLARE_FLEX_ARRAY\s*\(' + struct_args_pattern + r',\s*' +
|
||||
struct_args_pattern + r'\)', re.S), r'\1 \2[]'),
|
||||
(KernRe(r'DEFINE_DMA_UNMAP_ADDR\s*\(' + struct_args_pattern + r'\)', re.S), r'dma_addr_t \1'),
|
||||
(KernRe(r'DEFINE_DMA_UNMAP_LEN\s*\(' + struct_args_pattern + r'\)', re.S), r'__u32 \1'),
|
||||
(KernRe(r'VIRTIO_DECLARE_FEATURES\(([\w_]+)\)'), r'union { u64 \1; u64 \1_array[VIRTIO_FEATURES_U64S]; }'),
|
||||
(CMatch("__attribute__"), ""),
|
||||
(CMatch("__aligned"), ""),
|
||||
(CMatch("__counted_by"), ""),
|
||||
(CMatch("__counted_by_(le|be)"), ""),
|
||||
(CMatch("__guarded_by"), ""),
|
||||
(CMatch("__pt_guarded_by"), ""),
|
||||
(CMatch("__packed"), ""),
|
||||
(CMatch("CRYPTO_MINALIGN_ATTR"), ""),
|
||||
(CMatch("__private"), ""),
|
||||
(CMatch("__rcu"), ""),
|
||||
(CMatch("____cacheline_aligned_in_smp"), ""),
|
||||
(CMatch("____cacheline_aligned"), ""),
|
||||
(CMatch("__cacheline_group_(?:begin|end)"), ""),
|
||||
(CMatch("__ETHTOOL_DECLARE_LINK_MODE_MASK"), r"DECLARE_BITMAP(\1, __ETHTOOL_LINK_MODE_MASK_NBITS)"),
|
||||
(CMatch("DECLARE_PHY_INTERFACE_MASK",),r"DECLARE_BITMAP(\1, PHY_INTERFACE_MODE_MAX)"),
|
||||
(CMatch("DECLARE_BITMAP"), r"unsigned long \1[BITS_TO_LONGS(\2)]"),
|
||||
(CMatch("DECLARE_HASHTABLE"), r"unsigned long \1[1 << ((\2) - 1)]"),
|
||||
(CMatch("DECLARE_KFIFO"), r"\2 *\1"),
|
||||
(CMatch("DECLARE_KFIFO_PTR"), r"\2 *\1"),
|
||||
(CMatch("(?:__)?DECLARE_FLEX_ARRAY"), r"\1 \2[]"),
|
||||
(CMatch("DEFINE_DMA_UNMAP_ADDR"), r"dma_addr_t \1"),
|
||||
(CMatch("DEFINE_DMA_UNMAP_LEN"), r"__u32 \1"),
|
||||
(CMatch("VIRTIO_DECLARE_FEATURES"), r"union { u64 \1; u64 \1_array[VIRTIO_FEATURES_U64S]; }"),
|
||||
(CMatch("__cond_acquires"), ""),
|
||||
(CMatch("__cond_releases"), ""),
|
||||
(CMatch("__acquires"), ""),
|
||||
(CMatch("__releases"), ""),
|
||||
(CMatch("__must_hold"), ""),
|
||||
(CMatch("__must_not_hold"), ""),
|
||||
(CMatch("__must_hold_shared"), ""),
|
||||
(CMatch("__cond_acquires_shared"), ""),
|
||||
(CMatch("__acquires_shared"), ""),
|
||||
(CMatch("__releases_shared"), ""),
|
||||
(CMatch("__attribute__"), ""),
|
||||
|
||||
(NestedMatch(r"__cond_acquires\s*\("), ""),
|
||||
(NestedMatch(r"__cond_releases\s*\("), ""),
|
||||
(NestedMatch(r"__acquires\s*\("), ""),
|
||||
(NestedMatch(r"__releases\s*\("), ""),
|
||||
(NestedMatch(r"__must_hold\s*\("), ""),
|
||||
(NestedMatch(r"__must_not_hold\s*\("), ""),
|
||||
(NestedMatch(r"__must_hold_shared\s*\("), ""),
|
||||
(NestedMatch(r"__cond_acquires_shared\s*\("), ""),
|
||||
(NestedMatch(r"__acquires_shared\s*\("), ""),
|
||||
(NestedMatch(r"__releases_shared\s*\("), ""),
|
||||
(NestedMatch(r'\bSTRUCT_GROUP\('), r'\0'),
|
||||
#
|
||||
# Macro __struct_group() creates an union with an anonymous
|
||||
# and a non-anonymous struct, depending on the parameters. We only
|
||||
# need one of those at kernel-doc, as we won't be documenting the same
|
||||
# members twice.
|
||||
#
|
||||
(CMatch("struct_group"), r"struct { \2+ };"),
|
||||
(CMatch("struct_group_attr"), r"struct { \3+ };"),
|
||||
(CMatch("struct_group_tagged"), r"struct { \3+ };"),
|
||||
(CMatch("__struct_group"), r"struct { \4+ };"),
|
||||
]
|
||||
|
||||
#: Transforms for function prototypes.
|
||||
function_xforms = [
|
||||
(KernRe(r"^static +"), ""),
|
||||
(KernRe(r"^extern +"), ""),
|
||||
(KernRe(r"^asmlinkage +"), ""),
|
||||
(KernRe(r"^inline +"), ""),
|
||||
(KernRe(r"^__inline__ +"), ""),
|
||||
(KernRe(r"^__inline +"), ""),
|
||||
(KernRe(r"^__always_inline +"), ""),
|
||||
(KernRe(r"^noinline +"), ""),
|
||||
(KernRe(r"^__FORTIFY_INLINE +"), ""),
|
||||
(KernRe(r"__init +"), ""),
|
||||
(KernRe(r"__init_or_module +"), ""),
|
||||
(KernRe(r"__exit +"), ""),
|
||||
(KernRe(r"__deprecated +"), ""),
|
||||
(KernRe(r"__flatten +"), ""),
|
||||
(KernRe(r"__meminit +"), ""),
|
||||
(KernRe(r"__must_check +"), ""),
|
||||
(KernRe(r"__weak +"), ""),
|
||||
(KernRe(r"__sched +"), ""),
|
||||
(KernRe(r"_noprof"), ""),
|
||||
(KernRe(r"__always_unused *"), ""),
|
||||
(KernRe(r"__printf\s*\(\s*\d*\s*,\s*\d*\s*\) +"), ""),
|
||||
(KernRe(r"__(?:re)?alloc_size\s*\(\s*\d+\s*(?:,\s*\d+\s*)?\) +"), ""),
|
||||
(KernRe(r"__diagnose_as\s*\(\s*\S+\s*(?:,\s*\d+\s*)*\) +"), ""),
|
||||
(KernRe(r"DECL_BUCKET_PARAMS\s*\(\s*(\S+)\s*,\s*(\S+)\s*\)"), r"\1, \2"),
|
||||
(KernRe(r"__no_context_analysis\s*"), ""),
|
||||
(KernRe(r"__attribute_const__ +"), ""),
|
||||
(KernRe(r"__attribute__\s*\(\((?:[\w\s]+(?:\([^)]*\))?\s*,?)+\)\)\s+"), ""),
|
||||
(CMatch("static"), ""),
|
||||
(CMatch("extern"), ""),
|
||||
(CMatch("asmlinkage"), ""),
|
||||
(CMatch("inline"), ""),
|
||||
(CMatch("__inline__"), ""),
|
||||
(CMatch("__inline"), ""),
|
||||
(CMatch("__always_inline"), ""),
|
||||
(CMatch("noinline"), ""),
|
||||
(CMatch("__FORTIFY_INLINE"), ""),
|
||||
(CMatch("__init"), ""),
|
||||
(CMatch("__init_or_module"), ""),
|
||||
(CMatch("__exit"), ""),
|
||||
(CMatch("__deprecated"), ""),
|
||||
(CMatch("__flatten"), ""),
|
||||
(CMatch("__meminit"), ""),
|
||||
(CMatch("__must_check"), ""),
|
||||
(CMatch("__weak"), ""),
|
||||
(CMatch("__sched"), ""),
|
||||
(CMatch("__always_unused"), ""),
|
||||
(CMatch("__printf"), ""),
|
||||
(CMatch("__(?:re)?alloc_size"), ""),
|
||||
(CMatch("__diagnose_as"), ""),
|
||||
(CMatch("DECL_BUCKET_PARAMS"), r"\1, \2"),
|
||||
(CMatch("__no_context_analysis"), ""),
|
||||
(CMatch("__attribute_const__"), ""),
|
||||
(CMatch("__attribute__"), ""),
|
||||
|
||||
#
|
||||
# HACK: this is similar to process_export() hack. It is meant to
|
||||
# drop _noproof from function name. See for instance:
|
||||
# ahash_request_alloc kernel-doc declaration at include/crypto/hash.h.
|
||||
#
|
||||
(KernRe("_noprof"), ""),
|
||||
]
|
||||
|
||||
#: Transforms for variable prototypes.
|
||||
var_xforms = [
|
||||
(KernRe(r"__read_mostly"), ""),
|
||||
(KernRe(r"__ro_after_init"), ""),
|
||||
(KernRe(r'\s*__guarded_by\s*\([^\)]*\)', re.S), ""),
|
||||
(KernRe(r'\s*__pt_guarded_by\s*\([^\)]*\)', re.S), ""),
|
||||
(KernRe(r"LIST_HEAD\(([\w_]+)\)"), r"struct list_head \1"),
|
||||
(CMatch("__read_mostly"), ""),
|
||||
(CMatch("__ro_after_init"), ""),
|
||||
(CMatch("__guarded_by"), ""),
|
||||
(CMatch("__pt_guarded_by"), ""),
|
||||
(CMatch("LIST_HEAD"), r"struct list_head \1"),
|
||||
|
||||
(KernRe(r"(?://.*)$"), ""),
|
||||
(KernRe(r"(?:/\*.*\*/)"), ""),
|
||||
(KernRe(r";$"), ""),
|
||||
|
|
@ -158,13 +129,25 @@ class CTransforms:
|
|||
"var": var_xforms,
|
||||
}
|
||||
|
||||
def apply(self, xforms_type, text):
|
||||
def apply(self, xforms_type, source):
|
||||
"""
|
||||
Apply a set of transforms to a block of text.
|
||||
Apply a set of transforms to a block of source.
|
||||
|
||||
As tokenizer is used here, this function also remove comments
|
||||
at the end.
|
||||
"""
|
||||
if xforms_type not in self.xforms:
|
||||
return text
|
||||
return source
|
||||
|
||||
if isinstance(source, str):
|
||||
source = CTokenizer(source)
|
||||
|
||||
for search, subst in self.xforms[xforms_type]:
|
||||
text = search.sub(subst, text)
|
||||
return text
|
||||
#
|
||||
# KernRe only accept strings.
|
||||
#
|
||||
if isinstance(search, KernRe):
|
||||
source = str(source)
|
||||
|
||||
source = search.sub(subst, source)
|
||||
return str(source)
|
||||
|
|
|
|||
353
tools/lib/python/unittest_helper.py
Executable file
353
tools/lib/python/unittest_helper.py
Executable file
|
|
@ -0,0 +1,353 @@
|
|||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
# Copyright(c) 2025-2026: Mauro Carvalho Chehab <mchehab@kernel.org>.
|
||||
#
|
||||
# pylint: disable=C0103,R0912,R0914,E1101
|
||||
|
||||
"""
|
||||
Provides helper functions and classes execute python unit tests.
|
||||
|
||||
Those help functions provide a nice colored output summary of each
|
||||
executed test and, when a test fails, it shows the different in diff
|
||||
format when running in verbose mode, like::
|
||||
|
||||
$ tools/unittests/nested_match.py -v
|
||||
...
|
||||
Traceback (most recent call last):
|
||||
File "/new_devel/docs/tools/unittests/nested_match.py", line 69, in test_count_limit
|
||||
self.assertEqual(replaced, "bar(a); bar(b); foo(c)")
|
||||
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
AssertionError: 'bar(a) foo(b); foo(c)' != 'bar(a); bar(b); foo(c)'
|
||||
- bar(a) foo(b); foo(c)
|
||||
? ^^^^
|
||||
+ bar(a); bar(b); foo(c)
|
||||
? ^^^^^
|
||||
...
|
||||
|
||||
It also allows filtering what tests will be executed via ``-k`` parameter.
|
||||
|
||||
Typical usage is to do::
|
||||
|
||||
from unittest_helper import run_unittest
|
||||
...
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_unittest(__file__)
|
||||
|
||||
If passing arguments is needed, on a more complex scenario, it can be
|
||||
used like on this example::
|
||||
|
||||
from unittest_helper import TestUnits, run_unittest
|
||||
...
|
||||
env = {'sudo': ""}
|
||||
...
|
||||
if __name__ == "__main__":
|
||||
runner = TestUnits()
|
||||
base_parser = runner.parse_args()
|
||||
base_parser.add_argument('--sudo', action='store_true',
|
||||
help='Enable tests requiring sudo privileges')
|
||||
|
||||
args = base_parser.parse_args()
|
||||
|
||||
# Update module-level flag
|
||||
if args.sudo:
|
||||
env['sudo'] = "1"
|
||||
|
||||
# Run tests with customized arguments
|
||||
runner.run(__file__, parser=base_parser, args=args, env=env)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import atexit
|
||||
import os
|
||||
import re
|
||||
import unittest
|
||||
import sys
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class Summary(unittest.TestResult):
|
||||
"""
|
||||
Overrides ``unittest.TestResult`` class to provide a nice colored
|
||||
summary. When in verbose mode, displays actual/expected difference in
|
||||
unified diff format.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
#: Dictionary to store organized test results.
|
||||
self.test_results = {}
|
||||
|
||||
#: max length of the test names.
|
||||
self.max_name_length = 0
|
||||
|
||||
def startTest(self, test):
|
||||
super().startTest(test)
|
||||
test_id = test.id()
|
||||
parts = test_id.split(".")
|
||||
|
||||
# Extract module, class, and method names
|
||||
if len(parts) >= 3:
|
||||
module_name = parts[-3]
|
||||
else:
|
||||
module_name = ""
|
||||
if len(parts) >= 2:
|
||||
class_name = parts[-2]
|
||||
else:
|
||||
class_name = ""
|
||||
|
||||
method_name = parts[-1]
|
||||
|
||||
# Build the hierarchical structure
|
||||
if module_name not in self.test_results:
|
||||
self.test_results[module_name] = {}
|
||||
|
||||
if class_name not in self.test_results[module_name]:
|
||||
self.test_results[module_name][class_name] = []
|
||||
|
||||
# Track maximum test name length for alignment
|
||||
display_name = f"{method_name}:"
|
||||
|
||||
self.max_name_length = max(len(display_name), self.max_name_length)
|
||||
|
||||
def _record_test(self, test, status):
|
||||
test_id = test.id()
|
||||
parts = test_id.split(".")
|
||||
if len(parts) >= 3:
|
||||
module_name = parts[-3]
|
||||
else:
|
||||
module_name = ""
|
||||
if len(parts) >= 2:
|
||||
class_name = parts[-2]
|
||||
else:
|
||||
class_name = ""
|
||||
method_name = parts[-1]
|
||||
self.test_results[module_name][class_name].append((method_name, status))
|
||||
|
||||
def addSuccess(self, test):
|
||||
super().addSuccess(test)
|
||||
self._record_test(test, "OK")
|
||||
|
||||
def addFailure(self, test, err):
|
||||
super().addFailure(test, err)
|
||||
self._record_test(test, "FAIL")
|
||||
|
||||
def addError(self, test, err):
|
||||
super().addError(test, err)
|
||||
self._record_test(test, "ERROR")
|
||||
|
||||
def addSkip(self, test, reason):
|
||||
super().addSkip(test, reason)
|
||||
self._record_test(test, f"SKIP ({reason})")
|
||||
|
||||
def printResults(self):
|
||||
"""
|
||||
Print results using colors if tty.
|
||||
"""
|
||||
# Check for ANSI color support
|
||||
use_color = sys.stdout.isatty()
|
||||
COLORS = {
|
||||
"OK": "\033[32m", # Green
|
||||
"FAIL": "\033[31m", # Red
|
||||
"SKIP": "\033[1;33m", # Yellow
|
||||
"PARTIAL": "\033[33m", # Orange
|
||||
"EXPECTED_FAIL": "\033[36m", # Cyan
|
||||
"reset": "\033[0m", # Reset to default terminal color
|
||||
}
|
||||
if not use_color:
|
||||
for c in COLORS:
|
||||
COLORS[c] = ""
|
||||
|
||||
# Calculate maximum test name length
|
||||
if not self.test_results:
|
||||
return
|
||||
try:
|
||||
lengths = []
|
||||
for module in self.test_results.values():
|
||||
for tests in module.values():
|
||||
for test_name, _ in tests:
|
||||
lengths.append(len(test_name) + 1) # +1 for colon
|
||||
max_length = max(lengths) + 2 # Additional padding
|
||||
except ValueError:
|
||||
sys.exit("Test list is empty")
|
||||
|
||||
# Print results
|
||||
for module_name, classes in self.test_results.items():
|
||||
print(f"{module_name}:")
|
||||
for class_name, tests in classes.items():
|
||||
print(f" {class_name}:")
|
||||
for test_name, status in tests:
|
||||
# Get base status without reason for SKIP
|
||||
if status.startswith("SKIP"):
|
||||
status_code = status.split()[0]
|
||||
else:
|
||||
status_code = status
|
||||
color = COLORS.get(status_code, "")
|
||||
print(
|
||||
f" {test_name + ':':<{max_length}}{color}{status}{COLORS['reset']}"
|
||||
)
|
||||
print()
|
||||
|
||||
# Print summary
|
||||
print(f"\nRan {self.testsRun} tests", end="")
|
||||
if hasattr(self, "timeTaken"):
|
||||
print(f" in {self.timeTaken:.3f}s", end="")
|
||||
print()
|
||||
|
||||
if not self.wasSuccessful():
|
||||
print(f"\n{COLORS['FAIL']}FAILED (", end="")
|
||||
failures = getattr(self, "failures", [])
|
||||
errors = getattr(self, "errors", [])
|
||||
if failures:
|
||||
print(f"failures={len(failures)}", end="")
|
||||
if errors:
|
||||
if failures:
|
||||
print(", ", end="")
|
||||
print(f"errors={len(errors)}", end="")
|
||||
print(f"){COLORS['reset']}")
|
||||
|
||||
|
||||
def flatten_suite(suite):
|
||||
"""Flatten test suite hierarchy."""
|
||||
tests = []
|
||||
for item in suite:
|
||||
if isinstance(item, unittest.TestSuite):
|
||||
tests.extend(flatten_suite(item))
|
||||
else:
|
||||
tests.append(item)
|
||||
return tests
|
||||
|
||||
|
||||
class TestUnits:
|
||||
"""
|
||||
Helper class to set verbosity level.
|
||||
|
||||
This class discover test files, import its unittest classes and
|
||||
executes the test on it.
|
||||
"""
|
||||
def parse_args(self):
|
||||
"""Returns a parser for command line arguments."""
|
||||
parser = argparse.ArgumentParser(description="Test runner with regex filtering")
|
||||
parser.add_argument("-v", "--verbose", action="count", default=1)
|
||||
parser.add_argument("-f", "--failfast", action="store_true")
|
||||
parser.add_argument("-k", "--keyword",
|
||||
help="Regex pattern to filter test methods")
|
||||
return parser
|
||||
|
||||
def run(self, caller_file=None, pattern=None,
|
||||
suite=None, parser=None, args=None, env=None):
|
||||
"""
|
||||
Execute all tests from the unity test file.
|
||||
|
||||
It contains several optional parameters:
|
||||
|
||||
``caller_file``:
|
||||
- name of the file that contains test.
|
||||
|
||||
typical usage is to place __file__ at the caller test, e.g.::
|
||||
|
||||
if __name__ == "__main__":
|
||||
TestUnits().run(__file__)
|
||||
|
||||
``pattern``:
|
||||
- optional pattern to match multiple file names. Defaults
|
||||
to basename of ``caller_file``.
|
||||
|
||||
``suite``:
|
||||
- an unittest suite initialized by the caller using
|
||||
``unittest.TestLoader().discover()``.
|
||||
|
||||
``parser``:
|
||||
- an argparse parser. If not defined, this helper will create
|
||||
one.
|
||||
|
||||
``args``:
|
||||
- an ``argparse.Namespace`` data filled by the caller.
|
||||
|
||||
``env``:
|
||||
- environment variables that will be passed to the test suite
|
||||
|
||||
At least ``caller_file`` or ``suite`` must be used, otherwise a
|
||||
``TypeError`` will be raised.
|
||||
"""
|
||||
if not args:
|
||||
if not parser:
|
||||
parser = self.parse_args()
|
||||
args = parser.parse_args()
|
||||
|
||||
if not caller_file and not suite:
|
||||
raise TypeError("Either caller_file or suite is needed at TestUnits")
|
||||
|
||||
verbose = args.verbose
|
||||
|
||||
if not env:
|
||||
env = os.environ.copy()
|
||||
|
||||
env["VERBOSE"] = f"{verbose}"
|
||||
|
||||
patcher = patch.dict(os.environ, env)
|
||||
patcher.start()
|
||||
# ensure it gets stopped after
|
||||
atexit.register(patcher.stop)
|
||||
|
||||
|
||||
if verbose >= 2:
|
||||
unittest.TextTestRunner(verbosity=verbose).run = lambda suite: suite
|
||||
|
||||
# Load ONLY tests from the calling file
|
||||
if not suite:
|
||||
if not pattern:
|
||||
pattern = caller_file
|
||||
|
||||
loader = unittest.TestLoader()
|
||||
suite = loader.discover(start_dir=os.path.dirname(caller_file),
|
||||
pattern=os.path.basename(caller_file))
|
||||
|
||||
# Flatten the suite for environment injection
|
||||
tests_to_inject = flatten_suite(suite)
|
||||
|
||||
# Filter tests by method name if -k specified
|
||||
if args.keyword:
|
||||
try:
|
||||
pattern = re.compile(args.keyword)
|
||||
filtered_suite = unittest.TestSuite()
|
||||
for test in tests_to_inject: # Use the pre-flattened list
|
||||
method_name = test.id().split(".")[-1]
|
||||
if pattern.search(method_name):
|
||||
filtered_suite.addTest(test)
|
||||
suite = filtered_suite
|
||||
except re.error as e:
|
||||
sys.stderr.write(f"Invalid regex pattern: {e}\n")
|
||||
sys.exit(1)
|
||||
else:
|
||||
# Maintain original suite structure if no keyword filtering
|
||||
suite = unittest.TestSuite(tests_to_inject)
|
||||
|
||||
if verbose >= 2:
|
||||
resultclass = None
|
||||
else:
|
||||
resultclass = Summary
|
||||
|
||||
runner = unittest.TextTestRunner(verbosity=args.verbose,
|
||||
resultclass=resultclass,
|
||||
failfast=args.failfast)
|
||||
result = runner.run(suite)
|
||||
if resultclass:
|
||||
result.printResults()
|
||||
|
||||
sys.exit(not result.wasSuccessful())
|
||||
|
||||
|
||||
def run_unittest(fname):
|
||||
"""
|
||||
Basic usage of TestUnits class.
|
||||
|
||||
Use it when there's no need to pass any extra argument to the tests
|
||||
with. The recommended way is to place this at the end of each
|
||||
unittest module::
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_unittest(__file__)
|
||||
"""
|
||||
TestUnits().run(fname)
|
||||
17
tools/unittests/run.py
Executable file
17
tools/unittests/run.py
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
#!/bin/env python3
|
||||
import os
|
||||
import unittest
|
||||
import sys
|
||||
|
||||
TOOLS_DIR=os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")
|
||||
sys.path.insert(0, TOOLS_DIR)
|
||||
|
||||
from lib.python.unittest_helper import TestUnits
|
||||
|
||||
if __name__ == "__main__":
|
||||
loader = unittest.TestLoader()
|
||||
|
||||
suite = loader.discover(start_dir=os.path.join(TOOLS_DIR, "unittests"),
|
||||
pattern="test*.py")
|
||||
|
||||
TestUnits().run("", suite=suite)
|
||||
821
tools/unittests/test_cmatch.py
Executable file
821
tools/unittests/test_cmatch.py
Executable file
|
|
@ -0,0 +1,821 @@
|
|||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
# Copyright(c) 2026: Mauro Carvalho Chehab <mchehab@kernel.org>.
|
||||
#
|
||||
# pylint: disable=C0413,R0904
|
||||
|
||||
|
||||
"""
|
||||
Unit tests for kernel-doc CMatch.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
# Import Python modules
|
||||
|
||||
SRC_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
sys.path.insert(0, os.path.join(SRC_DIR, "../lib/python"))
|
||||
|
||||
from kdoc.c_lex import CMatch
|
||||
from kdoc.kdoc_re import KernRe
|
||||
from unittest_helper import run_unittest
|
||||
|
||||
#
|
||||
# Override unittest.TestCase to better compare diffs ignoring whitespaces
|
||||
#
|
||||
class TestCaseDiff(unittest.TestCase):
|
||||
"""
|
||||
Disable maximum limit on diffs and add a method to better
|
||||
handle diffs with whitespace differences.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Ensure that there won't be limit for diffs"""
|
||||
cls.maxDiff = None
|
||||
|
||||
|
||||
#
|
||||
# Tests doing with different macros
|
||||
#
|
||||
|
||||
class TestSearch(TestCaseDiff):
|
||||
"""
|
||||
Test search mechanism
|
||||
"""
|
||||
|
||||
def test_search_acquires_simple(self):
|
||||
line = "__acquires(ctx) foo();"
|
||||
result = ", ".join(CMatch("__acquires").search(line))
|
||||
self.assertEqual(result, "__acquires(ctx)")
|
||||
|
||||
def test_search_acquires_multiple(self):
|
||||
line = "__acquires(ctx) __acquires(other) bar();"
|
||||
result = ", ".join(CMatch("__acquires").search(line))
|
||||
self.assertEqual(result, "__acquires(ctx), __acquires(other)")
|
||||
|
||||
def test_search_acquires_nested_paren(self):
|
||||
line = "__acquires((ctx1, ctx2)) baz();"
|
||||
result = ", ".join(CMatch("__acquires").search(line))
|
||||
self.assertEqual(result, "__acquires((ctx1, ctx2))")
|
||||
|
||||
def test_search_must_hold(self):
|
||||
line = "__must_hold(&lock) do_something();"
|
||||
result = ", ".join(CMatch("__must_hold").search(line))
|
||||
self.assertEqual(result, "__must_hold(&lock)")
|
||||
|
||||
def test_search_must_hold_shared(self):
|
||||
line = "__must_hold_shared(RCU) other();"
|
||||
result = ", ".join(CMatch("__must_hold_shared").search(line))
|
||||
self.assertEqual(result, "__must_hold_shared(RCU)")
|
||||
|
||||
def test_search_no_false_positive(self):
|
||||
line = "call__acquires(foo); // should stay intact"
|
||||
result = ", ".join(CMatch(r"__acquires").search(line))
|
||||
self.assertEqual(result, "")
|
||||
|
||||
def test_search_no_macro_remains(self):
|
||||
line = "do_something_else();"
|
||||
result = ", ".join(CMatch("__acquires").search(line))
|
||||
self.assertEqual(result, "")
|
||||
|
||||
def test_search_no_function(self):
|
||||
line = "something"
|
||||
result = ", ".join(CMatch(line).search(line))
|
||||
self.assertEqual(result, "")
|
||||
|
||||
#
|
||||
# Override unittest.TestCase to better compare diffs ignoring whitespaces
|
||||
#
|
||||
class TestCaseDiff(unittest.TestCase):
|
||||
"""
|
||||
Disable maximum limit on diffs and add a method to better
|
||||
handle diffs with whitespace differences.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Ensure that there won't be limit for diffs"""
|
||||
cls.maxDiff = None
|
||||
|
||||
def assertLogicallyEqual(self, a, b):
|
||||
"""
|
||||
Compare two results ignoring multiple whitespace differences.
|
||||
|
||||
This is useful to check more complex matches picked from examples.
|
||||
On a plus side, we also don't need to use dedent.
|
||||
Please notice that line breaks still need to match. We might
|
||||
remove it at the regex, but this way, checking the diff is easier.
|
||||
"""
|
||||
a = re.sub(r"[\t ]+", " ", a.strip())
|
||||
b = re.sub(r"[\t ]+", " ", b.strip())
|
||||
|
||||
a = re.sub(r"\s+\n", "\n", a)
|
||||
b = re.sub(r"\s+\n", "\n", b)
|
||||
|
||||
a = re.sub(" ;", ";", a)
|
||||
b = re.sub(" ;", ";", b)
|
||||
|
||||
self.assertEqual(a, b)
|
||||
|
||||
#
|
||||
# Tests doing with different macros
|
||||
#
|
||||
|
||||
class TestSubMultipleMacros(TestCaseDiff):
|
||||
"""
|
||||
Tests doing with different macros.
|
||||
|
||||
Here, we won't use assertLogicallyEqual. Instead, we'll check if each
|
||||
of the expected patterns are present at the answer.
|
||||
"""
|
||||
|
||||
def test_acquires_simple(self):
|
||||
"""Simple replacement test with __acquires"""
|
||||
line = "__acquires(ctx) foo();"
|
||||
result = CMatch(r"__acquires").sub("REPLACED", line)
|
||||
|
||||
self.assertEqual("REPLACED foo();", result)
|
||||
|
||||
def test_acquires_multiple(self):
|
||||
"""Multiple __acquires"""
|
||||
line = "__acquires(ctx) __acquires(other) bar();"
|
||||
result = CMatch(r"__acquires").sub("REPLACED", line)
|
||||
|
||||
self.assertEqual("REPLACED REPLACED bar();", result)
|
||||
|
||||
def test_acquires_nested_paren(self):
|
||||
"""__acquires with nested pattern"""
|
||||
line = "__acquires((ctx1, ctx2)) baz();"
|
||||
result = CMatch(r"__acquires").sub("REPLACED", line)
|
||||
|
||||
self.assertEqual("REPLACED baz();", result)
|
||||
|
||||
def test_must_hold(self):
|
||||
"""__must_hold with a pointer"""
|
||||
line = "__must_hold(&lock) do_something();"
|
||||
result = CMatch(r"__must_hold").sub("REPLACED", line)
|
||||
|
||||
self.assertNotIn("__must_hold(", result)
|
||||
self.assertIn("do_something();", result)
|
||||
|
||||
def test_must_hold_shared(self):
|
||||
"""__must_hold with an upercase defined value"""
|
||||
line = "__must_hold_shared(RCU) other();"
|
||||
result = CMatch(r"__must_hold_shared").sub("REPLACED", line)
|
||||
|
||||
self.assertNotIn("__must_hold_shared(", result)
|
||||
self.assertIn("other();", result)
|
||||
|
||||
def test_no_false_positive(self):
|
||||
"""
|
||||
Ensure that unrelated text containing similar patterns is preserved
|
||||
"""
|
||||
line = "call__acquires(foo); // should stay intact"
|
||||
result = CMatch(r"\b__acquires").sub("REPLACED", line)
|
||||
|
||||
self.assertLogicallyEqual(result, "call__acquires(foo);")
|
||||
|
||||
def test_mixed_macros(self):
|
||||
"""Add a mix of macros"""
|
||||
line = "__acquires(ctx) __releases(ctx) __must_hold(&lock) foo();"
|
||||
|
||||
result = CMatch(r"__acquires").sub("REPLACED", line)
|
||||
result = CMatch(r"__releases").sub("REPLACED", result)
|
||||
result = CMatch(r"__must_hold").sub("REPLACED", result)
|
||||
|
||||
self.assertNotIn("__acquires(", result)
|
||||
self.assertNotIn("__releases(", result)
|
||||
self.assertNotIn("__must_hold(", result)
|
||||
|
||||
self.assertIn("foo();", result)
|
||||
|
||||
def test_no_macro_remains(self):
|
||||
"""Ensures that unmatched macros are untouched"""
|
||||
line = "do_something_else();"
|
||||
result = CMatch(r"__acquires").sub("REPLACED", line)
|
||||
|
||||
self.assertEqual(result, line)
|
||||
|
||||
def test_no_function(self):
|
||||
"""Ensures that no functions will remain untouched"""
|
||||
line = "something"
|
||||
result = CMatch(line).sub("REPLACED", line)
|
||||
|
||||
self.assertEqual(result, line)
|
||||
|
||||
#
|
||||
# Check if the diff is logically equivalent. To simplify, the tests here
|
||||
# use a single macro name for all replacements.
|
||||
#
|
||||
|
||||
class TestSubSimple(TestCaseDiff):
|
||||
"""
|
||||
Test argument replacements.
|
||||
|
||||
Here, the function name can be anything. So, we picked __attribute__(),
|
||||
to mimic a macro found at the Kernel, but none of the replacements her
|
||||
has any relationship with the Kernel usage.
|
||||
"""
|
||||
|
||||
MACRO = "__attribute__"
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Define a CMatch to be used for all tests"""
|
||||
cls.matcher = CMatch(cls.MACRO)
|
||||
|
||||
def test_sub_with_capture(self):
|
||||
"""Test all arguments replacement with a single arg"""
|
||||
line = f"{self.MACRO}(&ctx)\nfoo();"
|
||||
|
||||
result = self.matcher.sub(r"ACQUIRED(\0)", line)
|
||||
|
||||
self.assertLogicallyEqual("ACQUIRED(&ctx)\nfoo();", result)
|
||||
|
||||
def test_sub_zero_placeholder(self):
|
||||
"""Test all arguments replacement with a multiple args"""
|
||||
line = f"{self.MACRO}(arg1, arg2)\nbar();"
|
||||
|
||||
result = self.matcher.sub(r"REPLACED(\0)", line)
|
||||
|
||||
self.assertLogicallyEqual("REPLACED(arg1, arg2)\nbar();", result)
|
||||
|
||||
def test_sub_single_placeholder(self):
|
||||
"""Single replacement rule for \1"""
|
||||
line = f"{self.MACRO}(ctx, boo)\nfoo();"
|
||||
result = self.matcher.sub(r"ACQUIRED(\1)", line)
|
||||
|
||||
self.assertLogicallyEqual("ACQUIRED(ctx)\nfoo();", result)
|
||||
|
||||
def test_sub_multiple_placeholders(self):
|
||||
"""Replacement rule for both \1 and \2"""
|
||||
line = f"{self.MACRO}(arg1, arg2)\nbar();"
|
||||
result = self.matcher.sub(r"REPLACE(\1, \2)", line)
|
||||
|
||||
self.assertLogicallyEqual("REPLACE(arg1, arg2)\nbar();", result)
|
||||
|
||||
def test_sub_mixed_placeholders(self):
|
||||
"""Replacement rule for \0, \1 and additional text"""
|
||||
line = f"{self.MACRO}(foo, bar)\nbaz();"
|
||||
result = self.matcher.sub(r"ALL(\0) FIRST(\1)", line)
|
||||
|
||||
self.assertLogicallyEqual("ALL(foo, bar) FIRST(foo)\nbaz();", result)
|
||||
|
||||
def test_sub_no_placeholder(self):
|
||||
"""Replacement without placeholders"""
|
||||
line = f"{self.MACRO}(arg)\nfoo();"
|
||||
result = self.matcher.sub(r"NO_BACKREFS()", line)
|
||||
|
||||
self.assertLogicallyEqual("NO_BACKREFS()\nfoo();", result)
|
||||
|
||||
def test_sub_count_parameter(self):
|
||||
"""Verify that the algorithm stops after the requested count"""
|
||||
line = f"{self.MACRO}(a1) x();\n{self.MACRO}(a2) y();"
|
||||
result = self.matcher.sub(r"ONLY_FIRST(\1) ", line, count=1)
|
||||
|
||||
self.assertLogicallyEqual(f"ONLY_FIRST(a1) x();\n{self.MACRO}(a2) y();",
|
||||
result)
|
||||
|
||||
def test_strip_multiple_acquires(self):
|
||||
"""Check if spaces between removed delimiters will be dropped"""
|
||||
line = f"int {self.MACRO}(1) {self.MACRO}(2 ) {self.MACRO}(3) foo;"
|
||||
result = self.matcher.sub("", line)
|
||||
|
||||
self.assertLogicallyEqual(result, "int foo;")
|
||||
|
||||
def test_rise_early_greedy(self):
|
||||
line = f"{self.MACRO}(a, b, c, d);"
|
||||
sub = r"\1, \2+, \3"
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
result = self.matcher.sub(sub, line)
|
||||
|
||||
def test_rise_multiple_greedy(self):
|
||||
line = f"{self.MACRO}(a, b, c, d);"
|
||||
sub = r"\1, \2+, \3+"
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
result = self.matcher.sub(sub, line)
|
||||
|
||||
#
|
||||
# Test replacements with slashrefs
|
||||
#
|
||||
|
||||
|
||||
class TestSubWithLocalXforms(TestCaseDiff):
|
||||
"""
|
||||
Test diferent usecase patterns found at the Kernel.
|
||||
|
||||
Here, replacements using both CMatch and KernRe can be tested,
|
||||
as it will import the actual replacement rules used by kernel-doc.
|
||||
"""
|
||||
|
||||
struct_xforms = [
|
||||
(CMatch("__attribute__"), ' '),
|
||||
(CMatch('__aligned'), ' '),
|
||||
(CMatch('__counted_by'), ' '),
|
||||
(CMatch('__counted_by_(le|be)'), ' '),
|
||||
(CMatch('__guarded_by'), ' '),
|
||||
(CMatch('__pt_guarded_by'), ' '),
|
||||
|
||||
(CMatch('__cacheline_group_(begin|end)'), ''),
|
||||
|
||||
(CMatch('struct_group'), r'\2'),
|
||||
(CMatch('struct_group_attr'), r'\3'),
|
||||
(CMatch('struct_group_tagged'), r'struct \1 { \3+ } \2;'),
|
||||
(CMatch('__struct_group'), r'\4'),
|
||||
|
||||
(CMatch('__ETHTOOL_DECLARE_LINK_MODE_MASK'), r'DECLARE_BITMAP(\1, __ETHTOOL_LINK_MODE_MASK_NBITS)'),
|
||||
(CMatch('DECLARE_PHY_INTERFACE_MASK',), r'DECLARE_BITMAP(\1, PHY_INTERFACE_MODE_MAX)'),
|
||||
(CMatch('DECLARE_BITMAP'), r'unsigned long \1[BITS_TO_LONGS(\2)]'),
|
||||
|
||||
(CMatch('DECLARE_HASHTABLE'), r'unsigned long \1[1 << ((\2) - 1)]'),
|
||||
(CMatch('DECLARE_KFIFO'), r'\2 *\1'),
|
||||
(CMatch('DECLARE_KFIFO_PTR'), r'\2 *\1'),
|
||||
(CMatch('(?:__)?DECLARE_FLEX_ARRAY'), r'\1 \2[]'),
|
||||
(CMatch('DEFINE_DMA_UNMAP_ADDR'), r'dma_addr_t \1'),
|
||||
(CMatch('DEFINE_DMA_UNMAP_LEN'), r'__u32 \1'),
|
||||
(CMatch('VIRTIO_DECLARE_FEATURES'), r'union { u64 \1; u64 \1_array[VIRTIO_FEATURES_U64S]; }'),
|
||||
]
|
||||
|
||||
function_xforms = [
|
||||
(CMatch('__printf'), ""),
|
||||
(CMatch('__(?:re)?alloc_size'), ""),
|
||||
(CMatch("__diagnose_as"), ""),
|
||||
(CMatch("DECL_BUCKET_PARAMS"), r"\1, \2"),
|
||||
|
||||
(CMatch("__cond_acquires"), ""),
|
||||
(CMatch("__cond_releases"), ""),
|
||||
(CMatch("__acquires"), ""),
|
||||
(CMatch("__releases"), ""),
|
||||
(CMatch("__must_hold"), ""),
|
||||
(CMatch("__must_not_hold"), ""),
|
||||
(CMatch("__must_hold_shared"), ""),
|
||||
(CMatch("__cond_acquires_shared"), ""),
|
||||
(CMatch("__acquires_shared"), ""),
|
||||
(CMatch("__releases_shared"), ""),
|
||||
(CMatch("__attribute__"), ""),
|
||||
]
|
||||
|
||||
var_xforms = [
|
||||
(CMatch('__guarded_by'), ""),
|
||||
(CMatch('__pt_guarded_by'), ""),
|
||||
(CMatch("LIST_HEAD"), r"struct list_head \1"),
|
||||
]
|
||||
|
||||
#: Transforms main dictionary used at apply_transforms().
|
||||
xforms = {
|
||||
"struct": struct_xforms,
|
||||
"func": function_xforms,
|
||||
"var": var_xforms,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def apply_transforms(cls, xform_type, text):
|
||||
"""
|
||||
Mimic the behavior of kdoc_parser.apply_transforms() method.
|
||||
|
||||
For each element of STRUCT_XFORMS, apply apply_transforms.
|
||||
|
||||
There are two parameters:
|
||||
|
||||
- ``xform_type``
|
||||
Can be ``func``, ``struct`` or ``var``;
|
||||
- ``text``
|
||||
The text where the sub patterns from CTransforms will be applied.
|
||||
"""
|
||||
for search, subst in cls.xforms.get(xform_type):
|
||||
text = search.sub(subst, text)
|
||||
|
||||
return text.strip()
|
||||
|
||||
cls.matcher = CMatch(r"struct_group[\w\_]*")
|
||||
|
||||
def test_struct_group(self):
|
||||
"""
|
||||
Test struct_group using a pattern from
|
||||
drivers/net/ethernet/asix/ax88796c_main.h.
|
||||
"""
|
||||
line = """
|
||||
struct tx_pkt_info {
|
||||
struct_group(tx_overhead,
|
||||
struct tx_sop_header sop;
|
||||
struct tx_segment_header seg;
|
||||
);
|
||||
struct tx_eop_header eop;
|
||||
u16 pkt_len;
|
||||
u16 seq_num;
|
||||
};
|
||||
"""
|
||||
expected = """
|
||||
struct tx_pkt_info {
|
||||
struct tx_sop_header sop;
|
||||
struct tx_segment_header seg;
|
||||
struct tx_eop_header eop;
|
||||
u16 pkt_len;
|
||||
u16 seq_num;
|
||||
};
|
||||
"""
|
||||
|
||||
result = self.apply_transforms("struct", line)
|
||||
self.assertLogicallyEqual(result, expected)
|
||||
|
||||
def test_struct_group_attr(self):
|
||||
"""
|
||||
Test two struct_group_attr using patterns from fs/smb/client/cifspdu.h.
|
||||
"""
|
||||
line = """
|
||||
typedef struct smb_com_open_rsp {
|
||||
struct smb_hdr hdr; /* wct = 34 BB */
|
||||
__u8 AndXCommand;
|
||||
__u8 AndXReserved;
|
||||
__le16 AndXOffset;
|
||||
__u8 OplockLevel;
|
||||
__u16 Fid;
|
||||
__le32 CreateAction;
|
||||
struct_group_attr(common_attributes,,
|
||||
__le64 CreationTime;
|
||||
__le64 LastAccessTime;
|
||||
__le64 LastWriteTime;
|
||||
__le64 ChangeTime;
|
||||
__le32 FileAttributes;
|
||||
);
|
||||
__le64 AllocationSize;
|
||||
__le64 EndOfFile;
|
||||
__le16 FileType;
|
||||
__le16 DeviceState;
|
||||
__u8 DirectoryFlag;
|
||||
__u16 ByteCount; /* bct = 0 */
|
||||
} OPEN_RSP;
|
||||
typedef struct {
|
||||
struct_group_attr(common_attributes,,
|
||||
__le64 CreationTime;
|
||||
__le64 LastAccessTime;
|
||||
__le64 LastWriteTime;
|
||||
__le64 ChangeTime;
|
||||
__le32 Attributes;
|
||||
);
|
||||
__u32 Pad1;
|
||||
__le64 AllocationSize;
|
||||
__le64 EndOfFile;
|
||||
__le32 NumberOfLinks;
|
||||
__u8 DeletePending;
|
||||
__u8 Directory;
|
||||
__u16 Pad2;
|
||||
__le32 EASize;
|
||||
__le32 FileNameLength;
|
||||
union {
|
||||
char __pad;
|
||||
DECLARE_FLEX_ARRAY(char, FileName);
|
||||
};
|
||||
} FILE_ALL_INFO; /* level 0x107 QPathInfo */
|
||||
"""
|
||||
expected = """
|
||||
typedef struct smb_com_open_rsp {
|
||||
struct smb_hdr hdr;
|
||||
__u8 AndXCommand;
|
||||
__u8 AndXReserved;
|
||||
__le16 AndXOffset;
|
||||
__u8 OplockLevel;
|
||||
__u16 Fid;
|
||||
__le32 CreateAction;
|
||||
__le64 CreationTime;
|
||||
__le64 LastAccessTime;
|
||||
__le64 LastWriteTime;
|
||||
__le64 ChangeTime;
|
||||
__le32 FileAttributes;
|
||||
__le64 AllocationSize;
|
||||
__le64 EndOfFile;
|
||||
__le16 FileType;
|
||||
__le16 DeviceState;
|
||||
__u8 DirectoryFlag;
|
||||
__u16 ByteCount;
|
||||
} OPEN_RSP;
|
||||
typedef struct {
|
||||
__le64 CreationTime;
|
||||
__le64 LastAccessTime;
|
||||
__le64 LastWriteTime;
|
||||
__le64 ChangeTime;
|
||||
__le32 Attributes;
|
||||
__u32 Pad1;
|
||||
__le64 AllocationSize;
|
||||
__le64 EndOfFile;
|
||||
__le32 NumberOfLinks;
|
||||
__u8 DeletePending;
|
||||
__u8 Directory;
|
||||
__u16 Pad2;
|
||||
__le32 EASize;
|
||||
__le32 FileNameLength;
|
||||
union {
|
||||
char __pad;
|
||||
char FileName[];
|
||||
};
|
||||
} FILE_ALL_INFO;
|
||||
"""
|
||||
|
||||
result = self.apply_transforms("struct", line)
|
||||
self.assertLogicallyEqual(result, expected)
|
||||
|
||||
def test_raw_struct_group(self):
|
||||
"""
|
||||
Test a __struct_group pattern from include/uapi/cxl/features.h.
|
||||
"""
|
||||
line = """
|
||||
struct cxl_mbox_get_sup_feats_out {
|
||||
__struct_group(cxl_mbox_get_sup_feats_out_hdr, hdr, /* empty */,
|
||||
__le16 num_entries;
|
||||
__le16 supported_feats;
|
||||
__u8 reserved[4];
|
||||
);
|
||||
struct cxl_feat_entry ents[] __counted_by_le(num_entries);
|
||||
} __attribute__ ((__packed__));
|
||||
"""
|
||||
expected = """
|
||||
struct cxl_mbox_get_sup_feats_out {
|
||||
__le16 num_entries;
|
||||
__le16 supported_feats;
|
||||
__u8 reserved[4];
|
||||
struct cxl_feat_entry ents[];
|
||||
};
|
||||
"""
|
||||
|
||||
result = self.apply_transforms("struct", line)
|
||||
self.assertLogicallyEqual(result, expected)
|
||||
|
||||
def test_raw_struct_group_tagged(self):
|
||||
r"""
|
||||
Test cxl_regs with struct_group_tagged patterns from drivers/cxl/cxl.h.
|
||||
|
||||
NOTE:
|
||||
|
||||
This one has actually a violation from what kernel-doc would
|
||||
expect: Kernel-doc regex expects only 3 members, but this is
|
||||
actually defined as::
|
||||
|
||||
#define struct_group_tagged(TAG, NAME, MEMBERS...)
|
||||
|
||||
The replace expression there is::
|
||||
|
||||
struct \1 { \3 } \2;
|
||||
|
||||
but it should be really something like::
|
||||
|
||||
struct \1 { \3 \4 \5 \6 \7 \8 ... } \2;
|
||||
|
||||
a later fix would be needed to address it.
|
||||
|
||||
"""
|
||||
line = """
|
||||
struct cxl_regs {
|
||||
struct_group_tagged(cxl_component_regs, component,
|
||||
void __iomem *hdm_decoder;
|
||||
void __iomem *ras;
|
||||
);
|
||||
|
||||
|
||||
/* This is actually a violation: too much commas */
|
||||
struct_group_tagged(cxl_device_regs, device_regs,
|
||||
void __iomem *status, *mbox, *memdev;
|
||||
);
|
||||
|
||||
struct_group_tagged(cxl_pmu_regs, pmu_regs,
|
||||
void __iomem *pmu;
|
||||
);
|
||||
|
||||
struct_group_tagged(cxl_rch_regs, rch_regs,
|
||||
void __iomem *dport_aer;
|
||||
);
|
||||
|
||||
struct_group_tagged(cxl_rcd_regs, rcd_regs,
|
||||
void __iomem *rcd_pcie_cap;
|
||||
);
|
||||
};
|
||||
"""
|
||||
expected = """
|
||||
struct cxl_regs {
|
||||
struct cxl_component_regs {
|
||||
void __iomem *hdm_decoder;
|
||||
void __iomem *ras;
|
||||
} component;
|
||||
|
||||
struct cxl_device_regs {
|
||||
void __iomem *status, *mbox, *memdev;
|
||||
} device_regs;
|
||||
|
||||
struct cxl_pmu_regs {
|
||||
void __iomem *pmu;
|
||||
} pmu_regs;
|
||||
|
||||
struct cxl_rch_regs {
|
||||
void __iomem *dport_aer;
|
||||
} rch_regs;
|
||||
|
||||
struct cxl_rcd_regs {
|
||||
void __iomem *rcd_pcie_cap;
|
||||
} rcd_regs;
|
||||
};
|
||||
"""
|
||||
|
||||
result = self.apply_transforms("struct", line)
|
||||
self.assertLogicallyEqual(result, expected)
|
||||
|
||||
def test_struct_group_tagged_with_private(self):
|
||||
"""
|
||||
Replace struct_group_tagged with private, using the same regex
|
||||
for the replacement as what happens in xforms_lists.py.
|
||||
|
||||
As the private removal happens outside NestedGroup class, we manually
|
||||
dropped the remaining part of the struct, to simulate what happens
|
||||
at kdoc_parser.
|
||||
|
||||
Taken from include/net/page_pool/types.h
|
||||
"""
|
||||
line = """
|
||||
struct page_pool_params {
|
||||
struct_group_tagged(page_pool_params_slow, slow,
|
||||
struct net_device *netdev;
|
||||
unsigned int queue_idx;
|
||||
unsigned int flags;
|
||||
/* private: only under "slow" struct */
|
||||
unsigned int ignored;
|
||||
);
|
||||
/* Struct below shall not be ignored */
|
||||
struct_group_tagged(page_pool_params_fast, fast,
|
||||
unsigned int order;
|
||||
unsigned int pool_size;
|
||||
int nid;
|
||||
struct device *dev;
|
||||
struct napi_struct *napi;
|
||||
enum dma_data_direction dma_dir;
|
||||
unsigned int max_len;
|
||||
unsigned int offset;
|
||||
);
|
||||
};
|
||||
"""
|
||||
expected = """
|
||||
struct page_pool_params {
|
||||
struct page_pool_params_slow {
|
||||
struct net_device *netdev;
|
||||
unsigned int queue_idx;
|
||||
unsigned int flags;
|
||||
} slow;
|
||||
struct page_pool_params_fast {
|
||||
unsigned int order;
|
||||
unsigned int pool_size;
|
||||
int nid;
|
||||
struct device *dev;
|
||||
struct napi_struct *napi;
|
||||
enum dma_data_direction dma_dir;
|
||||
unsigned int max_len;
|
||||
unsigned int offset;
|
||||
} fast;
|
||||
};
|
||||
"""
|
||||
|
||||
result = self.apply_transforms("struct", line)
|
||||
self.assertLogicallyEqual(result, expected)
|
||||
|
||||
def test_struct_kcov(self):
|
||||
"""
|
||||
"""
|
||||
line = """
|
||||
struct kcov {
|
||||
refcount_t refcount;
|
||||
spinlock_t lock;
|
||||
enum kcov_mode mode __guarded_by(&lock);
|
||||
unsigned int size __guarded_by(&lock);
|
||||
void *area __guarded_by(&lock);
|
||||
struct task_struct *t __guarded_by(&lock);
|
||||
bool remote;
|
||||
unsigned int remote_size;
|
||||
int sequence;
|
||||
};
|
||||
"""
|
||||
expected = """
|
||||
"""
|
||||
|
||||
result = self.apply_transforms("struct", line)
|
||||
self.assertLogicallyEqual(result, expected)
|
||||
|
||||
|
||||
def test_struct_kcov(self):
|
||||
"""
|
||||
Test a struct from kernel/kcov.c.
|
||||
"""
|
||||
line = """
|
||||
struct kcov {
|
||||
refcount_t refcount;
|
||||
spinlock_t lock;
|
||||
enum kcov_mode mode __guarded_by(&lock);
|
||||
unsigned int size __guarded_by(&lock);
|
||||
void *area __guarded_by(&lock);
|
||||
struct task_struct *t __guarded_by(&lock);
|
||||
bool remote;
|
||||
unsigned int remote_size;
|
||||
int sequence;
|
||||
};
|
||||
"""
|
||||
expected = """
|
||||
struct kcov {
|
||||
refcount_t refcount;
|
||||
spinlock_t lock;
|
||||
enum kcov_mode mode;
|
||||
unsigned int size;
|
||||
void *area;
|
||||
struct task_struct *t;
|
||||
bool remote;
|
||||
unsigned int remote_size;
|
||||
int sequence;
|
||||
};
|
||||
"""
|
||||
|
||||
result = self.apply_transforms("struct", line)
|
||||
self.assertLogicallyEqual(result, expected)
|
||||
|
||||
def test_vars_stackdepot(self):
|
||||
"""
|
||||
Test guarded_by on vars from lib/stackdepot.c.
|
||||
"""
|
||||
line = """
|
||||
size_t pool_offset __guarded_by(&pool_lock) = DEPOT_POOL_SIZE;
|
||||
__guarded_by(&pool_lock) LIST_HEAD(free_stacks);
|
||||
void **stack_pools __pt_guarded_by(&pool_lock);
|
||||
"""
|
||||
expected = """
|
||||
size_t pool_offset = DEPOT_POOL_SIZE;
|
||||
struct list_head free_stacks;
|
||||
void **stack_pools;
|
||||
"""
|
||||
|
||||
result = self.apply_transforms("var", line)
|
||||
self.assertLogicallyEqual(result, expected)
|
||||
|
||||
def test_functions_with_acquires_and_releases(self):
|
||||
"""
|
||||
Test guarded_by on vars from lib/stackdepot.c.
|
||||
"""
|
||||
line = """
|
||||
bool prepare_report_consumer(unsigned long *flags,
|
||||
const struct access_info *ai,
|
||||
struct other_info *other_info) \
|
||||
__cond_acquires(true, &report_lock);
|
||||
|
||||
int tcp_sigpool_start(unsigned int id, struct tcp_sigpool *c) \
|
||||
__cond_acquires(0, RCU_BH);
|
||||
|
||||
bool undo_report_consumer(unsigned long *flags,
|
||||
const struct access_info *ai,
|
||||
struct other_info *other_info) \
|
||||
__cond_releases(true, &report_lock);
|
||||
|
||||
void debugfs_enter_cancellation(struct file *file,
|
||||
struct debugfs_cancellation *c) \
|
||||
__acquires(cancellation);
|
||||
|
||||
void debugfs_leave_cancellation(struct file *file,
|
||||
struct debugfs_cancellation *c) \
|
||||
__releases(cancellation);
|
||||
|
||||
acpi_cpu_flags acpi_os_acquire_lock(acpi_spinlock lockp) \
|
||||
__acquires(lockp);
|
||||
|
||||
void acpi_os_release_lock(acpi_spinlock lockp,
|
||||
acpi_cpu_flags not_used) \
|
||||
__releases(lockp)
|
||||
"""
|
||||
expected = """
|
||||
bool prepare_report_consumer(unsigned long *flags,
|
||||
const struct access_info *ai,
|
||||
struct other_info *other_info);
|
||||
|
||||
int tcp_sigpool_start(unsigned int id, struct tcp_sigpool *c);
|
||||
|
||||
bool undo_report_consumer(unsigned long *flags,
|
||||
const struct access_info *ai,
|
||||
struct other_info *other_info);
|
||||
|
||||
void debugfs_enter_cancellation(struct file *file,
|
||||
struct debugfs_cancellation *c);
|
||||
|
||||
void debugfs_leave_cancellation(struct file *file,
|
||||
struct debugfs_cancellation *c);
|
||||
|
||||
acpi_cpu_flags acpi_os_acquire_lock(acpi_spinlock lockp);
|
||||
|
||||
void acpi_os_release_lock(acpi_spinlock lockp,
|
||||
acpi_cpu_flags not_used)
|
||||
"""
|
||||
|
||||
result = self.apply_transforms("func", line)
|
||||
self.assertLogicallyEqual(result, expected)
|
||||
|
||||
#
|
||||
# Run all tests
|
||||
#
|
||||
if __name__ == "__main__":
|
||||
run_unittest(__file__)
|
||||
462
tools/unittests/test_tokenizer.py
Executable file
462
tools/unittests/test_tokenizer.py
Executable file
|
|
@ -0,0 +1,462 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Unit tests for struct/union member extractor class.
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
import unittest
|
||||
import sys
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
SRC_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
sys.path.insert(0, os.path.join(SRC_DIR, "../lib/python"))
|
||||
|
||||
from kdoc.c_lex import CToken, CTokenizer
|
||||
from unittest_helper import run_unittest
|
||||
|
||||
#
|
||||
# List of tests.
|
||||
#
|
||||
# The code will dynamically generate one test for each key on this dictionary.
|
||||
#
|
||||
def tokens_to_list(tokens):
|
||||
tuples = []
|
||||
|
||||
for tok in tokens:
|
||||
if tok.kind == CToken.SPACE:
|
||||
continue
|
||||
|
||||
tuples += [(tok.kind, tok.value, tok.level)]
|
||||
|
||||
return tuples
|
||||
|
||||
|
||||
def make_tokenizer_test(name, data):
|
||||
"""
|
||||
Create a test named ``name`` using parameters given by ``data`` dict.
|
||||
"""
|
||||
|
||||
def test(self):
|
||||
"""In-lined lambda-like function to run the test"""
|
||||
|
||||
#
|
||||
# Check if logger is working
|
||||
#
|
||||
if "log_level" in data:
|
||||
with self.assertLogs('kdoc.c_lex', level='ERROR') as cm:
|
||||
tokenizer = CTokenizer(data["source"])
|
||||
|
||||
return
|
||||
|
||||
#
|
||||
# Check if tokenizer is producing expected results
|
||||
#
|
||||
tokens = CTokenizer(data["source"]).tokens
|
||||
|
||||
result = tokens_to_list(tokens)
|
||||
expected = tokens_to_list(data["expected"])
|
||||
|
||||
self.assertEqual(result, expected, msg=f"{name}")
|
||||
|
||||
return test
|
||||
|
||||
#: Tokenizer tests.
|
||||
TESTS_TOKENIZER = {
|
||||
"__run__": make_tokenizer_test,
|
||||
|
||||
"basic_tokens": {
|
||||
"source": """
|
||||
int a; // comment
|
||||
float b = 1.23;
|
||||
""",
|
||||
"expected": [
|
||||
CToken(CToken.NAME, "int"),
|
||||
CToken(CToken.NAME, "a"),
|
||||
CToken(CToken.ENDSTMT, ";"),
|
||||
CToken(CToken.COMMENT, "// comment"),
|
||||
CToken(CToken.NAME, "float"),
|
||||
CToken(CToken.NAME, "b"),
|
||||
CToken(CToken.OP, "="),
|
||||
CToken(CToken.NUMBER, "1.23"),
|
||||
CToken(CToken.ENDSTMT, ";"),
|
||||
],
|
||||
},
|
||||
|
||||
"depth_counters": {
|
||||
"source": """
|
||||
struct X {
|
||||
int arr[10];
|
||||
func(a[0], (b + c));
|
||||
}
|
||||
""",
|
||||
"expected": [
|
||||
CToken(CToken.STRUCT, "struct"),
|
||||
CToken(CToken.NAME, "X"),
|
||||
CToken(CToken.BEGIN, "{", brace_level=1),
|
||||
|
||||
CToken(CToken.NAME, "int", brace_level=1),
|
||||
CToken(CToken.NAME, "arr", brace_level=1),
|
||||
CToken(CToken.BEGIN, "[", brace_level=1, bracket_level=1),
|
||||
CToken(CToken.NUMBER, "10", brace_level=1, bracket_level=1),
|
||||
CToken(CToken.END, "]", brace_level=1),
|
||||
CToken(CToken.ENDSTMT, ";", brace_level=1),
|
||||
CToken(CToken.NAME, "func", brace_level=1),
|
||||
CToken(CToken.BEGIN, "(", brace_level=1, paren_level=1),
|
||||
CToken(CToken.NAME, "a", brace_level=1, paren_level=1),
|
||||
CToken(CToken.BEGIN, "[", brace_level=1, paren_level=1, bracket_level=1),
|
||||
CToken(CToken.NUMBER, "0", brace_level=1, paren_level=1, bracket_level=1),
|
||||
CToken(CToken.END, "]", brace_level=1, paren_level=1),
|
||||
CToken(CToken.PUNC, ",", brace_level=1, paren_level=1),
|
||||
CToken(CToken.BEGIN, "(", brace_level=1, paren_level=2),
|
||||
CToken(CToken.NAME, "b", brace_level=1, paren_level=2),
|
||||
CToken(CToken.OP, "+", brace_level=1, paren_level=2),
|
||||
CToken(CToken.NAME, "c", brace_level=1, paren_level=2),
|
||||
CToken(CToken.END, ")", brace_level=1, paren_level=1),
|
||||
CToken(CToken.END, ")", brace_level=1),
|
||||
CToken(CToken.ENDSTMT, ";", brace_level=1),
|
||||
CToken(CToken.END, "}"),
|
||||
],
|
||||
},
|
||||
|
||||
"mismatch_error": {
|
||||
"source": "int a$ = 5;", # $ is illegal
|
||||
"log_level": "ERROR",
|
||||
},
|
||||
}
|
||||
|
||||
def make_private_test(name, data):
|
||||
"""
|
||||
Create a test named ``name`` using parameters given by ``data`` dict.
|
||||
"""
|
||||
|
||||
def test(self):
|
||||
"""In-lined lambda-like function to run the test"""
|
||||
tokens = CTokenizer(data["source"])
|
||||
result = str(tokens)
|
||||
|
||||
#
|
||||
# Avoid whitespace false positives
|
||||
#
|
||||
result = re.sub(r"\s++", " ", result).strip()
|
||||
expected = re.sub(r"\s++", " ", data["trimmed"]).strip()
|
||||
|
||||
msg = f"failed when parsing this source:\n{data['source']}"
|
||||
self.assertEqual(result, expected, msg=msg)
|
||||
|
||||
return test
|
||||
|
||||
#: Tests to check if CTokenizer is handling properly public/private comments.
|
||||
TESTS_PRIVATE = {
|
||||
#
|
||||
# Simplest case: no private. Ensure that trimming won't affect struct
|
||||
#
|
||||
"__run__": make_private_test,
|
||||
"no private": {
|
||||
"source": """
|
||||
struct foo {
|
||||
int a;
|
||||
int b;
|
||||
int c;
|
||||
};
|
||||
""",
|
||||
"trimmed": """
|
||||
struct foo {
|
||||
int a;
|
||||
int b;
|
||||
int c;
|
||||
};
|
||||
""",
|
||||
},
|
||||
|
||||
#
|
||||
# Play "by the books" by always having a public in place
|
||||
#
|
||||
|
||||
"balanced_private": {
|
||||
"source": """
|
||||
struct foo {
|
||||
int a;
|
||||
/* private: */
|
||||
int b;
|
||||
/* public: */
|
||||
int c;
|
||||
};
|
||||
""",
|
||||
"trimmed": """
|
||||
struct foo {
|
||||
int a;
|
||||
int c;
|
||||
};
|
||||
""",
|
||||
},
|
||||
|
||||
"balanced_non_greddy_private": {
|
||||
"source": """
|
||||
struct foo {
|
||||
int a;
|
||||
/* private: */
|
||||
int b;
|
||||
/* public: */
|
||||
int c;
|
||||
/* private: */
|
||||
int d;
|
||||
/* public: */
|
||||
int e;
|
||||
|
||||
};
|
||||
""",
|
||||
"trimmed": """
|
||||
struct foo {
|
||||
int a;
|
||||
int c;
|
||||
int e;
|
||||
};
|
||||
""",
|
||||
},
|
||||
|
||||
"balanced_inner_private": {
|
||||
"source": """
|
||||
struct foo {
|
||||
struct {
|
||||
int a;
|
||||
/* private: ignore below */
|
||||
int b;
|
||||
/* public: but this should not be ignored */
|
||||
};
|
||||
int b;
|
||||
};
|
||||
""",
|
||||
"trimmed": """
|
||||
struct foo {
|
||||
struct {
|
||||
int a;
|
||||
};
|
||||
int b;
|
||||
};
|
||||
""",
|
||||
},
|
||||
|
||||
#
|
||||
# Test what happens if there's no public after private place
|
||||
#
|
||||
|
||||
"unbalanced_private": {
|
||||
"source": """
|
||||
struct foo {
|
||||
int a;
|
||||
/* private: */
|
||||
int b;
|
||||
int c;
|
||||
};
|
||||
""",
|
||||
"trimmed": """
|
||||
struct foo {
|
||||
int a;
|
||||
};
|
||||
""",
|
||||
},
|
||||
|
||||
"unbalanced_inner_private": {
|
||||
"source": """
|
||||
struct foo {
|
||||
struct {
|
||||
int a;
|
||||
/* private: ignore below */
|
||||
int b;
|
||||
/* but this should not be ignored */
|
||||
};
|
||||
int b;
|
||||
};
|
||||
""",
|
||||
"trimmed": """
|
||||
struct foo {
|
||||
struct {
|
||||
int a;
|
||||
};
|
||||
int b;
|
||||
};
|
||||
""",
|
||||
},
|
||||
|
||||
"unbalanced_struct_group_tagged_with_private": {
|
||||
"source": """
|
||||
struct page_pool_params {
|
||||
struct_group_tagged(page_pool_params_fast, fast,
|
||||
unsigned int order;
|
||||
unsigned int pool_size;
|
||||
int nid;
|
||||
struct device *dev;
|
||||
struct napi_struct *napi;
|
||||
enum dma_data_direction dma_dir;
|
||||
unsigned int max_len;
|
||||
unsigned int offset;
|
||||
};
|
||||
struct_group_tagged(page_pool_params_slow, slow,
|
||||
struct net_device *netdev;
|
||||
unsigned int queue_idx;
|
||||
unsigned int flags;
|
||||
/* private: used by test code only */
|
||||
void (*init_callback)(netmem_ref netmem, void *arg);
|
||||
void *init_arg;
|
||||
};
|
||||
};
|
||||
""",
|
||||
"trimmed": """
|
||||
struct page_pool_params {
|
||||
struct_group_tagged(page_pool_params_fast, fast,
|
||||
unsigned int order;
|
||||
unsigned int pool_size;
|
||||
int nid;
|
||||
struct device *dev;
|
||||
struct napi_struct *napi;
|
||||
enum dma_data_direction dma_dir;
|
||||
unsigned int max_len;
|
||||
unsigned int offset;
|
||||
};
|
||||
struct_group_tagged(page_pool_params_slow, slow,
|
||||
struct net_device *netdev;
|
||||
unsigned int queue_idx;
|
||||
unsigned int flags;
|
||||
};
|
||||
};
|
||||
""",
|
||||
},
|
||||
|
||||
"unbalanced_two_struct_group_tagged_first_with_private": {
|
||||
"source": """
|
||||
struct page_pool_params {
|
||||
struct_group_tagged(page_pool_params_slow, slow,
|
||||
struct net_device *netdev;
|
||||
unsigned int queue_idx;
|
||||
unsigned int flags;
|
||||
/* private: used by test code only */
|
||||
void (*init_callback)(netmem_ref netmem, void *arg);
|
||||
void *init_arg;
|
||||
};
|
||||
struct_group_tagged(page_pool_params_fast, fast,
|
||||
unsigned int order;
|
||||
unsigned int pool_size;
|
||||
int nid;
|
||||
struct device *dev;
|
||||
struct napi_struct *napi;
|
||||
enum dma_data_direction dma_dir;
|
||||
unsigned int max_len;
|
||||
unsigned int offset;
|
||||
};
|
||||
};
|
||||
""",
|
||||
"trimmed": """
|
||||
struct page_pool_params {
|
||||
struct_group_tagged(page_pool_params_slow, slow,
|
||||
struct net_device *netdev;
|
||||
unsigned int queue_idx;
|
||||
unsigned int flags;
|
||||
};
|
||||
struct_group_tagged(page_pool_params_fast, fast,
|
||||
unsigned int order;
|
||||
unsigned int pool_size;
|
||||
int nid;
|
||||
struct device *dev;
|
||||
struct napi_struct *napi;
|
||||
enum dma_data_direction dma_dir;
|
||||
unsigned int max_len;
|
||||
unsigned int offset;
|
||||
};
|
||||
};
|
||||
""",
|
||||
},
|
||||
"unbalanced_without_end_of_line": {
|
||||
"source": """ \
|
||||
struct page_pool_params { \
|
||||
struct_group_tagged(page_pool_params_slow, slow, \
|
||||
struct net_device *netdev; \
|
||||
unsigned int queue_idx; \
|
||||
unsigned int flags;
|
||||
/* private: used by test code only */
|
||||
void (*init_callback)(netmem_ref netmem, void *arg); \
|
||||
void *init_arg; \
|
||||
}; \
|
||||
struct_group_tagged(page_pool_params_fast, fast, \
|
||||
unsigned int order; \
|
||||
unsigned int pool_size; \
|
||||
int nid; \
|
||||
struct device *dev; \
|
||||
struct napi_struct *napi; \
|
||||
enum dma_data_direction dma_dir; \
|
||||
unsigned int max_len; \
|
||||
unsigned int offset; \
|
||||
}; \
|
||||
};
|
||||
""",
|
||||
"trimmed": """
|
||||
struct page_pool_params {
|
||||
struct_group_tagged(page_pool_params_slow, slow,
|
||||
struct net_device *netdev;
|
||||
unsigned int queue_idx;
|
||||
unsigned int flags;
|
||||
};
|
||||
struct_group_tagged(page_pool_params_fast, fast,
|
||||
unsigned int order;
|
||||
unsigned int pool_size;
|
||||
int nid;
|
||||
struct device *dev;
|
||||
struct napi_struct *napi;
|
||||
enum dma_data_direction dma_dir;
|
||||
unsigned int max_len;
|
||||
unsigned int offset;
|
||||
};
|
||||
};
|
||||
""",
|
||||
},
|
||||
}
|
||||
|
||||
#: Dict containing all test groups fror CTokenizer
|
||||
TESTS = {
|
||||
"TestPublicPrivate": TESTS_PRIVATE,
|
||||
"TestTokenizer": TESTS_TOKENIZER,
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
self.maxDiff = None
|
||||
|
||||
def build_test_class(group_name, table):
|
||||
"""
|
||||
Dynamically creates a class instance using type() as a generator
|
||||
for a new class derivated from unittest.TestCase.
|
||||
|
||||
We're opting to do it inside a function to avoid the risk of
|
||||
changing the globals() dictionary.
|
||||
"""
|
||||
|
||||
class_dict = {
|
||||
"setUp": setUp
|
||||
}
|
||||
|
||||
run = table["__run__"]
|
||||
|
||||
for test_name, data in table.items():
|
||||
if test_name == "__run__":
|
||||
continue
|
||||
|
||||
class_dict[f"test_{test_name}"] = run(test_name, data)
|
||||
|
||||
cls = type(group_name, (unittest.TestCase,), class_dict)
|
||||
|
||||
return cls.__name__, cls
|
||||
|
||||
#
|
||||
# Create classes and add them to the global dictionary
|
||||
#
|
||||
for group, table in TESTS.items():
|
||||
t = build_test_class(group, table)
|
||||
globals()[t[0]] = t[1]
|
||||
|
||||
#
|
||||
# main
|
||||
#
|
||||
if __name__ == "__main__":
|
||||
run_unittest(__file__)
|
||||
Loading…
Reference in New Issue
Block a user