xref: /linux/tools/verification/rvgen/rvgen/kunit.py (revision b035a8be20ddfb932205beb9a1cbd80ea42e6cfb)
1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0-only
3#
4# Copyright (C) 2026-2029 Red Hat, Inc. Gabriele Monaco <gmonaco@redhat.com>
5#
6# Generator for runtime verification kunit files
7
8import re
9from pathlib import Path
10from . import generator
11
12
13class KUnitError(Exception):
14    """Exception raised for errors in KUnit generation and file handling."""
15
16
17class KUnit(generator.RVGenerator):
18    template_dir = ""
19
20    def __init__(self, extra_params={}):
21        super().__init__(extra_params)
22        self.local = extra_params.get("local", False)
23        self.kunit_c = self._read_template_file("kunit.c")
24        if not self.local:
25            self._fill_rv_kernel_dir()
26        try:
27            self.monitor_path = self.__find_monitor_c_file()
28            with open(self.monitor_path, 'r') as f:
29                self.content = f.read()
30        except OSError as e:
31            raise KUnitError(e) from e
32        self.monitor_class = self.__detect_monitor_class()
33
34    def _read_template_file(self, file):
35        if file in ("main.c", "Kconfig"):
36            return ""
37        return super()._read_template_file(file)
38
39    def __find_monitor_c_file(self) -> str:
40        """Look for the monitor file in the kernel tree or in the current folder."""
41        if not self.local:
42            path = Path(self.rv_dir) / "monitors" / self.name / f"{self.name}.c"
43            if path.exists():
44                return str(path)
45
46        path = Path(self.name) / f"{self.name}.c"
47        if path.exists():
48            return str(path)
49
50        raise FileNotFoundError(f"Could not find monitor C file for '{self.name}'")
51
52    def __extract_function_args(self, handler_name: str) -> str:
53        pattern = re.compile(
54            r'^\s*(.*?)\b' + re.escape(handler_name) + r'\(([^)]*)\)',
55            re.MULTILINE | re.DOTALL
56        )
57        match = pattern.search(self.content)
58        if not match:
59            return "/* XXX: fill handlers argument. */"
60
61        return match.group(2).strip()
62
63    def __parse_attach_handlers(self) -> list[str]:
64        """Find handlers by parsing when they are attached to tracepoints."""
65        probe_pattern = re.compile(
66            r'rv_attach_trace_probe\(.*, ([a-zA-Z0-9_]+)\)'
67        )
68        handlers = []
69        for match in probe_pattern.finditer(self.content):
70            handler = match.group(1)
71            if handler not in handlers:
72                handlers.append(handler)
73        return handlers
74
75    def __detect_monitor_class(self) -> str:
76        for c in ("da", "ha", "ltl"):
77            if f"{c}_monitor.h" in self.content:
78                return c
79        return "da"
80
81    def __fill_kunit_c(self, struct_name: str) -> str:
82        kunit_c = self.kunit_c
83        kunit_c = kunit_c.replace("%%MODEL_NAME%%", self.name)
84        kunit_c = kunit_c.replace("%%MODEL_NAME_UP%%", self.name.upper())
85        kunit_c = kunit_c.replace("%%MONITOR_CLASS%%", self.monitor_class)
86        kunit_c = kunit_c.replace("%%STRUCT_NAME%%", struct_name)
87        return kunit_c
88
89    def __fill_kunit_h(self, struct_name, prototypes) -> str:
90        return f"""/* SPDX-License-Identifier: GPL-2.0-only */
91/*
92 * Automatically generated by rvgen kunit.
93 * May need manual intervention for function prototypes that couldn't be
94 * found (e.g. are in another file) or variables to be exported.
95 */
96
97#ifndef __{self.name.upper()}_KUNIT_H
98#define __{self.name.upper()}_KUNIT_H
99
100#if IS_ENABLED(CONFIG_RV_MONITORS_KUNIT_TEST)
101
102#include <linux/rv.h>
103#include <rv/kunit.h>
104
105extern const struct {struct_name} {{
106\tstruct rv_kunit_mon mon;
107\t{"\n\t".join(prototypes)}
108}} {struct_name};
109#endif
110
111#endif /* __{self.name.upper()}_KUNIT_H */
112"""
113
114    def __fill_monitor_handlers(self, struct_name, assignments):
115        struct_definition = f"""#if IS_ENABLED(CONFIG_RV_MONITORS_KUNIT_TEST)
116#include <kunit/visibility.h>
117#include "{self.name}_kunit.h"
118
119const struct {struct_name} {struct_name} = {{
120\t.mon = RV_MON_OPS_INIT(),
121\t{"\n\t".join(assignments)}
122}};
123EXPORT_SYMBOL_IF_KUNIT({struct_name});
124#endif"""
125
126        if self.auto_patch:
127            try:
128                with open(self.monitor_path, 'w') as f:
129                    f.write(f"{self.content}\n{struct_definition}\n")
130            except OSError as e:
131                raise KUnitError(f"Error patching monitor file {self.monitor_path}: {e}") from e
132        else:
133            print(f"Append the following to {self.name}.c:\n")
134            print(struct_definition)
135        print("Now complete the test and add it to rv_monitors_test.c")
136
137    def print_files(self):
138
139        handlers = self.__parse_attach_handlers()
140
141        if not handlers:
142            raise KUnitError(f"No handlers found in {self.monitor_path}")
143
144        prototypes = []
145        assignments = []
146        for handler in handlers:
147            arguments = self.__extract_function_args(handler)
148
149            prototypes.append(f"void (*{handler})({arguments});")
150            assignments.append(f".{handler} = {handler},")
151
152        struct_name = f"rv_{self.name}_ops"
153
154        self.__fill_monitor_handlers(struct_name, assignments)
155
156        dir_path = Path(self.monitor_path).parent
157
158        header_file_path = dir_path / f"{self.name}_kunit.h"
159        kunit_c_file_path = dir_path / f"{self.name}_kunit.c"
160
161        use_backup = True
162        if header_file_path.exists() or kunit_c_file_path.exists():
163            try:
164                response = input("KUnit file(s) already exist. Backup? [Y/n] ")
165                if response.strip().lower() in ("n", "no"):
166                    use_backup = False
167            except EOFError:
168                print("Non-interactive session detected, backing up existing files.")
169        else:
170            use_backup = False
171
172        if use_backup:
173            for path in (header_file_path, kunit_c_file_path):
174                if path.exists():
175                    try:
176                        path.rename(path.with_suffix(path.suffix + ".old"))
177                    except OSError as e:
178                        raise KUnitError(f"Error backing up file {path}: {e}") from e
179
180        header_content = self.__fill_kunit_h(struct_name, prototypes)
181        try:
182            with open(header_file_path, 'w') as f:
183                f.write(header_content)
184            print(f"Successfully created KUnit header file: {header_file_path}")
185        except OSError as e:
186            raise KUnitError(f"Error writing to file {header_file_path}: {e}") from e
187
188        kunit_c_content = self.__fill_kunit_c(struct_name)
189        try:
190            with open(kunit_c_file_path, 'w') as f:
191                f.write(kunit_c_content)
192            print(f"Successfully created KUnit C file: {kunit_c_file_path}")
193        except OSError as e:
194            raise KUnitError(f"Error writing to file {kunit_c_file_path}: {e}") from e
195