Brian Silverman | 4e662aa | 2022-05-11 23:10:19 -0700 | [diff] [blame] | 1 | // Copyright 2022 Google LLC |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or |
| 4 | // https://www.apache.org/licenses/LICENSE-2.0> or the MIT license |
| 5 | // <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your |
| 6 | // option. This file may not be copied, modified, or distributed |
| 7 | // except according to those terms. |
| 8 | |
| 9 | use std::{ |
| 10 | ffi::OsStr, |
| 11 | fs::File, |
| 12 | io::{Read, Write}, |
| 13 | panic::RefUnwindSafe, |
| 14 | path::{Path, PathBuf}, |
| 15 | sync::Mutex, |
| 16 | }; |
| 17 | |
| 18 | use autocxx_engine::{ |
| 19 | Builder, BuilderBuild, BuilderContext, BuilderError, RebuildDependencyRecorder, HEADER, |
| 20 | }; |
| 21 | use log::info; |
| 22 | use once_cell::sync::OnceCell; |
| 23 | use proc_macro2::{Span, TokenStream}; |
Brian Silverman | f3ec38b | 2022-07-06 20:43:36 -0700 | [diff] [blame] | 24 | use quote::{format_ident, quote, TokenStreamExt}; |
Brian Silverman | 4e662aa | 2022-05-11 23:10:19 -0700 | [diff] [blame] | 25 | use syn::Token; |
| 26 | use tempfile::{tempdir, TempDir}; |
| 27 | |
| 28 | const KEEP_TEMPDIRS: bool = false; |
| 29 | |
| 30 | /// API to run a documentation test. Panics if the test fails. |
| 31 | /// Guarantees not to emit anything to stdout and so can be run in an mdbook context. |
| 32 | pub fn doctest( |
| 33 | cxx_code: &str, |
| 34 | header_code: &str, |
| 35 | rust_code: TokenStream, |
| 36 | manifest_dir: &OsStr, |
| 37 | ) -> Result<(), TestError> { |
| 38 | std::env::set_var("CARGO_PKG_NAME", "autocxx-integration-tests"); |
| 39 | std::env::set_var("CARGO_MANIFEST_DIR", manifest_dir); |
| 40 | do_run_test_manual(cxx_code, header_code, rust_code, None, None) |
| 41 | } |
| 42 | |
| 43 | fn configure_builder(b: &mut BuilderBuild) -> &mut BuilderBuild { |
| 44 | let target = rust_info::get().target_triple.unwrap(); |
| 45 | b.host(&target) |
| 46 | .target(&target) |
| 47 | .opt_level(1) |
| 48 | .flag("-std=c++14") // For clang |
| 49 | .flag_if_supported("/GX") // Enable C++ exceptions for msvc |
| 50 | .flag_if_supported("-Wall") |
| 51 | .flag_if_supported("-Werror") |
| 52 | } |
| 53 | |
| 54 | /// What environment variables we should set in order to tell rustc how to find |
| 55 | /// the Rust code. |
| 56 | pub enum RsFindMode { |
| 57 | AutocxxRs, |
| 58 | AutocxxRsArchive, |
Brian Silverman | f3ec38b | 2022-07-06 20:43:36 -0700 | [diff] [blame] | 59 | AutocxxRsFile, |
Brian Silverman | 021ead3 | 2022-08-14 14:04:05 -0700 | [diff] [blame] | 60 | /// This just calls the callback instead of setting any environment variables. The callback |
| 61 | /// receives the path to the temporary directory. |
| 62 | Custom(Box<dyn FnOnce(&Path)>), |
Brian Silverman | 4e662aa | 2022-05-11 23:10:19 -0700 | [diff] [blame] | 63 | } |
| 64 | |
| 65 | /// API to test building pre-generated files. |
| 66 | pub fn build_from_folder( |
| 67 | folder: &Path, |
| 68 | main_rs_file: &Path, |
| 69 | generated_rs_files: Vec<PathBuf>, |
| 70 | cpp_files: &[&str], |
| 71 | rs_find_mode: RsFindMode, |
| 72 | ) -> Result<(), TestError> { |
| 73 | let target_dir = folder.join("target"); |
| 74 | std::fs::create_dir(&target_dir).unwrap(); |
| 75 | let mut b = BuilderBuild::new(); |
| 76 | for cpp_file in cpp_files.iter() { |
| 77 | b.file(folder.join(cpp_file)); |
| 78 | } |
| 79 | configure_builder(&mut b) |
| 80 | .out_dir(&target_dir) |
| 81 | .include(folder) |
| 82 | .include(folder.join("demo")) |
| 83 | .try_compile("autocxx-demo") |
| 84 | .map_err(TestError::CppBuild)?; |
| 85 | // use the trybuild crate to build the Rust file. |
| 86 | let r = get_builder().lock().unwrap().build( |
| 87 | &target_dir, |
| 88 | "autocxx-demo", |
| 89 | &folder, |
| 90 | &["input.h", "cxx.h"], |
| 91 | &main_rs_file, |
| 92 | generated_rs_files, |
| 93 | rs_find_mode, |
| 94 | ); |
| 95 | if r.is_err() { |
| 96 | return Err(TestError::RsBuild); // details of Rust panic are a bit messy to include, and |
| 97 | // not important at the moment. |
| 98 | } |
| 99 | Ok(()) |
| 100 | } |
| 101 | |
| 102 | fn get_builder() -> &'static Mutex<LinkableTryBuilder> { |
| 103 | static INSTANCE: OnceCell<Mutex<LinkableTryBuilder>> = OnceCell::new(); |
| 104 | INSTANCE.get_or_init(|| Mutex::new(LinkableTryBuilder::new())) |
| 105 | } |
| 106 | |
| 107 | /// TryBuild which maintains a directory of libraries to link. |
| 108 | /// This is desirable because otherwise, if we alter the RUSTFLAGS |
| 109 | /// then trybuild rebuilds *everything* including all the dev-dependencies. |
| 110 | /// This object exists purely so that we use the same RUSTFLAGS for every |
| 111 | /// test case. |
| 112 | struct LinkableTryBuilder { |
| 113 | /// Directory in which we'll keep any linkable libraries |
| 114 | temp_dir: TempDir, |
| 115 | } |
| 116 | |
| 117 | impl LinkableTryBuilder { |
| 118 | fn new() -> Self { |
| 119 | LinkableTryBuilder { |
| 120 | temp_dir: tempdir().unwrap(), |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | fn move_items_into_temp_dir<P1: AsRef<Path>>(&self, src_path: &P1, pattern: &str) { |
| 125 | for item in std::fs::read_dir(src_path).unwrap() { |
| 126 | let item = item.unwrap(); |
| 127 | if item.file_name().into_string().unwrap().contains(pattern) { |
| 128 | let dest = self.temp_dir.path().join(item.file_name()); |
| 129 | if dest.exists() { |
| 130 | std::fs::remove_file(&dest).unwrap(); |
| 131 | } |
| 132 | if KEEP_TEMPDIRS { |
| 133 | std::fs::copy(item.path(), dest).unwrap(); |
| 134 | } else { |
| 135 | std::fs::rename(item.path(), dest).unwrap(); |
| 136 | } |
| 137 | } |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | #[allow(clippy::too_many_arguments)] |
| 142 | fn build<P1: AsRef<Path>, P2: AsRef<Path>, P3: AsRef<Path> + RefUnwindSafe>( |
| 143 | &self, |
| 144 | library_path: &P1, |
| 145 | library_name: &str, |
| 146 | header_path: &P2, |
| 147 | header_names: &[&str], |
| 148 | rs_path: &P3, |
| 149 | generated_rs_files: Vec<PathBuf>, |
| 150 | rs_find_mode: RsFindMode, |
| 151 | ) -> std::thread::Result<()> { |
| 152 | // Copy all items from the source dir into our temporary dir if their name matches |
| 153 | // the pattern given in `library_name`. |
| 154 | self.move_items_into_temp_dir(library_path, library_name); |
| 155 | for header_name in header_names { |
| 156 | self.move_items_into_temp_dir(header_path, header_name); |
| 157 | } |
| 158 | for generated_rs in generated_rs_files { |
| 159 | self.move_items_into_temp_dir( |
| 160 | &generated_rs.parent().unwrap(), |
| 161 | generated_rs.file_name().unwrap().to_str().unwrap(), |
| 162 | ); |
| 163 | } |
| 164 | let temp_path = self.temp_dir.path().to_str().unwrap(); |
| 165 | let mut rustflags = format!("-L {}", temp_path); |
| 166 | if std::env::var_os("AUTOCXX_ASAN").is_some() { |
| 167 | rustflags.push_str(" -Z sanitizer=address -Clinker=clang++ -Clink-arg=-fuse-ld=lld"); |
| 168 | } |
| 169 | std::env::set_var("RUSTFLAGS", rustflags); |
| 170 | match rs_find_mode { |
| 171 | RsFindMode::AutocxxRs => std::env::set_var("AUTOCXX_RS", temp_path), |
| 172 | RsFindMode::AutocxxRsArchive => std::env::set_var( |
| 173 | "AUTOCXX_RS_JSON_ARCHIVE", |
| 174 | self.temp_dir.path().join("gen.rs.json"), |
| 175 | ), |
Brian Silverman | f3ec38b | 2022-07-06 20:43:36 -0700 | [diff] [blame] | 176 | RsFindMode::AutocxxRsFile => std::env::set_var( |
| 177 | "AUTOCXX_RS_FILE", |
| 178 | self.temp_dir.path().join("gen0.include.rs"), |
| 179 | ), |
Brian Silverman | 021ead3 | 2022-08-14 14:04:05 -0700 | [diff] [blame] | 180 | RsFindMode::Custom(f) => f(self.temp_dir.path()), |
Brian Silverman | 4e662aa | 2022-05-11 23:10:19 -0700 | [diff] [blame] | 181 | }; |
| 182 | std::panic::catch_unwind(|| { |
| 183 | let test_cases = trybuild::TestCases::new(); |
| 184 | test_cases.pass(rs_path) |
| 185 | }) |
| 186 | } |
| 187 | } |
| 188 | |
| 189 | fn write_to_file(tdir: &TempDir, filename: &str, content: &str) -> PathBuf { |
| 190 | let path = tdir.path().join(filename); |
| 191 | let mut f = File::create(&path).unwrap(); |
| 192 | f.write_all(content.as_bytes()).unwrap(); |
| 193 | path |
| 194 | } |
| 195 | |
| 196 | /// A positive test, we expect to pass. |
| 197 | pub fn run_test( |
| 198 | cxx_code: &str, |
| 199 | header_code: &str, |
| 200 | rust_code: TokenStream, |
| 201 | generate: &[&str], |
| 202 | generate_pods: &[&str], |
| 203 | ) { |
| 204 | do_run_test( |
| 205 | cxx_code, |
| 206 | header_code, |
| 207 | rust_code, |
| 208 | directives_from_lists(generate, generate_pods, None), |
| 209 | None, |
| 210 | None, |
| 211 | None, |
Brian Silverman | f3ec38b | 2022-07-06 20:43:36 -0700 | [diff] [blame] | 212 | "unsafe_ffi", |
Brian Silverman | 4e662aa | 2022-05-11 23:10:19 -0700 | [diff] [blame] | 213 | ) |
| 214 | .unwrap() |
| 215 | } |
| 216 | |
| 217 | // A trait for objects which can check the output of the code creation |
| 218 | // process. |
| 219 | pub trait CodeCheckerFns { |
| 220 | fn check_rust(&self, _rs: syn::File) -> Result<(), TestError> { |
| 221 | Ok(()) |
| 222 | } |
| 223 | fn check_cpp(&self, _cpp: &[PathBuf]) -> Result<(), TestError> { |
| 224 | Ok(()) |
| 225 | } |
| 226 | fn skip_build(&self) -> bool { |
| 227 | false |
| 228 | } |
| 229 | } |
| 230 | |
| 231 | // A function applied to the resultant generated Rust code |
| 232 | // which can be used to inspect that code. |
| 233 | pub type CodeChecker = Box<dyn CodeCheckerFns>; |
| 234 | |
| 235 | // A trait for objects which can modify builders for testing purposes. |
| 236 | pub trait BuilderModifierFns { |
| 237 | fn modify_autocxx_builder<'a>( |
| 238 | &self, |
| 239 | builder: Builder<'a, TestBuilderContext>, |
| 240 | ) -> Builder<'a, TestBuilderContext>; |
| 241 | fn modify_cc_builder<'a>(&self, builder: &'a mut cc::Build) -> &'a mut cc::Build { |
| 242 | builder |
| 243 | } |
| 244 | } |
| 245 | |
| 246 | pub type BuilderModifier = Box<dyn BuilderModifierFns>; |
| 247 | |
| 248 | /// A positive test, we expect to pass. |
| 249 | #[allow(clippy::too_many_arguments)] // least typing for each test |
| 250 | pub fn run_test_ex( |
| 251 | cxx_code: &str, |
| 252 | header_code: &str, |
| 253 | rust_code: TokenStream, |
| 254 | directives: TokenStream, |
| 255 | builder_modifier: Option<BuilderModifier>, |
| 256 | code_checker: Option<CodeChecker>, |
| 257 | extra_rust: Option<TokenStream>, |
| 258 | ) { |
| 259 | do_run_test( |
| 260 | cxx_code, |
| 261 | header_code, |
| 262 | rust_code, |
| 263 | directives, |
| 264 | builder_modifier, |
| 265 | code_checker, |
| 266 | extra_rust, |
Brian Silverman | f3ec38b | 2022-07-06 20:43:36 -0700 | [diff] [blame] | 267 | "unsafe_ffi", |
Brian Silverman | 4e662aa | 2022-05-11 23:10:19 -0700 | [diff] [blame] | 268 | ) |
| 269 | .unwrap() |
| 270 | } |
| 271 | |
Brian Silverman | f3ec38b | 2022-07-06 20:43:36 -0700 | [diff] [blame] | 272 | pub fn run_generate_all_test(header_code: &str) { |
| 273 | run_test_ex( |
| 274 | "", |
| 275 | header_code, |
| 276 | quote! {}, |
| 277 | quote! { generate_all!() }, |
| 278 | None, |
| 279 | None, |
| 280 | None, |
| 281 | ); |
| 282 | } |
| 283 | |
Brian Silverman | 4e662aa | 2022-05-11 23:10:19 -0700 | [diff] [blame] | 284 | pub fn run_test_expect_fail( |
| 285 | cxx_code: &str, |
| 286 | header_code: &str, |
| 287 | rust_code: TokenStream, |
| 288 | generate: &[&str], |
| 289 | generate_pods: &[&str], |
| 290 | ) { |
| 291 | do_run_test( |
| 292 | cxx_code, |
| 293 | header_code, |
| 294 | rust_code, |
| 295 | directives_from_lists(generate, generate_pods, None), |
| 296 | None, |
| 297 | None, |
| 298 | None, |
Brian Silverman | f3ec38b | 2022-07-06 20:43:36 -0700 | [diff] [blame] | 299 | "unsafe_ffi", |
Brian Silverman | 4e662aa | 2022-05-11 23:10:19 -0700 | [diff] [blame] | 300 | ) |
| 301 | .expect_err("Unexpected success"); |
| 302 | } |
| 303 | |
| 304 | pub fn run_test_expect_fail_ex( |
| 305 | cxx_code: &str, |
| 306 | header_code: &str, |
| 307 | rust_code: TokenStream, |
| 308 | directives: TokenStream, |
| 309 | builder_modifier: Option<BuilderModifier>, |
| 310 | code_checker: Option<CodeChecker>, |
| 311 | extra_rust: Option<TokenStream>, |
| 312 | ) { |
| 313 | do_run_test( |
| 314 | cxx_code, |
| 315 | header_code, |
| 316 | rust_code, |
| 317 | directives, |
| 318 | builder_modifier, |
| 319 | code_checker, |
| 320 | extra_rust, |
Brian Silverman | f3ec38b | 2022-07-06 20:43:36 -0700 | [diff] [blame] | 321 | "unsafe_ffi", |
Brian Silverman | 4e662aa | 2022-05-11 23:10:19 -0700 | [diff] [blame] | 322 | ) |
| 323 | .expect_err("Unexpected success"); |
| 324 | } |
| 325 | |
| 326 | /// In the future maybe the tests will distinguish the exact type of failure expected. |
| 327 | #[derive(Debug)] |
| 328 | pub enum TestError { |
| 329 | AutoCxx(BuilderError), |
| 330 | CppBuild(cc::Error), |
| 331 | RsBuild, |
| 332 | NoRs, |
| 333 | RsFileOpen(std::io::Error), |
| 334 | RsFileRead(std::io::Error), |
| 335 | RsFileParse(syn::Error), |
| 336 | RsCodeExaminationFail(String), |
| 337 | CppCodeExaminationFail, |
| 338 | } |
| 339 | |
| 340 | pub fn directives_from_lists( |
| 341 | generate: &[&str], |
| 342 | generate_pods: &[&str], |
| 343 | extra_directives: Option<TokenStream>, |
| 344 | ) -> TokenStream { |
| 345 | let generate = generate.iter().map(|s| { |
| 346 | quote! { |
| 347 | generate!(#s) |
| 348 | } |
| 349 | }); |
| 350 | let generate_pods = generate_pods.iter().map(|s| { |
| 351 | quote! { |
| 352 | generate_pod!(#s) |
| 353 | } |
| 354 | }); |
| 355 | quote! { |
| 356 | #(#generate)* |
| 357 | #(#generate_pods)* |
| 358 | #extra_directives |
| 359 | } |
| 360 | } |
| 361 | |
| 362 | #[allow(clippy::too_many_arguments)] // least typing for each test |
| 363 | pub fn do_run_test( |
| 364 | cxx_code: &str, |
| 365 | header_code: &str, |
| 366 | rust_code: TokenStream, |
| 367 | directives: TokenStream, |
| 368 | builder_modifier: Option<BuilderModifier>, |
| 369 | rust_code_checker: Option<CodeChecker>, |
| 370 | extra_rust: Option<TokenStream>, |
Brian Silverman | f3ec38b | 2022-07-06 20:43:36 -0700 | [diff] [blame] | 371 | safety_policy: &str, |
Brian Silverman | 4e662aa | 2022-05-11 23:10:19 -0700 | [diff] [blame] | 372 | ) -> Result<(), TestError> { |
| 373 | let hexathorpe = Token); |
Brian Silverman | f3ec38b | 2022-07-06 20:43:36 -0700 | [diff] [blame] | 374 | let safety_policy = format_ident!("{}", safety_policy); |
Brian Silverman | 4e662aa | 2022-05-11 23:10:19 -0700 | [diff] [blame] | 375 | let unexpanded_rust = quote! { |
| 376 | use autocxx::prelude::*; |
| 377 | |
| 378 | include_cpp!( |
| 379 | #hexathorpe include "input.h" |
Brian Silverman | f3ec38b | 2022-07-06 20:43:36 -0700 | [diff] [blame] | 380 | safety!(#safety_policy) |
Brian Silverman | 4e662aa | 2022-05-11 23:10:19 -0700 | [diff] [blame] | 381 | #directives |
| 382 | ); |
| 383 | |
| 384 | #extra_rust |
| 385 | |
| 386 | fn main() { |
| 387 | #rust_code |
| 388 | } |
| 389 | |
| 390 | }; |
| 391 | do_run_test_manual( |
| 392 | cxx_code, |
| 393 | header_code, |
| 394 | unexpanded_rust, |
| 395 | builder_modifier, |
| 396 | rust_code_checker, |
| 397 | ) |
| 398 | } |
| 399 | |
| 400 | /// The [`BuilderContext`] used in autocxx's integration tests. |
| 401 | pub struct TestBuilderContext; |
| 402 | |
| 403 | impl BuilderContext for TestBuilderContext { |
| 404 | fn get_dependency_recorder() -> Option<Box<dyn RebuildDependencyRecorder>> { |
| 405 | None |
| 406 | } |
| 407 | } |
| 408 | |
| 409 | pub fn do_run_test_manual( |
| 410 | cxx_code: &str, |
| 411 | header_code: &str, |
| 412 | mut rust_code: TokenStream, |
| 413 | builder_modifier: Option<BuilderModifier>, |
| 414 | rust_code_checker: Option<CodeChecker>, |
| 415 | ) -> Result<(), TestError> { |
| 416 | const HEADER_NAME: &str = "input.h"; |
| 417 | // Step 2: Write the C++ header snippet to a temp file |
| 418 | let tdir = tempdir().unwrap(); |
| 419 | write_to_file( |
| 420 | &tdir, |
| 421 | HEADER_NAME, |
| 422 | &format!("#pragma once\n{}", header_code), |
| 423 | ); |
| 424 | write_to_file(&tdir, "cxx.h", HEADER); |
| 425 | |
| 426 | rust_code.append_all(quote! { |
| 427 | #[link(name="autocxx-demo")] |
| 428 | extern {} |
| 429 | }); |
| 430 | info!("Unexpanded Rust: {}", rust_code); |
| 431 | |
| 432 | let write_rust_to_file = |ts: &TokenStream| -> PathBuf { |
| 433 | // Step 3: Write the Rust code to a temp file |
| 434 | let rs_code = format!("{}", ts); |
| 435 | write_to_file(&tdir, "input.rs", &rs_code) |
| 436 | }; |
| 437 | |
| 438 | let target_dir = tdir.path().join("target"); |
| 439 | std::fs::create_dir(&target_dir).unwrap(); |
| 440 | |
| 441 | let rs_path = write_rust_to_file(&rust_code); |
| 442 | |
| 443 | info!("Path is {:?}", tdir.path()); |
| 444 | let builder = Builder::<TestBuilderContext>::new(&rs_path, &[tdir.path()]) |
| 445 | .custom_gendir(target_dir.clone()); |
| 446 | let builder = if let Some(builder_modifier) = &builder_modifier { |
| 447 | builder_modifier.modify_autocxx_builder(builder) |
| 448 | } else { |
| 449 | builder |
| 450 | }; |
| 451 | let build_results = builder.build_listing_files().map_err(TestError::AutoCxx)?; |
| 452 | let mut b = build_results.0; |
| 453 | let generated_rs_files = build_results.1; |
| 454 | |
| 455 | if let Some(code_checker) = &rust_code_checker { |
| 456 | let mut file = File::open(generated_rs_files.get(0).ok_or(TestError::NoRs)?) |
| 457 | .map_err(TestError::RsFileOpen)?; |
| 458 | let mut content = String::new(); |
| 459 | file.read_to_string(&mut content) |
| 460 | .map_err(TestError::RsFileRead)?; |
| 461 | |
| 462 | let ast = syn::parse_file(&content).map_err(TestError::RsFileParse)?; |
| 463 | code_checker.check_rust(ast)?; |
| 464 | code_checker.check_cpp(&build_results.2)?; |
| 465 | if code_checker.skip_build() { |
| 466 | return Ok(()); |
| 467 | } |
| 468 | } |
| 469 | |
| 470 | if !cxx_code.is_empty() { |
| 471 | // Step 4: Write the C++ code snippet to a .cc file, along with a #include |
| 472 | // of the header emitted in step 5. |
| 473 | let cxx_code = format!("#include \"input.h\"\n#include \"cxxgen.h\"\n{}", cxx_code); |
| 474 | let cxx_path = write_to_file(&tdir, "input.cxx", &cxx_code); |
| 475 | b.file(cxx_path); |
| 476 | } |
| 477 | |
| 478 | let b = configure_builder(&mut b).out_dir(&target_dir); |
| 479 | let b = if let Some(builder_modifier) = builder_modifier { |
| 480 | builder_modifier.modify_cc_builder(b) |
| 481 | } else { |
| 482 | b |
| 483 | }; |
| 484 | b.include(tdir.path()) |
| 485 | .try_compile("autocxx-demo") |
| 486 | .map_err(TestError::CppBuild)?; |
| 487 | if KEEP_TEMPDIRS { |
| 488 | println!("Generated .rs files: {:?}", generated_rs_files); |
| 489 | } |
| 490 | // Step 8: use the trybuild crate to build the Rust file. |
| 491 | let r = get_builder().lock().unwrap().build( |
| 492 | &target_dir, |
| 493 | "autocxx-demo", |
| 494 | &tdir.path(), |
| 495 | &["input.h", "cxx.h"], |
| 496 | &rs_path, |
| 497 | generated_rs_files, |
| 498 | RsFindMode::AutocxxRs, |
| 499 | ); |
| 500 | if KEEP_TEMPDIRS { |
| 501 | println!("Tempdir: {:?}", tdir.into_path().to_str()); |
| 502 | } |
| 503 | if r.is_err() { |
| 504 | return Err(TestError::RsBuild); // details of Rust panic are a bit messy to include, and |
| 505 | // not important at the moment. |
| 506 | } |
| 507 | Ok(()) |
| 508 | } |