1 // SPDX-License-Identifier: GPL-2.0 2 3 //! Generates KUnit tests from saved `rustdoc`-generated tests. 4 //! 5 //! KUnit passes a context (`struct kunit *`) to each test, which should be forwarded to the other 6 //! KUnit functions and macros. 7 //! 8 //! However, we want to keep this as an implementation detail because: 9 //! 10 //! - Test code should not care about the implementation. 11 //! 12 //! - Documentation looks worse if it needs to carry extra details unrelated to the piece 13 //! being described. 14 //! 15 //! - Test code should be able to define functions and call them, without having to carry 16 //! the context. 17 //! 18 //! - Later on, we may want to be able to test non-kernel code (e.g. `core` or third-party 19 //! crates) which likely use the standard library `assert*!` macros. 20 //! 21 //! For this reason, instead of the passed context, `kunit_get_current_test()` is used instead 22 //! (i.e. `current->kunit_test`). 23 //! 24 //! Note that this means other threads/tasks potentially spawned by a given test, if failing, will 25 //! report the failure in the kernel log but will not fail the actual test. Saving the pointer in 26 //! e.g. a `static` per test does not fully solve the issue either, because currently KUnit does 27 //! not support assertions (only expectations) from other tasks. Thus leave that feature for 28 //! the future, which simplifies the code here too. We could also simply not allow `assert`s in 29 //! other tasks, but that seems overly constraining, and we do want to support them, eventually. 30 31 use std::{ 32 fs, 33 fs::File, 34 io::{ 35 BufWriter, 36 Read, 37 Write, // 38 }, 39 path::{ 40 Path, 41 PathBuf, // 42 }, // 43 }; 44 45 /// Find the real path to the original file based on the `file` portion of the test name. 46 /// 47 /// `rustdoc` generated `file`s look like `sync_locked_by_rs`. Underscores (except the last one) 48 /// may represent an actual underscore in a directory/file, or a path separator. Thus the actual 49 /// file might be `sync_locked_by.rs`, `sync/locked_by.rs`, `sync_locked/by.rs` or 50 /// `sync/locked/by.rs`. This function walks the file system to determine which is the real one. 51 /// 52 /// This does require that ambiguities do not exist, but that seems fair, especially since this is 53 /// all supposed to be temporary until `rustdoc` gives us proper metadata to build this. If such 54 /// ambiguities are detected, they are diagnosed and the script panics. 55 fn find_real_path<'a>(srctree: &Path, valid_paths: &'a mut Vec<PathBuf>, file: &str) -> &'a str { 56 valid_paths.clear(); 57 58 let potential_components: Vec<&str> = file.strip_suffix("_rs").unwrap().split('_').collect(); 59 60 find_candidates(srctree, valid_paths, Path::new(""), &potential_components); 61 fn find_candidates( 62 srctree: &Path, 63 valid_paths: &mut Vec<PathBuf>, 64 prefix: &Path, 65 potential_components: &[&str], 66 ) { 67 // The base case: check whether all the potential components left, joined by underscores, 68 // is a file. 69 let joined_potential_components = potential_components.join("_") + ".rs"; 70 if srctree 71 .join("rust/kernel") 72 .join(prefix) 73 .join(&joined_potential_components) 74 .is_file() 75 { 76 // Avoid `srctree` here in order to keep paths relative to it in the KTAP output. 77 valid_paths.push( 78 Path::new("rust/kernel") 79 .join(prefix) 80 .join(joined_potential_components), 81 ); 82 } 83 84 // In addition, check whether each component prefix, joined by underscores, is a directory. 85 // If not, there is no need to check for combinations with that prefix. 86 for i in 1..potential_components.len() { 87 let (components_prefix, components_rest) = potential_components.split_at(i); 88 let prefix = prefix.join(components_prefix.join("_")); 89 if srctree.join("rust/kernel").join(&prefix).is_dir() { 90 find_candidates(srctree, valid_paths, &prefix, components_rest); 91 } 92 } 93 } 94 95 match valid_paths.as_slice() { 96 [] => panic!( 97 "No path candidates found for `{file}`. This is likely a bug in the build system, or \ 98 some files went away while compiling." 99 ), 100 [valid_path] => valid_path.to_str().unwrap(), 101 valid_paths => { 102 use std::fmt::Write; 103 104 let mut candidates = String::new(); 105 for path in valid_paths { 106 writeln!(&mut candidates, " {path:?}").unwrap(); 107 } 108 panic!( 109 "Several path candidates found for `{file}`, please resolve the ambiguity by \ 110 renaming a file or folder. Candidates:\n{candidates}", 111 ); 112 } 113 } 114 } 115 116 fn main() { 117 let srctree = std::env::var("srctree").unwrap(); 118 let srctree = Path::new(&srctree); 119 120 let mut paths = fs::read_dir("rust/test/doctests/kernel") 121 .unwrap() 122 .map(|entry| entry.unwrap().path()) 123 .collect::<Vec<_>>(); 124 125 // Sort paths. 126 paths.sort(); 127 128 let mut rust_tests = String::new(); 129 let mut c_test_declarations = String::new(); 130 let mut c_test_cases = String::new(); 131 let mut body = String::new(); 132 let mut last_file = String::new(); 133 let mut number = 0; 134 let mut valid_paths: Vec<PathBuf> = Vec::new(); 135 let mut real_path: &str = ""; 136 for path in paths { 137 // The `name` follows the `{file}_{line}_{number}` pattern (see description in 138 // `scripts/rustdoc_test_builder.rs`). Discard the `number`. 139 let name = path.file_name().unwrap().to_str().unwrap().to_string(); 140 141 // Extract the `file` and the `line`, discarding the `number`. 142 let (file, line) = name.rsplit_once('_').unwrap().0.rsplit_once('_').unwrap(); 143 144 // Generate an ID sequence ("test number") for each one in the file. 145 if file == last_file { 146 number += 1; 147 } else { 148 number = 0; 149 last_file = file.to_string(); 150 151 // Figure out the real path, only once per file. 152 real_path = find_real_path(srctree, &mut valid_paths, file); 153 } 154 155 // Generate a KUnit name (i.e. test name and C symbol) for this test. 156 // 157 // We avoid the line number, like `rustdoc` does, to make things slightly more stable for 158 // bisection purposes. However, to aid developers in mapping back what test failed, we will 159 // print a diagnostics line in the KTAP report. 160 let kunit_name = format!("rust_doctest_kernel_{file}_{number}"); 161 162 // Read the test's text contents to dump it below. 163 body.clear(); 164 File::open(path).unwrap().read_to_string(&mut body).unwrap(); 165 166 // Calculate how many lines before `main` function (including the `main` function line). 167 let body_offset = body 168 .lines() 169 .take_while(|line| !line.contains("fn main() {")) 170 .count() 171 + 1; 172 173 use std::fmt::Write; 174 write!( 175 rust_tests, 176 r#"/// Generated `{name}` KUnit test case from a Rust documentation test. 177 #[no_mangle] 178 pub extern "C" fn {kunit_name}(__kunit_test: *mut ::kernel::bindings::kunit) {{ 179 /// Overrides the usual [`assert!`] macro with one that calls KUnit instead. 180 #[allow(unused)] 181 macro_rules! assert {{ 182 ($cond:expr $(,)?) => {{{{ 183 ::kernel::kunit_assert!( 184 "{kunit_name}", c"{real_path}", __DOCTEST_ANCHOR - {line}, $cond 185 ); 186 }}}} 187 }} 188 189 /// Overrides the usual [`assert_eq!`] macro with one that calls KUnit instead. 190 #[allow(unused)] 191 macro_rules! assert_eq {{ 192 ($left:expr, $right:expr $(,)?) => {{{{ 193 ::kernel::kunit_assert_eq!( 194 "{kunit_name}", c"{real_path}", __DOCTEST_ANCHOR - {line}, $left, $right 195 ); 196 }}}} 197 }} 198 199 // Many tests need the prelude, so provide it by default. 200 #[allow(unused)] 201 use ::kernel::prelude::*; 202 203 // Unconditionally print the location of the original doctest (i.e. rather than the location in 204 // the generated file) so that developers can easily map the test back to the source code. 205 // 206 // This information is also printed when assertions fail, but this helps in the successful cases 207 // when the user is running KUnit manually, or when passing `--raw_output` to `kunit.py`. 208 // 209 // This follows the syntax for declaring test metadata in the proposed KTAP v2 spec, which may 210 // be used for the proposed KUnit test attributes API. Thus hopefully this will make migration 211 // easier later on. 212 ::kernel::kunit::info(fmt!(" # {kunit_name}.location: {real_path}:{line}\n")); 213 214 /// The anchor where the test code body starts. 215 #[allow(unused)] 216 static __DOCTEST_ANCHOR: i32 = ::core::line!() as i32 + {body_offset} + 2; 217 {{ 218 #![allow(unreachable_pub, clippy::disallowed_names)] 219 {body} 220 main(); 221 }} 222 }} 223 224 "# 225 ) 226 .unwrap(); 227 228 write!(c_test_declarations, "void {kunit_name}(struct kunit *);\n").unwrap(); 229 write!(c_test_cases, " KUNIT_CASE({kunit_name}),\n").unwrap(); 230 } 231 232 let rust_tests = rust_tests.trim(); 233 let c_test_declarations = c_test_declarations.trim(); 234 let c_test_cases = c_test_cases.trim(); 235 236 write!( 237 BufWriter::new(File::create("rust/doctests_kernel_generated.rs").unwrap()), 238 r#"//! `kernel` crate documentation tests. 239 240 const __LOG_PREFIX: &[u8] = b"rust_doctests_kernel\0"; 241 242 /// Dummy module type for doctest context. 243 struct LocalModule; 244 245 use kernel::{{ 246 str::CStr, 247 ModuleMetadata, 248 ThisModule, // 249 }}; 250 use core::ptr::null_mut; 251 252 impl ModuleMetadata for LocalModule {{ 253 const NAME: &'static CStr = c"rust_doctests_kernel"; 254 const THIS_MODULE: ThisModule = {{ 255 // SAFETY: `try_module_get`/`module_put` handle null module pointers gracefully. 256 unsafe {{ ThisModule::from_ptr(null_mut()) }} 257 }}; 258 }} 259 260 {rust_tests} 261 "# 262 ) 263 .unwrap(); 264 265 write!( 266 BufWriter::new(File::create("rust/doctests_kernel_generated_kunit.c").unwrap()), 267 r#"/* 268 * `kernel` crate documentation tests. 269 */ 270 271 #include <kunit/test.h> 272 273 {c_test_declarations} 274 275 static struct kunit_case test_cases[] = {{ 276 {c_test_cases} 277 {{ }} 278 }}; 279 280 static struct kunit_suite test_suite = {{ 281 .name = "rust_doctests_kernel", 282 .test_cases = test_cases, 283 }}; 284 285 kunit_test_suite(test_suite); 286 287 MODULE_LICENSE("GPL"); 288 "# 289 ) 290 .unwrap(); 291 } 292