Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge branch 'mauro' into docs-mw

This series comes after:
https://lore.kernel.org/linux-doc/cover.1773770483.git.mchehab+huawei@kernel.org/

It basically contains patches I submitted before on a 40+ patch series,
but were less relevant, plus a couple of other minor fixes:

- patch 1 improves one of the CTokenizer unit test, fixing some
potential issues on it;
- patches 2 and 3 contain some improvement/fixes for Sphinx
Python autodoc extension. They basically document c_lex.py;

- The remaining patches:
- create a new class for kernel-doc config;
- fix some internal representations of KdocItem;
- add unit tests for KernelDoc() parser class;
- add support to output KdocItem in YAML, which is a
machine-readable output for all documented kAPI.

None of the patches should affect man or html output.

+1322 -58
+9
Documentation/tools/kdoc_ancillary.rst
··· 21 21 :undoc-members: 22 22 23 23 24 + C tokenizer 25 + =========== 26 + 27 + .. automodule:: lib.python.kdoc.c_lex 28 + :members: 29 + :show-inheritance: 30 + :undoc-members: 31 + 32 + 24 33 Chinese, Japanese and Korean variable fonts handler 25 34 =================================================== 26 35
+40 -8
tools/docs/kernel-doc
··· 240 240 help=EXPORT_FILE_DESC) 241 241 242 242 # 243 - # Output format mutually-exclusive group 243 + # Output format 244 244 # 245 - out_group = parser.add_argument_group("Output format selection (mutually exclusive)") 246 - 247 - out_fmt = out_group.add_mutually_exclusive_group() 245 + out_fmt = parser.add_argument_group("Output format selection (mutually exclusive)") 248 246 249 247 out_fmt.add_argument("-m", "-man", "--man", action="store_true", 250 248 help="Output troff manual page format.") ··· 250 252 help="Output reStructuredText format (default).") 251 253 out_fmt.add_argument("-N", "-none", "--none", action="store_true", 252 254 help="Do not output documentation, only warnings.") 255 + 256 + out_fmt.add_argument("-y", "--yaml-file", "--yaml", 257 + help="Stores kernel-doc output on a yaml file.") 258 + out_fmt.add_argument("-k", "--kdoc-item", "--kdoc", action="store_true", 259 + help="Store KdocItem inside yaml file. Ued together with --yaml.") 260 + 253 261 254 262 # 255 263 # Output selection mutually-exclusive group ··· 327 323 from kdoc.kdoc_files import KernelFiles # pylint: disable=C0415 328 324 from kdoc.kdoc_output import RestFormat, ManFormat # pylint: disable=C0415 329 325 330 - if args.man: 331 - out_style = ManFormat(modulename=args.modulename) 332 - elif args.none: 326 + yaml_content = set() 327 + if args.yaml_file: 333 328 out_style = None 329 + 330 + if args.man: 331 + yaml_content |= {"man"} 332 + 333 + if args.rst: 334 + yaml_content |= {"rst"} 335 + 336 + if args.kdoc_item or not yaml_content: 337 + yaml_content |= {"KdocItem"} 338 + 334 339 else: 335 - out_style = RestFormat() 340 + n_outputs = 0 341 + 342 + if args.man: 343 + out_style = ManFormat(modulename=args.modulename) 344 + n_outputs += 1 345 + 346 + if args.none: 347 + out_style = None 348 + n_outputs += 1 349 + 350 + if args.rst or n_outputs == 0: 351 + n_outputs += 1 352 + out_style = RestFormat() 353 + 354 + if n_outputs > 1: 355 + parser.error("Those arguments are muttually exclusive: --man, --rst, --none, except when generating a YAML file.") 356 + 357 + elif not n_outputs: 358 + out_style = RestFormat() 336 359 337 360 kfiles = KernelFiles(verbose=args.verbose, 361 + yaml_file=args.yaml_file, yaml_content=yaml_content, 338 362 out_style=out_style, werror=args.werror, 339 363 wreturn=args.wreturn, wshort_desc=args.wshort_desc, 340 364 wcontents_before_sections=args.wcontents_before_sections)
+5 -4
tools/lib/python/kdoc/c_lex.py
··· 336 336 self.sub_tokeninzer = CTokenizer(sub_str) 337 337 338 338 def groups(self, new_tokenizer): 339 - """ 339 + r""" 340 340 Create replacement arguments for backrefs like: 341 341 342 - ``\0``, ``\1``, ``\2``, ...``\n`` 342 + ``\0``, ``\1``, ``\2``, ... ``\{number}`` 343 343 344 - It also accepts a ``+`` character to the highest backref. When used, 345 - it means in practice to ignore delimins after it, being greedy. 344 + It also accepts a ``+`` character to the highest backref, like 345 + ``\4+``. When used, the backref will be greedy, picking all other 346 + arguments afterwards. 346 347 347 348 The logic is smart enough to only go up to the maximum required 348 349 argument, even if there are more.
+71 -33
tools/lib/python/kdoc/kdoc_files.py
··· 9 9 to generate documentation. 10 10 """ 11 11 12 - import argparse 13 12 import logging 14 13 import os 15 14 import re ··· 16 17 from kdoc.kdoc_parser import KernelDoc 17 18 from kdoc.xforms_lists import CTransforms 18 19 from kdoc.kdoc_output import OutputFormat 20 + from kdoc.kdoc_yaml_file import KDocTestFile 19 21 20 22 21 23 class GlobSourceFiles: ··· 87 87 file_not_found_cb(fname) 88 88 89 89 90 + class KdocConfig(): 91 + """ 92 + Stores all configuration attributes that kdoc_parser and kdoc_output 93 + needs. 94 + """ 95 + def __init__(self, verbose=False, werror=False, wreturn=False, 96 + wshort_desc=False, wcontents_before_sections=False, 97 + logger=None): 98 + 99 + self.verbose = verbose 100 + self.werror = werror 101 + self.wreturn = wreturn 102 + self.wshort_desc = wshort_desc 103 + self.wcontents_before_sections = wcontents_before_sections 104 + 105 + if logger: 106 + self.log = logger 107 + else: 108 + self.log = logging.getLogger(__file__) 109 + 110 + self.warning = self.log.warning 111 + 90 112 class KernelFiles(): 91 113 """ 92 114 Parse kernel-doc tags on multiple kernel source files. ··· 153 131 154 132 If not specified, defaults to use: ``logging.getLogger("kernel-doc")`` 155 133 134 + ``yaml_file`` 135 + If defined, stores the output inside a YAML file. 136 + 137 + ``yaml_content`` 138 + Defines what will be inside the YAML file. 139 + 156 140 Note: 157 141 There are two type of parsers defined here: 158 142 ··· 188 160 if fname in self.files: 189 161 return 190 162 191 - doc = KernelDoc(self.config, fname, self.xforms) 163 + if self.test_file: 164 + store_src = True 165 + else: 166 + store_src = False 167 + 168 + doc = KernelDoc(self.config, fname, self.xforms, store_src=store_src) 192 169 export_table, entries = doc.parse_kdoc() 193 170 194 171 self.export_table[fname] = export_table ··· 202 169 self.export_files.add(fname) # parse_kdoc() already check exports 203 170 204 171 self.results[fname] = entries 172 + 173 + source = doc.get_source() 174 + if source: 175 + self.source[fname] = source 205 176 206 177 def process_export_file(self, fname): 207 178 """ ··· 236 199 def __init__(self, verbose=False, out_style=None, xforms=None, 237 200 werror=False, wreturn=False, wshort_desc=False, 238 201 wcontents_before_sections=False, 239 - logger=None): 202 + yaml_file=None, yaml_content=None, logger=None): 240 203 """ 241 204 Initialize startup variables and parse all files. 242 205 """ ··· 261 224 if kdoc_werror: 262 225 werror = kdoc_werror 263 226 227 + if not logger: 228 + logger = logging.getLogger("kernel-doc") 229 + else: 230 + logger = logger 231 + 264 232 # Some variables are global to the parser logic as a whole as they are 265 233 # used to send control configuration to KernelDoc class. As such, 266 234 # those variables are read-only inside the KernelDoc. 267 - self.config = argparse.Namespace 235 + self.config = KdocConfig(verbose, werror, wreturn, wshort_desc, 236 + wcontents_before_sections, logger) 268 237 269 - self.config.verbose = verbose 270 - self.config.werror = werror 271 - self.config.wreturn = wreturn 272 - self.config.wshort_desc = wshort_desc 273 - self.config.wcontents_before_sections = wcontents_before_sections 238 + # Override log warning, as we want to count errors 239 + self.config.warning = self.warning 240 + 241 + if yaml_file: 242 + self.test_file = KDocTestFile(self.config, yaml_file, yaml_content) 243 + else: 244 + self.test_file = None 274 245 275 246 if xforms: 276 247 self.xforms = xforms 277 248 else: 278 249 self.xforms = CTransforms() 279 250 280 - if not logger: 281 - self.config.log = logging.getLogger("kernel-doc") 282 - else: 283 - self.config.log = logger 284 - 285 - self.config.warning = self.warning 286 - 287 251 self.config.src_tree = os.environ.get("SRCTREE", None) 288 252 289 253 # Initialize variables that are internal to KernelFiles 290 254 291 255 self.out_style = out_style 256 + self.out_style.set_config(self.config) 292 257 293 258 self.errors = 0 294 259 self.results = {} 260 + self.source = {} 295 261 296 262 self.files = set() 297 263 self.export_files = set() ··· 334 294 returning kernel-doc markups on each interaction. 335 295 """ 336 296 337 - self.out_style.set_config(self.config) 338 - 339 297 if not filenames: 340 298 filenames = sorted(self.results.keys()) 341 299 ··· 353 315 for s in symbol: 354 316 function_table.add(s) 355 317 356 - self.out_style.set_filter(export, internal, symbol, nosymbol, 357 - function_table, enable_lineno, 358 - no_doc_sections) 359 - 360 - msg = "" 361 318 if fname not in self.results: 362 319 self.config.log.warning("No kernel-doc for file %s", fname) 363 320 continue 364 321 365 322 symbols = self.results[fname] 366 - self.out_style.set_symbols(symbols) 367 323 368 - for arg in symbols: 369 - m = self.out_msg(fname, arg.name, arg) 324 + if self.test_file: 325 + self.test_file.set_filter(export, internal, symbol, nosymbol, 326 + function_table, enable_lineno, 327 + no_doc_sections) 370 328 371 - if m is None: 372 - ln = arg.get("ln", 0) 373 - dtype = arg.get('type', "") 329 + self.test_file.output_symbols(fname, symbols, 330 + self.source.get(fname)) 374 331 375 - self.config.log.warning("%s:%d Can't handle %s", 376 - fname, ln, dtype) 377 - else: 378 - msg += m 332 + continue 379 333 334 + self.out_style.set_filter(export, internal, symbol, nosymbol, 335 + function_table, enable_lineno, 336 + no_doc_sections) 337 + 338 + msg = self.out_style.output_symbols(fname, symbols) 380 339 if msg: 381 340 yield fname, msg 341 + 342 + if self.test_file: 343 + self.test_file.write()
+36 -3
tools/lib/python/kdoc/kdoc_item.py
··· 22 22 self.sections = {} 23 23 self.sections_start_lines = {} 24 24 self.parameterlist = [] 25 - self.parameterdesc_start_lines = [] 25 + self.parameterdesc_start_lines = {} 26 26 self.parameterdescs = {} 27 27 self.parametertypes = {} 28 + 29 + self.warnings = [] 30 + 28 31 # 29 32 # Just save everything else into our own dict so that the output 30 33 # side can grab it directly as before. As we move things into more 31 34 # structured data, this will, hopefully, fade away. 32 35 # 33 - self.other_stuff = other_stuff 36 + known_keys = { 37 + 'declaration_start_line', 38 + 'sections', 39 + 'sections_start_lines', 40 + 'parameterlist', 41 + 'parameterdesc_start_lines', 42 + 'parameterdescs', 43 + 'parametertypes', 44 + 'warnings', 45 + } 46 + 47 + self.other_stuff = {} 48 + for k, v in other_stuff.items(): 49 + if k in known_keys: 50 + setattr(self, k, v) # real attribute 51 + else: 52 + self.other_stuff[k] = v 34 53 35 54 def get(self, key, default = None): 36 55 """ ··· 60 41 def __getitem__(self, key): 61 42 return self.get(key) 62 43 44 + @classmethod 45 + def from_dict(cls, d): 46 + """Create a KdocItem from a plain dict.""" 47 + 48 + cp = d.copy() 49 + name = cp.pop('name', None) 50 + fname = cp.pop('fname', None) 51 + type = cp.pop('type', None) 52 + start_line = cp.pop('start_line', 1) 53 + other_stuff = cp.pop('other_stuff', {}) 54 + 55 + # Everything that’s left goes straight to __init__ 56 + return cls(name, fname, type, start_line, **cp, **other_stuff) 57 + 63 58 # 64 59 # Tracking of section and parameter information. 65 60 # ··· 82 49 Set sections and start lines. 83 50 """ 84 51 self.sections = sections 85 - self.section_start_lines = start_lines 52 + self.sections_start_lines = start_lines 86 53 87 54 def set_params(self, names, descs, types, starts): 88 55 """
+22 -1
tools/lib/python/kdoc/kdoc_output.py
··· 222 222 223 223 return None 224 224 225 + def output_symbols(self, fname, symbols): 226 + """ 227 + Handles a set of KdocItem symbols. 228 + """ 229 + self.set_symbols(symbols) 230 + 231 + msg = "" 232 + for arg in symbols: 233 + m = self.msg(fname, arg.name, arg) 234 + 235 + if m is None: 236 + ln = arg.get("ln", 0) 237 + dtype = arg.get('type', "") 238 + 239 + self.config.log.warning("%s:%d Can't handle %s", 240 + fname, ln, dtype) 241 + else: 242 + msg += m 243 + 244 + return msg 245 + 225 246 # Virtual methods to be overridden by inherited classes 226 247 # At the base class, those do nothing. 227 248 def set_symbols(self, symbols): ··· 389 368 else: 390 369 self.data += f'{self.lineprefix}**{section}**\n\n' 391 370 392 - self.print_lineno(args.section_start_lines.get(section, 0)) 371 + self.print_lineno(args.sections_start_lines.get(section, 0)) 393 372 self.output_highlight(text) 394 373 self.data += "\n" 395 374 self.data += "\n"
+29 -4
tools/lib/python/kdoc/kdoc_parser.py
··· 140 140 self.parametertypes = {} 141 141 self.parameterdesc_start_lines = {} 142 142 143 - self.section_start_lines = {} 143 + self.sections_start_lines = {} 144 144 self.sections = {} 145 145 146 146 self.anon_struct_union = False ··· 220 220 self.sections[name] += '\n' + contents 221 221 else: 222 222 self.sections[name] = contents 223 - self.section_start_lines[name] = self.new_start_line 223 + self.sections_start_lines[name] = self.new_start_line 224 224 self.new_start_line = 0 225 225 226 226 # self.config.log.debug("Section: %s : %s", name, pformat(vars(self))) ··· 246 246 #: String to write when a parameter is not described. 247 247 undescribed = "-- undescribed --" 248 248 249 - def __init__(self, config, fname, xforms): 249 + def __init__(self, config, fname, xforms, store_src=False): 250 250 """Initialize internal variables""" 251 251 252 252 self.fname = fname 253 253 self.config = config 254 254 self.xforms = xforms 255 + self.store_src = store_src 255 256 256 257 tokenizer_set_log(self.config.log, f"{self.fname}: CMatch: ") 257 258 ··· 264 263 265 264 # Place all potential outputs into an array 266 265 self.entries = [] 266 + 267 + # When store_src is true, the kernel-doc source content is stored here 268 + self.source = None 267 269 268 270 # 269 271 # We need Python 3.7 for its "dicts remember the insertion ··· 320 316 for section in ["Description", "Return"]: 321 317 if section in sections and not sections[section].rstrip(): 322 318 del sections[section] 323 - item.set_sections(sections, self.entry.section_start_lines) 319 + item.set_sections(sections, self.entry.sections_start_lines) 324 320 item.set_params(self.entry.parameterlist, self.entry.parameterdescs, 325 321 self.entry.parametertypes, 326 322 self.entry.parameterdesc_start_lines) ··· 1596 1592 state.DOCBLOCK: process_docblock, 1597 1593 } 1598 1594 1595 + def get_source(self): 1596 + """ 1597 + Return the file content of the lines handled by kernel-doc at the 1598 + latest parse_kdoc() run. 1599 + 1600 + Returns none if KernelDoc() was not initialized with store_src, 1601 + """ 1602 + return self.source 1603 + 1599 1604 def parse_kdoc(self): 1600 1605 """ 1601 1606 Open and process each line of a C source file. ··· 1618 1605 prev = "" 1619 1606 prev_ln = None 1620 1607 export_table = set() 1608 + self.source = [] 1609 + self.state = state.NORMAL 1621 1610 1622 1611 try: 1623 1612 with open(self.fname, "r", encoding="utf8", ··· 1646 1631 ln, state.name[self.state], 1647 1632 line) 1648 1633 1634 + prev_state = self.state 1635 + 1649 1636 # This is an optimization over the original script. 1650 1637 # There, when export_file was used for the same file, 1651 1638 # it was read twice. Here, we use the already-existing ··· 1657 1640 not self.process_export(export_table, line): 1658 1641 # Hand this line to the appropriate state handler 1659 1642 self.state_actions[self.state](self, ln, line) 1643 + 1644 + if self.store_src and prev_state != self.state or self.state != state.NORMAL: 1645 + if self.state == state.NAME: 1646 + # A "/**" was detected. Add a new source element 1647 + self.source.append({"ln": ln, "data": line + "\n"}) 1648 + else: 1649 + # Append to the existing one 1650 + self.source[-1]["data"] += line + "\n" 1660 1651 1661 1652 self.emit_unused_warnings() 1662 1653
+7 -2
tools/lib/python/kdoc/kdoc_re.py
··· 70 70 71 71 flags_name = " | ".join(flags) 72 72 73 + max_len = 60 74 + pattern = "" 75 + for pos in range(0, len(self.regex.pattern), max_len): 76 + pattern += '"' + self.regex.pattern[pos:max_len + pos] + '" ' 77 + 73 78 if flags_name: 74 - return f'KernRe("{self.regex.pattern}", {flags_name})' 79 + return f'KernRe({pattern}, {flags_name})' 75 80 else: 76 - return f'KernRe("{self.regex.pattern}")' 81 + return f'KernRe({pattern})' 77 82 78 83 def __add__(self, other): 79 84 """
+155
tools/lib/python/kdoc/kdoc_yaml_file.py
··· 1 + #!/usr/bin/env python3 2 + # SPDX-License-Identifier: GPL-2.0 3 + # Copyright(c) 2026: Mauro Carvalho Chehab <mchehab@kernel.org>. 4 + 5 + import os 6 + 7 + from kdoc.kdoc_output import ManFormat, RestFormat 8 + 9 + 10 + class KDocTestFile(): 11 + """ 12 + Handles the logic needed to store kernel‑doc output inside a YAML file. 13 + Useful for unit tests and regression tests. 14 + """ 15 + 16 + def __init__(self, config, yaml_file, yaml_content): 17 + # 18 + # Bail out early if yaml is not available 19 + # 20 + try: 21 + import yaml 22 + except ImportError: 23 + sys.exit("Warning: yaml package not available. Aborting it.") 24 + 25 + self.config = config 26 + self.test_file = os.path.expanduser(yaml_file) 27 + self.yaml_content = yaml_content 28 + 29 + self.tests = [] 30 + 31 + out_dir = os.path.dirname(self.test_file) 32 + if out_dir and not os.path.isdir(out_dir): 33 + sys.exit(f"Directory {out_dir} doesn't exist.") 34 + 35 + self.out_style = [] 36 + 37 + if "man" in self.yaml_content: 38 + out_style = ManFormat() 39 + out_style.set_config(self.config) 40 + 41 + self.out_style.append(out_style) 42 + 43 + if "rst" in self.yaml_content: 44 + out_style = RestFormat() 45 + out_style.set_config(self.config) 46 + 47 + self.out_style.append(out_style) 48 + 49 + def set_filter(self, export, internal, symbol, nosymbol, 50 + function_table, enable_lineno, no_doc_sections): 51 + """ 52 + Set filters at the output classes. 53 + """ 54 + for out_style in self.out_style: 55 + out_style.set_filter(export, internal, symbol, 56 + nosymbol, function_table, 57 + enable_lineno, no_doc_sections) 58 + 59 + @staticmethod 60 + def get_kdoc_item(arg, start_line=1): 61 + 62 + d = vars(arg) 63 + 64 + declaration_start_line = d.get("declaration_start_line") 65 + if not declaration_start_line: 66 + return d 67 + 68 + d["declaration_start_line"] = start_line 69 + 70 + parameterdesc_start_lines = d.get("parameterdesc_start_lines") 71 + if parameterdesc_start_lines: 72 + for key in parameterdesc_start_lines: 73 + ln = parameterdesc_start_lines[key] 74 + ln += start_line - declaration_start_line 75 + 76 + parameterdesc_start_lines[key] = ln 77 + 78 + sections_start_lines = d.get("sections_start_lines") 79 + if sections_start_lines: 80 + for key in sections_start_lines: 81 + ln = sections_start_lines[key] 82 + ln += start_line - declaration_start_line 83 + 84 + sections_start_lines[key] = ln 85 + 86 + return d 87 + 88 + def output_symbols(self, fname, symbols, source): 89 + """ 90 + Store source, symbols and output strings at self.tests. 91 + """ 92 + 93 + # 94 + # KdocItem needs to be converted into dicts 95 + # 96 + kdoc_item = [] 97 + expected = [] 98 + 99 + if not symbols and not source: 100 + return 101 + 102 + if not source or len(symbols) != len(source): 103 + print(f"Warning: lengths are different. Ignoring {fname}") 104 + 105 + # Folding without line numbers is too hard. 106 + # The right thing to do here to proceed would be to delete 107 + # not-handled source blocks, as len(source) should be bigger 108 + # than len(symbols) 109 + return 110 + 111 + base_name = "test_" + fname.replace(".", "_").replace("/", "_") 112 + expected_dict = {} 113 + start_line=1 114 + 115 + for i in range(0, len(symbols)): 116 + arg = symbols[i] 117 + 118 + if "KdocItem" in self.yaml_content: 119 + msg = self.get_kdoc_item(arg) 120 + 121 + expected_dict["kdoc_item"] = msg 122 + 123 + for out_style in self.out_style: 124 + if isinstance(out_style, ManFormat): 125 + key = "man" 126 + else: 127 + key = "rst" 128 + 129 + expected_dict[key]= out_style.output_symbols(fname, [arg]) 130 + 131 + name = f"{base_name}_{i:03d}" 132 + 133 + test = { 134 + "name": name, 135 + "description": f"{fname} line {source[i]["ln"]}", 136 + "fname": fname, 137 + "source": source[i]["data"], 138 + "expected": [expected_dict] 139 + } 140 + 141 + self.tests.append(test) 142 + 143 + expected_dict = {} 144 + 145 + def write(self): 146 + """ 147 + Output the content of self.tests to self.test_file. 148 + """ 149 + import yaml 150 + 151 + data = {"tests": self.tests} 152 + 153 + with open(self.test_file, "w", encoding="utf-8") as fp: 154 + yaml.safe_dump(data, fp, sort_keys=False, default_style="|", 155 + default_flow_style=False, allow_unicode=True)
+156
tools/unittests/kdoc-test-schema.yaml
··· 1 + # SPDX-License-Identifier: GPL-2.0 2 + # Copyright(c) 2026: Mauro Carvalho Chehab <mchehab@kernel.org>. 3 + 4 + # KDoc Test File Schema 5 + 6 + # This schema contains objects and properties needed to run kernel-doc 7 + # self-tests. 8 + 9 + $schema: "http://json-schema.org/draft-07/schema#" 10 + 11 + tests: 12 + type: array 13 + minItems: 1 14 + description: | 15 + A list of kernel-doc tests. 16 + 17 + properties: 18 + type: object 19 + properties: 20 + name: 21 + type: string 22 + description: | 23 + Test name. Should be an unique identifier within the schema. 24 + Don't prepend it with "test", as the dynamic test creation will 25 + do it. 26 + 27 + description: 28 + type: string 29 + description: | 30 + Test description 31 + 32 + source: 33 + type: string 34 + description: | 35 + C source code that should be parsed by kernel-doc. 36 + 37 + fname: 38 + type: string 39 + description: | 40 + The filename that contains the element. 41 + When placing real testcases, please use here the name of 42 + the C file (or header) from where the source code was picked. 43 + 44 + exports: 45 + type: array 46 + items: { type: string } 47 + description: | 48 + A list of export identifiers that are expected when parsing source. 49 + 50 + expected: 51 + type: array 52 + minItems: 1 53 + description: | 54 + A list of expected values. This list consists on objects to check 55 + both kdoc_parser and/or kdoc_output objects. 56 + 57 + items: 58 + type: object 59 + properties: 60 + # 61 + # kdoc_item 62 + # 63 + kdoc_item: 64 + type: object 65 + description: | 66 + Object expected to represent the C source code after parsed 67 + by tools/lib/python/kdoc/kdoc_parser.py KernelDoc class. 68 + See tools/lib/python/kdoc/kdoc_item.py for its contents. 69 + 70 + properties: 71 + name: 72 + type: string 73 + description: | 74 + The name of the identifier (function name, struct name, etc). 75 + type: 76 + type: string 77 + description: | 78 + Type of the object, as filled by kdoc_parser. can be: 79 + - enum 80 + - typedef 81 + - union 82 + - struct 83 + - var 84 + - function 85 + declaration_start_line: 86 + type: integer 87 + description: | 88 + The line number where the kernel-doc markup started. 89 + The first line of the code is line number 1. 90 + sections: 91 + type: object 92 + additionalProperties: { type: string } 93 + description: | 94 + Sections inside the kernel-doc markups: 95 + - "description" 96 + - "return" 97 + - any other part of the markup that starts with "something:" 98 + sections_start_lines: 99 + type: object 100 + additionalProperties: { type: integer } 101 + description: | 102 + a list of section names and the starting line of it. 103 + parameterlist: 104 + type: array 105 + items: { type: string } 106 + description: | 107 + Ordered list of parameter names. 108 + 109 + parameterdesc_start_lines: 110 + type: object 111 + additionalProperties: { type: integer } 112 + description: | 113 + Mapping from parameter name to the line where its 114 + description starts. 115 + parameterdescs: 116 + type: object 117 + additionalProperties: { type: string } 118 + description: | 119 + Mapping from parameter name to its description. 120 + 121 + parametertypes: 122 + type: object 123 + additionalProperties: { type: string } 124 + description: | 125 + Mapping from parameter name to its type. 126 + 127 + other_stuff: 128 + type: object 129 + additionalProperties: {} 130 + description: | 131 + Extra properties that will be stored at the item. 132 + Should match what kdoc_output expects. 133 + 134 + required: 135 + - name 136 + - type 137 + - declaration_start_line 138 + 139 + rst: 140 + type: string 141 + description: | 142 + The expected output for RestOutput class. 143 + 144 + man: 145 + type: string 146 + description: | 147 + The expected output for ManOutput class. 148 + 149 + anyOf: 150 + required: kdoc_item 151 + required: source 152 + 153 + required: 154 + - name 155 + - fname 156 + - expected
+154
tools/unittests/kdoc-test.yaml
··· 1 + # SPDX-License-Identifier: GPL-2.0 2 + # Copyright (c) 2026: Mauro Carvalho Chehab <mchehab@kernel.org> 3 + 4 + # Test cases for the dynamic tests. 5 + # Useful to test if kernel-doc classes are doing what it is expected. 6 + # 7 + 8 + tests: 9 + - name: func1 10 + fname: mock_functions.c 11 + description: "Simplest function test: do nothing, just rst output" 12 + 13 + source: | 14 + /** 15 + * func1 - Not exported function 16 + * @arg1: @arg1 does nothing 17 + * 18 + * Does nothing 19 + * 20 + * return: 21 + * always return 0. 22 + */ 23 + int func1(char *arg1) { return 0; }; 24 + 25 + 26 + expected: 27 + - rst: | 28 + .. c:function:: int func1 (char *arg1) 29 + 30 + Not exported function 31 + 32 + .. container:: kernelindent 33 + 34 + **Parameters** 35 + 36 + ``char *arg1`` 37 + **arg1** does nothing 38 + 39 + **Description** 40 + 41 + Does nothing 42 + 43 + **Return** 44 + 45 + always return 0. 46 + 47 + # TODO: how to handle timestamps at .TH? 48 + man: | 49 + .TH "func1" 9 "February 2026" "" "Kernel API Manual" 50 + .SH NAME 51 + func1 \- Not exported function 52 + .SH SYNOPSIS 53 + .B "int" func1 54 + .BI "(char *arg1 " ");" 55 + .SH ARGUMENTS 56 + .IP "arg1" 12 57 + \fIarg1\fP does nothing 58 + .SH "DESCRIPTION" 59 + Does nothing 60 + .SH "RETURN" 61 + always return 0. 62 + .SH "SEE ALSO" 63 + .PP 64 + Kernel file \fBmock_functions.c\fR 65 + 66 + - name: func2 67 + fname: func2.c 68 + description: Simple test with exports 69 + 70 + source: | 71 + /** 72 + * func2() - Exported function 73 + * @arg1: @arg1 does nothing 74 + * 75 + * Does nothing 76 + * 77 + * return: 78 + * always return 0. 79 + */ 80 + int func2(char *arg1) { return 0; }; 81 + EXPORT_SYMBOL(func2); 82 + 83 + exports: func2 84 + expected: 85 + - kdoc_item: 86 + name: func2 87 + type: function 88 + declaration_start_line: 1 89 + 90 + sections: 91 + Description: | 92 + Does nothing 93 + 94 + Return: | 95 + always return 0. 96 + 97 + sections_start_lines: 98 + Description: 3 99 + Return: 6 100 + 101 + parameterdescs: 102 + arg1: | 103 + @arg1 does nothing 104 + parameterlist: 105 + - arg1 106 + parameterdesc_start_lines: 107 + arg1: 2 108 + parametertypes: 109 + arg1: char *arg1 110 + 111 + other_stuff: 112 + func_macro: false 113 + functiontype: int 114 + purpose: "Exported function" 115 + typedef: false 116 + 117 + rst: | 118 + .. c:function:: int func2 (char *arg1) 119 + 120 + Exported function 121 + 122 + .. container:: kernelindent 123 + 124 + **Parameters** 125 + 126 + ``char *arg1`` 127 + **arg1** does nothing 128 + 129 + **Description** 130 + 131 + Does nothing 132 + 133 + **Return** 134 + 135 + always return 0. 136 + 137 + # TODO: how to handle timestamps at .TH? 138 + man: | 139 + .TH "func2" 9 "February 2026" "" "Kernel API Manual" 140 + .SH NAME 141 + func2 \- Exported function 142 + .SH SYNOPSIS 143 + .B "int" func2 144 + .BI "(char *arg1 " ");" 145 + .SH ARGUMENTS 146 + .IP "arg1" 12 147 + \fIarg1\fP does nothing 148 + .SH "DESCRIPTION" 149 + Does nothing 150 + .SH "RETURN" 151 + always return 0. 152 + .SH "SEE ALSO" 153 + .PP 154 + Kernel file \fBfunc2.c\fR
+534
tools/unittests/test_kdoc_parser.py
··· 1 + #!/usr/bin/env python3 2 + # SPDX-License-Identifier: GPL-2.0 3 + # Copyright(c) 2026: Mauro Carvalho Chehab <mchehab@kernel.org>. 4 + # 5 + # pylint: disable=C0200,C0413,W0102,R0914 6 + 7 + """ 8 + Unit tests for kernel-doc parser. 9 + """ 10 + 11 + import logging 12 + import os 13 + import re 14 + import shlex 15 + import sys 16 + import unittest 17 + 18 + from textwrap import dedent 19 + from unittest.mock import patch, MagicMock, mock_open 20 + 21 + import yaml 22 + 23 + SRC_DIR = os.path.dirname(os.path.realpath(__file__)) 24 + sys.path.insert(0, os.path.join(SRC_DIR, "../lib/python")) 25 + 26 + from kdoc.kdoc_files import KdocConfig 27 + from kdoc.kdoc_item import KdocItem 28 + from kdoc.kdoc_parser import KernelDoc 29 + from kdoc.kdoc_output import RestFormat, ManFormat 30 + 31 + from kdoc.xforms_lists import CTransforms 32 + 33 + from unittest_helper import run_unittest 34 + 35 + 36 + # 37 + # Test file 38 + # 39 + TEST_FILE = os.path.join(SRC_DIR, "kdoc-test.yaml") 40 + 41 + # 42 + # Ancillary logic to clean whitespaces 43 + # 44 + #: Regex to help cleaning whitespaces 45 + RE_WHITESPC = re.compile(r"[ \t]++") 46 + RE_BEGINSPC = re.compile(r"^\s+", re.MULTILINE) 47 + RE_ENDSPC = re.compile(r"\s+$", re.MULTILINE) 48 + 49 + def clean_whitespc(val, relax_whitespace=False): 50 + """ 51 + Cleanup whitespaces to avoid false positives. 52 + 53 + By default, strip only bein/end whitespaces, but, when relax_whitespace 54 + is true, also replace multiple whitespaces in the middle. 55 + """ 56 + 57 + if isinstance(val, str): 58 + val = val.strip() 59 + if relax_whitespace: 60 + val = RE_WHITESPC.sub(" ", val) 61 + val = RE_BEGINSPC.sub("", val) 62 + val = RE_ENDSPC.sub("", val) 63 + elif isinstance(val, list): 64 + val = [clean_whitespc(item, relax_whitespace) for item in val] 65 + elif isinstance(val, dict): 66 + val = {k: clean_whitespc(v, relax_whitespace) for k, v in val.items()} 67 + return val 68 + 69 + # 70 + # Helper classes to help mocking with logger and config 71 + # 72 + class MockLogging(logging.Handler): 73 + """ 74 + Simple class to store everything on a list 75 + """ 76 + 77 + def __init__(self, level=logging.NOTSET): 78 + super().__init__(level) 79 + self.messages = [] 80 + self.formatter = logging.Formatter() 81 + 82 + def emit(self, record: logging.LogRecord) -> None: 83 + """ 84 + Append a formatted record to self.messages. 85 + """ 86 + try: 87 + # The `format` method uses the handler's formatter. 88 + message = self.format(record) 89 + self.messages.append(message) 90 + except Exception: 91 + self.handleError(record) 92 + 93 + class MockKdocConfig(KdocConfig): 94 + def __init__(self, *args, **kwargs): 95 + super().__init__(*args, **kwargs) 96 + 97 + self.log = logging.getLogger(__file__) 98 + self.handler = MockLogging() 99 + self.log.addHandler(self.handler) 100 + 101 + def warning(self, msg): 102 + """Ancillary routine to output a warning and increment error count.""" 103 + 104 + self.log.warning(msg) 105 + 106 + # 107 + # Helper class to generate KdocItem and validate its contents 108 + # 109 + # TODO: check self.config.handler.messages content 110 + # 111 + class GenerateKdocItem(unittest.TestCase): 112 + """ 113 + Base class to run KernelDoc parser class 114 + """ 115 + 116 + DEFAULT = vars(KdocItem("", "", "", 0)) 117 + 118 + config = MockKdocConfig() 119 + xforms = CTransforms() 120 + 121 + def setUp(self): 122 + self.maxDiff = None 123 + 124 + def run_test(self, source, __expected_list, exports={}, fname="test.c", 125 + relax_whitespace=False): 126 + """ 127 + Stores expected values and patch the test to use source as 128 + a "file" input. 129 + """ 130 + debug_level = int(os.getenv("VERBOSE", "0")) 131 + source = dedent(source) 132 + 133 + # Ensure that default values will be there 134 + expected_list = [] 135 + for e in __expected_list: 136 + if not isinstance(e, dict): 137 + e = vars(e) 138 + 139 + new_e = self.DEFAULT.copy() 140 + new_e["fname"] = fname 141 + for key, value in e.items(): 142 + new_e[key] = value 143 + 144 + expected_list.append(new_e) 145 + 146 + patcher = patch('builtins.open', 147 + new_callable=mock_open, read_data=source) 148 + 149 + kernel_doc = KernelDoc(self.config, fname, self.xforms) 150 + 151 + with patcher: 152 + export_table, entries = kernel_doc.parse_kdoc() 153 + 154 + self.assertEqual(export_table, exports) 155 + self.assertEqual(len(entries), len(expected_list)) 156 + 157 + for i in range(0, len(entries)): 158 + 159 + entry = entries[i] 160 + expected = expected_list[i] 161 + self.assertNotEqual(expected, None) 162 + self.assertNotEqual(expected, {}) 163 + self.assertIsInstance(entry, KdocItem) 164 + 165 + d = vars(entry) 166 + for key, value in expected.items(): 167 + result = clean_whitespc(d[key], relax_whitespace) 168 + value = clean_whitespc(value, relax_whitespace) 169 + 170 + if debug_level > 1: 171 + sys.stderr.write(f"{key}: assert('{result}' == '{value}')\n") 172 + 173 + self.assertEqual(result, value, msg=f"at {key}") 174 + 175 + # 176 + # Ancillary function that replicates kdoc_files way to generate output 177 + # 178 + def cleanup_timestamp(text): 179 + lines = text.split("\n") 180 + 181 + for i, line in enumerate(lines): 182 + if not line.startswith('.TH'): 183 + continue 184 + 185 + parts = shlex.split(line) 186 + if len(parts) > 3: 187 + parts[3] = "" 188 + 189 + lines[i] = " ".join(parts) 190 + 191 + 192 + return "\n".join(lines) 193 + 194 + def gen_output(fname, out_style, symbols, expected, 195 + config=None, relax_whitespace=False): 196 + """ 197 + Use the output class to return an output content from KdocItem symbols. 198 + """ 199 + 200 + if not config: 201 + config = MockKdocConfig() 202 + 203 + out_style.set_config(config) 204 + 205 + msg = out_style.output_symbols(fname, symbols) 206 + 207 + result = clean_whitespc(msg, relax_whitespace) 208 + result = cleanup_timestamp(result) 209 + 210 + expected = clean_whitespc(expected, relax_whitespace) 211 + expected = cleanup_timestamp(expected) 212 + 213 + return result, expected 214 + 215 + # 216 + # Classes to be used by dynamic test generation from YAML 217 + # 218 + class CToKdocItem(GenerateKdocItem): 219 + def setUp(self): 220 + self.maxDiff = None 221 + 222 + def run_parser_test(self, source, symbols, exports, fname): 223 + if isinstance(symbols, dict): 224 + symbols = [symbols] 225 + 226 + if isinstance(exports, str): 227 + exports=set([exports]) 228 + elif isinstance(exports, list): 229 + exports=set(exports) 230 + 231 + self.run_test(source, symbols, exports=exports, 232 + fname=fname, relax_whitespace=True) 233 + 234 + class KdocItemToMan(unittest.TestCase): 235 + out_style = ManFormat() 236 + 237 + def setUp(self): 238 + self.maxDiff = None 239 + 240 + def run_out_test(self, fname, symbols, expected): 241 + """ 242 + Generate output using out_style, 243 + """ 244 + result, expected = gen_output(fname, self.out_style, 245 + symbols, expected) 246 + 247 + self.assertEqual(result, expected) 248 + 249 + class KdocItemToRest(unittest.TestCase): 250 + out_style = RestFormat() 251 + 252 + def setUp(self): 253 + self.maxDiff = None 254 + 255 + def run_out_test(self, fname, symbols, expected): 256 + """ 257 + Generate output using out_style, 258 + """ 259 + result, expected = gen_output(fname, self.out_style, symbols, 260 + expected, relax_whitespace=True) 261 + 262 + self.assertEqual(result, expected) 263 + 264 + 265 + class CToMan(unittest.TestCase): 266 + out_style = ManFormat() 267 + config = MockKdocConfig() 268 + xforms = CTransforms() 269 + 270 + def setUp(self): 271 + self.maxDiff = None 272 + 273 + def run_out_test(self, fname, source, expected): 274 + """ 275 + Generate output using out_style, 276 + """ 277 + patcher = patch('builtins.open', 278 + new_callable=mock_open, read_data=source) 279 + 280 + kernel_doc = KernelDoc(self.config, fname, self.xforms) 281 + 282 + with patcher: 283 + export_table, entries = kernel_doc.parse_kdoc() 284 + 285 + result, expected = gen_output(fname, self.out_style, 286 + entries, expected, config=self.config) 287 + 288 + self.assertEqual(result, expected) 289 + 290 + 291 + class CToRest(unittest.TestCase): 292 + out_style = RestFormat() 293 + config = MockKdocConfig() 294 + xforms = CTransforms() 295 + 296 + def setUp(self): 297 + self.maxDiff = None 298 + 299 + def run_out_test(self, fname, source, expected): 300 + """ 301 + Generate output using out_style, 302 + """ 303 + patcher = patch('builtins.open', 304 + new_callable=mock_open, read_data=source) 305 + 306 + kernel_doc = KernelDoc(self.config, fname, self.xforms) 307 + 308 + with patcher: 309 + export_table, entries = kernel_doc.parse_kdoc() 310 + 311 + result, expected = gen_output(fname, self.out_style, entries, 312 + expected, relax_whitespace=True, 313 + config=self.config) 314 + 315 + self.assertEqual(result, expected) 316 + 317 + 318 + # 319 + # Selftest class 320 + # 321 + class TestSelfValidate(GenerateKdocItem): 322 + """ 323 + Tests to check if logic inside GenerateKdocItem.run_test() is working. 324 + """ 325 + 326 + SOURCE = """ 327 + /** 328 + * function3: Exported function 329 + * @arg1: @arg1 does nothing 330 + * 331 + * Does nothing 332 + * 333 + * return: 334 + * always return 0. 335 + */ 336 + int function3(char *arg1) { return 0; }; 337 + EXPORT_SYMBOL(function3); 338 + """ 339 + 340 + EXPECTED = [{ 341 + 'name': 'function3', 342 + 'type': 'function', 343 + 'declaration_start_line': 2, 344 + 345 + 'sections_start_lines': { 346 + 'Description': 4, 347 + 'Return': 7, 348 + }, 349 + 'sections': { 350 + 'Description': 'Does nothing\n\n', 351 + 'Return': '\nalways return 0.\n' 352 + }, 353 + 354 + 'sections_start_lines': { 355 + 'Description': 4, 356 + 'Return': 7, 357 + }, 358 + 359 + 'parameterdescs': {'arg1': '@arg1 does nothing\n'}, 360 + 'parameterlist': ['arg1'], 361 + 'parameterdesc_start_lines': {'arg1': 3}, 362 + 'parametertypes': {'arg1': 'char *arg1'}, 363 + 364 + 'other_stuff': { 365 + 'func_macro': False, 366 + 'functiontype': 'int', 367 + 'purpose': 'Exported function', 368 + 'typedef': False 369 + }, 370 + }] 371 + 372 + EXPORTS = {"function3"} 373 + 374 + def test_parse_pass(self): 375 + """ 376 + Test if export_symbol is properly handled. 377 + """ 378 + self.run_test(self.SOURCE, self.EXPECTED, self.EXPORTS) 379 + 380 + @unittest.expectedFailure 381 + def test_no_exports(self): 382 + """ 383 + Test if export_symbol is properly handled. 384 + """ 385 + self.run_test(self.SOURCE, [], {}) 386 + 387 + @unittest.expectedFailure 388 + def test_with_empty_expected(self): 389 + """ 390 + Test if export_symbol is properly handled. 391 + """ 392 + self.run_test(self.SOURCE, [], self.EXPORTS) 393 + 394 + @unittest.expectedFailure 395 + def test_with_unfilled_expected(self): 396 + """ 397 + Test if export_symbol is properly handled. 398 + """ 399 + self.run_test(self.SOURCE, [{}], self.EXPORTS) 400 + 401 + @unittest.expectedFailure 402 + def test_with_default_expected(self): 403 + """ 404 + Test if export_symbol is properly handled. 405 + """ 406 + self.run_test(self.SOURCE, [self.DEFAULT.copy()], self.EXPORTS) 407 + 408 + # 409 + # Class and logic to create dynamic tests from YAML 410 + # 411 + 412 + class KernelDocDynamicTests(): 413 + """ 414 + Dynamically create a set of tests from a YAML file. 415 + """ 416 + 417 + @classmethod 418 + def create_parser_test(cls, name, fname, source, symbols, exports): 419 + """ 420 + Return a function that will be attached to the test class. 421 + """ 422 + def test_method(self): 423 + """Lambda-like function to run tests with provided vars""" 424 + self.run_parser_test(source, symbols, exports, fname) 425 + 426 + test_method.__name__ = f"test_gen_{name}" 427 + 428 + setattr(CToKdocItem, test_method.__name__, test_method) 429 + 430 + @classmethod 431 + def create_out_test(cls, name, fname, symbols, out_type, data): 432 + """ 433 + Return a function that will be attached to the test class. 434 + """ 435 + def test_method(self): 436 + """Lambda-like function to run tests with provided vars""" 437 + self.run_out_test(fname, symbols, data) 438 + 439 + test_method.__name__ = f"test_{out_type}_{name}" 440 + 441 + if out_type == "man": 442 + setattr(KdocItemToMan, test_method.__name__, test_method) 443 + else: 444 + setattr(KdocItemToRest, test_method.__name__, test_method) 445 + 446 + @classmethod 447 + def create_src2out_test(cls, name, fname, source, out_type, data): 448 + """ 449 + Return a function that will be attached to the test class. 450 + """ 451 + def test_method(self): 452 + """Lambda-like function to run tests with provided vars""" 453 + self.run_out_test(fname, source, data) 454 + 455 + test_method.__name__ = f"test_{out_type}_{name}" 456 + 457 + if out_type == "man": 458 + setattr(CToMan, test_method.__name__, test_method) 459 + else: 460 + setattr(CToRest, test_method.__name__, test_method) 461 + 462 + @classmethod 463 + def create_tests(cls): 464 + """ 465 + Iterate over all scenarios and add a method to the class for each. 466 + 467 + The logic in this function assumes a valid test that are compliant 468 + with kdoc-test-schema.yaml. There is an unit test to check that. 469 + As such, it picks mandatory values directly, and uses get() for the 470 + optional ones. 471 + """ 472 + 473 + with open(TEST_FILE, encoding="utf-8") as fp: 474 + testset = yaml.safe_load(fp) 475 + 476 + tests = testset["tests"] 477 + 478 + for idx, test in enumerate(tests): 479 + name = test["name"] 480 + fname = test["fname"] 481 + source = test["source"] 482 + expected_list = test["expected"] 483 + 484 + exports = test.get("exports", []) 485 + 486 + # 487 + # The logic below allows setting up to 5 types of test: 488 + # 1. from source to kdoc_item: test KernelDoc class; 489 + # 2. from kdoc_item to man: test ManOutput class; 490 + # 3. from kdoc_item to rst: test RestOutput class; 491 + # 4. from source to man without checking expected KdocItem; 492 + # 5. from source to rst without checking expected KdocItem. 493 + # 494 + for expected in expected_list: 495 + kdoc_item = expected.get("kdoc_item") 496 + man = expected.get("man", []) 497 + rst = expected.get("rst", []) 498 + 499 + if kdoc_item: 500 + if isinstance(kdoc_item, dict): 501 + kdoc_item = [kdoc_item] 502 + 503 + symbols = [] 504 + 505 + for arg in kdoc_item: 506 + arg["fname"] = fname 507 + arg["start_line"] = 1 508 + 509 + symbols.append(KdocItem.from_dict(arg)) 510 + 511 + if source: 512 + cls.create_parser_test(name, fname, source, 513 + symbols, exports) 514 + 515 + if man: 516 + cls.create_out_test(name, fname, symbols, "man", man) 517 + 518 + if rst: 519 + cls.create_out_test(name, fname, symbols, "rst", rst) 520 + 521 + elif source: 522 + if man: 523 + cls.create_src2out_test(name, fname, source, "man", man) 524 + 525 + if rst: 526 + cls.create_src2out_test(name, fname, source, "rst", rst) 527 + 528 + KernelDocDynamicTests.create_tests() 529 + 530 + # 531 + # Run all tests 532 + # 533 + if __name__ == "__main__": 534 + run_unittest(__file__)
+94
tools/unittests/test_kdoc_test_schema.py
··· 1 + #!/usr/bin/env python3 2 + # SPDX-License-Identifier: GPL-2.0 3 + """ 4 + Unit‑test driver for kernel‑doc YAML tests. 5 + 6 + Two kinds of tests are defined: 7 + 8 + * **Schema‑validation tests** – if ``jsonschema`` is available, the 9 + YAML files in this directory are validated against the JSON‑Schema 10 + described in ``kdoc-test-schema.yaml``. When the library is not 11 + present, a warning is emitted and the validation step is simply 12 + skipped – the dynamic kernel‑doc tests still run. 13 + 14 + * **Kernel‑doc tests** – dynamically generate one test method per 15 + scenario in ``kdoc-test.yaml``. Each method simply forwards 16 + the data to ``self.run_test`` – you only need to implement that 17 + helper in your own code. 18 + 19 + File names are kept as module‑level constants so that the 20 + implementation stays completely independent of ``pathlib``. 21 + """ 22 + 23 + import os 24 + import sys 25 + import warnings 26 + import yaml 27 + import unittest 28 + from typing import Any, Dict, List 29 + 30 + SRC_DIR = os.path.dirname(os.path.realpath(__file__)) 31 + sys.path.insert(0, os.path.join(SRC_DIR, "../lib/python")) 32 + 33 + from unittest_helper import run_unittest 34 + 35 + 36 + # 37 + # Files to read 38 + # 39 + BASE = os.path.realpath(os.path.dirname(__file__)) 40 + 41 + SCHEMA_FILE = os.path.join(BASE, "kdoc-test-schema.yaml") 42 + TEST_FILE = os.path.join(BASE, "kdoc-test.yaml") 43 + 44 + # 45 + # Schema‑validation test 46 + # 47 + class TestYAMLSchemaValidation(unittest.TestCase): 48 + """ 49 + Checks if TEST_FILE matches SCHEMA_FILE. 50 + """ 51 + 52 + @classmethod 53 + def setUpClass(cls): 54 + """ 55 + Import jsonschema if available. 56 + """ 57 + 58 + try: 59 + from jsonschema import Draft7Validator 60 + except ImportError: 61 + print("Warning: jsonschema package not available. Skipping schema validation") 62 + cls.validator = None 63 + return 64 + 65 + with open(SCHEMA_FILE, encoding="utf-8") as fp: 66 + cls.schema = yaml.safe_load(fp) 67 + 68 + cls.validator = Draft7Validator(cls.schema) 69 + 70 + def test_kdoc_test_yaml_followsschema(self): 71 + """ 72 + Run jsonschema validation if the validator is available. 73 + If not, emit a warning and return without failing. 74 + """ 75 + if self.validator is None: 76 + return 77 + 78 + with open(TEST_FILE, encoding="utf-8") as fp: 79 + data = yaml.safe_load(fp) 80 + 81 + errors = self.validator.iter_errors(data) 82 + 83 + msgs = [] 84 + for error in errors: 85 + msgs.append(error.message) 86 + 87 + if msgs: 88 + self.fail("Schema validation failed:\n\t" + "\n\t".join(msgs)) 89 + 90 + # -------------------------------------------------------------------- 91 + # Entry point 92 + # -------------------------------------------------------------------- 93 + if __name__ == "__main__": 94 + run_unittest(__file__)
+10 -3
tools/unittests/test_tokenizer.py
··· 46 46 # 47 47 # Check if logger is working 48 48 # 49 - if "log_level" in data: 50 - with self.assertLogs('kdoc.c_lex', level='ERROR') as cm: 49 + if "log_msg" in data: 50 + with self.assertLogs() as cm: 51 51 tokenizer = CTokenizer(data["source"]) 52 + 53 + msg_found = False 54 + for result in cm.output: 55 + if data["log_msg"] in result: 56 + msg_found = True 57 + 58 + self.assertTrue(msg_found, f"Missing log {data['log_msg']}") 52 59 53 60 return 54 61 ··· 131 124 132 125 "mismatch_error": { 133 126 "source": "int a$ = 5;", # $ is illegal 134 - "log_level": "ERROR", 127 + "log_msg": "Unexpected token", 135 128 }, 136 129 } 137 130