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 tests = String::new(); 129 let mut body = String::new(); 130 let mut last_file = String::new(); 131 let mut number = 0; 132 let mut valid_paths: Vec<PathBuf> = Vec::new(); 133 let mut real_path: &str = ""; 134 for path in paths { 135 // The `name` follows the `{file}_{line}_{number}` pattern (see description in 136 // `scripts/rustdoc_test_builder.rs`). Discard the `number`. 137 let name = path.file_name().unwrap().to_str().unwrap().to_string(); 138 139 // Extract the `file` and the `line`, discarding the `number`. 140 let (file, line) = name.rsplit_once('_').unwrap().0.rsplit_once('_').unwrap(); 141 142 // Generate an ID sequence ("test number") for each one in the file. 143 if file == last_file { 144 number += 1; 145 } else { 146 number = 0; 147 last_file = file.to_string(); 148 149 // Figure out the real path, only once per file. 150 real_path = find_real_path(srctree, &mut valid_paths, file); 151 } 152 153 // Generate a KUnit name (i.e. test name and C symbol) for this test. 154 // 155 // We avoid the line number, like `rustdoc` does, to make things slightly more stable for 156 // bisection purposes. However, to aid developers in mapping back what test failed, we will 157 // print a diagnostics line in the KTAP report. 158 let kunit_name = format!("rust_doctest_kernel_{file}_{number}"); 159 160 // Read the test's text contents to dump it below. 161 body.clear(); 162 File::open(path).unwrap().read_to_string(&mut body).unwrap(); 163 164 // Calculate how many lines before `main` function (including the `main` function line). 165 let body_offset = body 166 .lines() 167 .take_while(|line| !line.contains("fn main() {")) 168 .count() 169 + 1; 170 171 use std::fmt::Write; 172 write!( 173 tests, 174 r#"/// Generated `{name}` KUnit test case from a Rust documentation test. 175 #[test] 176 fn {kunit_name}() {{ 177 /// Overrides the usual [`file!`] macro with one that expands to the real path. 178 #[allow(unused)] 179 macro_rules! file {{ 180 () => {{ "{real_path}" }} 181 }} 182 183 /// Overrides the usual [`line!`] macro with one that expands to the real line number. 184 #[allow(unused)] 185 macro_rules! line {{ 186 // NOTE: This does not expand to a literal, but a constant expression. 187 // Therefore code that depends on `line!()` expanding to a literal needs special adjustment. 188 () => {{ const {{ ::core::line!() - __DOCTEST_ANCHOR + {line} }} }} 189 }} 190 191 // Many tests need the prelude, so provide it by default. 192 #[allow(unused)] 193 use ::kernel::prelude::*; 194 195 // Unconditionally print the location of the original doctest (i.e. rather than the location in 196 // the generated file) so that developers can easily map the test back to the source code. 197 // 198 // This information is also printed when assertions fail, but this helps in the successful cases 199 // when the user is running KUnit manually, or when passing `--raw_output` to `kunit.py`. 200 // 201 // This follows the syntax for declaring test metadata in the proposed KTAP v2 spec, which may 202 // be used for the proposed KUnit test attributes API. Thus hopefully this will make migration 203 // easier later on. 204 ::kernel::kunit::info(fmt!(" # {kunit_name}.location: {real_path}:{line}\n")); 205 206 /// The anchor where the test code body starts. 207 #[allow(unused)] 208 static __DOCTEST_ANCHOR: u32 = ::core::line!() + {body_offset} + 2; 209 {{ 210 #![allow(unreachable_pub, clippy::disallowed_names)] 211 {body} 212 main(); 213 }} 214 }} 215 216 "# 217 ) 218 .unwrap(); 219 } 220 221 let tests = tests.trim(); 222 223 write!( 224 BufWriter::new(File::create("rust/doctests_kernel_generated.rs").unwrap()), 225 r#"//! `kernel` crate documentation tests. 226 227 const __LOG_PREFIX: &[u8] = b"rust_doctests_kernel\0"; 228 229 /// Dummy module type for doctest context. 230 struct LocalModule; 231 232 use kernel::{{ 233 str::CStr, 234 ModuleMetadata, 235 ThisModule, // 236 }}; 237 use core::ptr::null_mut; 238 239 impl ModuleMetadata for LocalModule {{ 240 const NAME: &'static CStr = c"rust_doctests_kernel"; 241 const THIS_MODULE: ThisModule = {{ 242 // SAFETY: `try_module_get`/`module_put` handle null module pointers gracefully. 243 unsafe {{ ThisModule::from_ptr(null_mut()) }} 244 }}; 245 }} 246 247 #[kernel::macros::kunit_tests(rust_doctests_kernel)] 248 mod tests {{ 249 {tests} 250 }} 251 "# 252 ) 253 .unwrap(); 254 } 255