LCOV - code coverage report
Current view: top level - utils/testing/src - lib.rs (source / functions) Hit Total Coverage
Test: unnamed Lines: 0 237 0.0 %
Date: 2024-10-23 19:20:50 Functions: 0 36 0.0 %

          Line data    Source code
       1             : //! This crate provides convenient access to the [`compiletest_rs`] package for testing [Dylint]
       2             : //! libraries.
       3             : //!
       4             : //! **Note: If your test has dependencies, you must use `ui_test_example` or `ui_test_examples`.**
       5             : //! See the [`question_mark_in_expression`] example in this repository.
       6             : //!
       7             : //! This crate provides the following three functions:
       8             : //!
       9             : //! - [`ui_test`] - test a library on all source files in a directory
      10             : //! - [`ui_test_example`] - test a library on one example target
      11             : //! - [`ui_test_examples`] - test a library on all example targets
      12             : //!
      13             : //! For most situations, you can add the following to your library's `lib.rs` file:
      14             : //!
      15             : //! ```rust,ignore
      16             : //! #[test]
      17             : //! fn ui() {
      18             : //!     dylint_testing::ui_test(env!("CARGO_PKG_NAME"), "ui");
      19             : //! }
      20             : //! ```
      21             : //!
      22             : //! And include one or more `.rs` and `.stderr` files in a `ui` directory alongside your library's
      23             : //! `src` directory. See the [examples] in this repository.
      24             : //!
      25             : //! # Test builder
      26             : //!
      27             : //! In addition to the above three functions, [`ui::Test`] is a test "builder." Currently, the main
      28             : //! advantage of using `Test` over the above functions is that `Test` allows flags to be passed to
      29             : //! `rustc`. For an example of its use, see [`non_thread_safe_call_in_test`] in this repository.
      30             : //!
      31             : //! `Test` has three constructors, which correspond to the above three functions as follows:
      32             : //!
      33             : //! - [`ui::Test::src_base`] <-> [`ui_test`]
      34             : //! - [`ui::Test::example`] <-> [`ui_test_example`]
      35             : //! - [`ui::Test::examples`] <-> [`ui_test_examples`]
      36             : //!
      37             : //! In each case, the constructor's arguments are exactly those of the corresponding function.
      38             : //!
      39             : //! A `Test` instance has the following methods:
      40             : //!
      41             : //! - `dylint_toml` - set the `dylint.toml` file's contents (for testing [configurable libraries])
      42             : //! - `rustc_flags` - pass flags to the compiler when running the test
      43             : //! - `run` - run the test
      44             : //!
      45             : //! # Updating `.stderr` files
      46             : //!
      47             : //! If the standard error that results from running your `.rs` file differs from the contents of
      48             : //! your `.stderr` file, `compiletest_rs` will produce a report like the following:
      49             : //!
      50             : //! ```text
      51             : //! diff of stderr:
      52             : //!
      53             : //!  error: calling `std::env::set_var` in a test could affect the outcome of other tests
      54             : //!    --> $DIR/main.rs:8:5
      55             : //!     |
      56             : //!  LL |     std::env::set_var("KEY", "VALUE");
      57             : //!     |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      58             : //!     |
      59             : //!     = note: `-D non-thread-safe-call-in-test` implied by `-D warnings`
      60             : //!
      61             : //! -error: aborting due to previous error
      62             : //! +error: calling `std::env::set_var` in a test could affect the outcome of other tests
      63             : //! +  --> $DIR/main.rs:23:9
      64             : //! +   |
      65             : //! +LL |         std::env::set_var("KEY", "VALUE");
      66             : //! +   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      67             : //! +
      68             : //! +error: aborting due to 2 previous errors
      69             : //!
      70             : //!
      71             : //!
      72             : //! The actual stderr differed from the expected stderr.
      73             : //! Actual stderr saved to ...
      74             : //! ```
      75             : //!
      76             : //! The meaning of each line is as follows:
      77             : //!
      78             : //! - A line beginning with a plus (`+`) is in the actual standard error, but not in your `.stderr`
      79             : //!   file.
      80             : //! - A line beginning with a minus (`-`) is in your `.stderr` file, but not in the actual standard
      81             : //!   error.
      82             : //! - A line beginning with a space (` `) is in both the actual standard error and your `.stderr`
      83             : //!   file, and is provided for context.
      84             : //! - All other lines (e.g., `diff of stderr:`) contain `compiletest_rs` messages.
      85             : //!
      86             : //! **Note:** In the actual standard error, a blank line usually follows the `error: aborting due to
      87             : //! N previous errors` line. So a correct `.stderr` file will typically contain one blank line at
      88             : //! the end.
      89             : //!
      90             : //! In general, it is not too hard to update a `.stderr` file by hand. However, the `compiletest_rs`
      91             : //! report should contain a line of the form `Actual stderr saved to PATH`. Copying `PATH` to your
      92             : //! `.stderr` file should update it completely.
      93             : //!
      94             : //! Additional documentation on `compiletest_rs` can be found in [its repository].
      95             : //!
      96             : //! [Dylint]: https://github.com/trailofbits/dylint/tree/master
      97             : //! [`compiletest_rs`]: https://github.com/Manishearth/compiletest-rs
      98             : //! [`non_thread_safe_call_in_test`]: https://github.com/trailofbits/dylint/tree/master/examples/general/non_thread_safe_call_in_test/src/lib.rs
      99             : //! [`question_mark_in_expression`]: https://github.com/trailofbits/dylint/tree/master/examples/restriction/question_mark_in_expression/Cargo.toml
     100             : //! [`ui::Test::example`]: https://docs.rs/dylint_testing/latest/dylint_testing/ui/struct.Test.html#method.example
     101             : //! [`ui::Test::examples`]: https://docs.rs/dylint_testing/latest/dylint_testing/ui/struct.Test.html#method.examples
     102             : //! [`ui::Test::src_base`]: https://docs.rs/dylint_testing/latest/dylint_testing/ui/struct.Test.html#method.src_base
     103             : //! [`ui::Test`]: https://docs.rs/dylint_testing/latest/dylint_testing/ui/struct.Test.html
     104             : //! [`ui_test_example`]: https://docs.rs/dylint_testing/latest/dylint_testing/fn.ui_test_example.html
     105             : //! [`ui_test_examples`]: https://docs.rs/dylint_testing/latest/dylint_testing/fn.ui_test_examples.html
     106             : //! [`ui_test`]: https://docs.rs/dylint_testing/latest/dylint_testing/fn.ui_test.html
     107             : //! [configurable libraries]: https://github.com/trailofbits/dylint/tree/master#configurable-libraries
     108             : //! [docs.rs documentation]: https://docs.rs/dylint_testing/latest/dylint_testing/
     109             : //! [examples]: https://github.com/trailofbits/dylint/tree/master/examples
     110             : //! [its repository]: https://github.com/Manishearth/compiletest-rs
     111             : 
     112             : use anyhow::{anyhow, ensure, Context, Result};
     113             : use cargo_metadata::{Metadata, Package, Target};
     114             : use compiletest_rs as compiletest;
     115             : use dylint_internal::{env, library_filename, rustup::is_rustc, CommandExt};
     116             : use once_cell::sync::{Lazy, OnceCell};
     117             : use regex::Regex;
     118             : use std::{
     119             :     env::{consts, remove_var, set_var, var_os},
     120             :     ffi::{OsStr, OsString},
     121             :     fs::{copy, read_dir, remove_file},
     122             :     io::BufRead,
     123             :     path::{Path, PathBuf},
     124             :     sync::Mutex,
     125             : };
     126             : 
     127             : pub mod ui;
     128             : 
     129             : static DRIVER: OnceCell<PathBuf> = OnceCell::new();
     130             : static LINKING_FLAGS: OnceCell<Vec<String>> = OnceCell::new();
     131             : 
     132             : /// Test a library on all source files in a directory.
     133             : ///
     134             : /// - `name` is the name of a Dylint library to be tested. (Often, this is the same as the package
     135             : ///   name.)
     136             : /// - `src_base` is a directory containing:
     137             : ///   - source files on which to test the library (`.rs` files), and
     138             : ///   - the output those files should produce (`.stderr` files).
     139           0 : pub fn ui_test(name: &str, src_base: impl AsRef<Path>) {
     140           0 :     ui::Test::src_base(name, src_base).run();
     141           0 : }
     142             : 
     143             : /// Test a library on one example target.
     144             : ///
     145             : /// - `name` is the name of a Dylint library to be tested.
     146             : /// - `example` is an example target on which to test the library.
     147           0 : pub fn ui_test_example(name: &str, example: &str) {
     148           0 :     ui::Test::example(name, example).run();
     149           0 : }
     150             : 
     151             : /// Test a library on all example targets.
     152             : ///
     153             : /// - `name` is the name of a Dylint library to be tested.
     154           0 : pub fn ui_test_examples(name: &str) {
     155           0 :     ui::Test::examples(name).run();
     156           0 : }
     157             : 
     158           0 : fn initialize(name: &str) -> Result<&Path> {
     159           0 :     DRIVER
     160           0 :         .get_or_try_init(|| {
     161           0 :             let _ = env_logger::try_init();
     162           0 : 
     163           0 :             // smoelius: Try to order failures by how informative they are: failure to build the
     164           0 :             // library, failure to find the library, failure to build/find the driver.
     165           0 : 
     166           0 :             dylint_internal::cargo::build(&format!("library `{name}`"))
     167           0 :                 .build()
     168           0 :                 .success()?;
     169             : 
     170             :             // smoelius: `DYLINT_LIBRARY_PATH` must be set before `dylint_libs` is called.
     171             :             // smoelius: This was true when `dylint_libs` called `name_toolchain_map`, but that is
     172             :             // no longer the case. I am leaving the comment here for now in case removal
     173             :             // of the `name_toolchain_map` call causes a regression.
     174           0 :             let metadata = dylint_internal::cargo::current_metadata().unwrap();
     175           0 :             let dylint_library_path = metadata.target_directory.join("debug");
     176           0 :             set_var(env::DYLINT_LIBRARY_PATH, dylint_library_path);
     177             : 
     178           0 :             let dylint_libs = dylint_libs(name)?;
     179           0 :             let driver = dylint::driver_builder::get(
     180           0 :                 &dylint::opts::Dylint::default(),
     181           0 :                 env!("RUSTUP_TOOLCHAIN"),
     182           0 :             )?;
     183             : 
     184           0 :             set_var(env::CLIPPY_DISABLE_DOCS_LINKS, "true");
     185           0 :             set_var(env::DYLINT_LIBS, dylint_libs);
     186           0 : 
     187           0 :             Ok(driver)
     188           0 :         })
     189           0 :         .map(PathBuf::as_path)
     190           0 : }
     191             : 
     192             : #[doc(hidden)]
     193           0 : pub fn dylint_libs(name: &str) -> Result<String> {
     194           0 :     let metadata = dylint_internal::cargo::current_metadata().unwrap();
     195           0 :     let rustup_toolchain = env::var(env::RUSTUP_TOOLCHAIN)?;
     196           0 :     let filename = library_filename(name, &rustup_toolchain);
     197           0 :     let path = metadata.target_directory.join("debug").join(filename);
     198           0 :     let paths = vec![path];
     199           0 :     serde_json::to_string(&paths).map_err(Into::into)
     200           0 : }
     201             : 
     202           0 : fn example_target(package: &Package, example: &str) -> Result<Target> {
     203           0 :     package
     204           0 :         .targets
     205           0 :         .iter()
     206           0 :         .find(|target| target.kind == ["example"] && target.name == example)
     207           0 :         .cloned()
     208           0 :         .ok_or_else(|| anyhow!("Could not find example `{}`", example))
     209           0 : }
     210             : 
     211             : #[allow(clippy::unnecessary_wraps)]
     212           0 : fn example_targets(package: &Package) -> Result<Vec<Target>> {
     213           0 :     Ok(package
     214           0 :         .targets
     215           0 :         .iter()
     216           0 :         .filter(|target| target.kind == ["example"])
     217           0 :         .cloned()
     218           0 :         .collect())
     219           0 : }
     220             : 
     221           0 : fn run_example_test(
     222           0 :     driver: &Path,
     223           0 :     metadata: &Metadata,
     224           0 :     package: &Package,
     225           0 :     target: &Target,
     226           0 :     config: &ui::Config,
     227           0 : ) -> Result<()> {
     228           0 :     let linking_flags = linking_flags(metadata, package, target)?;
     229           0 :     let file_name = target
     230           0 :         .src_path
     231           0 :         .file_name()
     232           0 :         .ok_or_else(|| anyhow!("Could not get file name"))?;
     233             : 
     234           0 :     let tempdir = tempfile::tempdir().with_context(|| "`tempdir` failed")?;
     235           0 :     let src_base = tempdir.path();
     236           0 :     let to = src_base.join(file_name);
     237           0 : 
     238           0 :     copy(&target.src_path, &to).with_context(|| {
     239           0 :         format!(
     240           0 :             "Could not copy `{}` to `{}`",
     241           0 :             target.src_path,
     242           0 :             to.to_string_lossy()
     243           0 :         )
     244           0 :     })?;
     245           0 :     ["fixed", "stderr", "stdout"]
     246           0 :         .map(|extension| copy_with_extension(&target.src_path, &to, extension).unwrap_or_default());
     247           0 : 
     248           0 :     let mut config = config.clone();
     249           0 :     config.rustc_flags.extend(linking_flags.iter().cloned());
     250           0 : 
     251           0 :     run_tests(driver, src_base, &config);
     252           0 : 
     253           0 :     Ok(())
     254           0 : }
     255             : 
     256           0 : fn linking_flags(
     257           0 :     metadata: &Metadata,
     258           0 :     package: &Package,
     259           0 :     target: &Target,
     260           0 : ) -> Result<&'static [String]> {
     261           0 :     LINKING_FLAGS
     262           0 :         .get_or_try_init(|| {
     263           0 :             let rustc_flags = rustc_flags(metadata, package, target)?;
     264             : 
     265           0 :             let mut linking_flags = Vec::new();
     266           0 : 
     267           0 :             let mut iter = rustc_flags.into_iter();
     268           0 :             while let Some(flag) = iter.next() {
     269           0 :                 if flag.starts_with("--edition=") {
     270           0 :                     linking_flags.push(flag);
     271           0 :                 } else if flag == "--extern" || flag == "-L" {
     272           0 :                     let arg = next(&flag, &mut iter)?;
     273           0 :                     linking_flags.extend([flag, arg.trim_matches('\'').to_owned()]);
     274           0 :                 }
     275             :             }
     276             : 
     277           0 :             Ok(linking_flags)
     278           0 :         })
     279           0 :         .map(Vec::as_slice)
     280           0 : }
     281             : 
     282             : // smoelius: We need to recover the `rustc` flags used to build a target. I can see four options:
     283             : //
     284             : // * Use `cargo build --build-plan`
     285             : //   - Pros: Easily parsable JSON output
     286             : //   - Cons: Unstable and likely to be removed: https://github.com/rust-lang/cargo/issues/7614
     287             : // * Parse the output of `cargo build --verbose`
     288             : //   - Pros: ?
     289             : //   - Cons: Not as easily parsable, requires synchronization (see below)
     290             : // * Use a custom executor like Siderophile does: https://github.com/trailofbits/siderophile/blob/26c067306f6c2f66d9530dacef6b17dbf59cdf8c/src/trawl_source/mod.rs#L399
     291             : //   - Pros: Ground truth
     292             : //   - Cons: Seems a bit of a heavy lift (Note: I think Siderophile's approach was inspired by
     293             : //     `cargo-geiger`.)
     294             : // * Set `RUSTC_WORKSPACE_WRAPPER` to something that logs `rustc` invocations
     295             : //   - Pros: Ground truth
     296             : //   - Cons: Requires a separate executable/script, portability could be an issue
     297             : //
     298             : // I am going with the second option for now, because it seems to be the least of all evils. This
     299             : // decision may need to be revisited.
     300             : 
     301           0 : static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\s*Running\s*`(.*)`$").unwrap());
     302             : 
     303           0 : fn rustc_flags(metadata: &Metadata, package: &Package, target: &Target) -> Result<Vec<String>> {
     304             :     // smoelius: The following comments are old and retained for posterity. The linking flags are
     305             :     // now initialized using a `OnceCell`, which makes the mutex unnecessary.
     306             :     //   smoelius: Force rebuilding of the example by removing it. This is kind of messy. The
     307             :     //   example is a shared resource that may be needed by multiple tests. For now, I lock a mutex
     308             :     //   while the example is removed and put back.
     309             :     //   smoelius: Should we use a temporary target directory here?
     310           0 :     let output = {
     311           0 :         remove_example(metadata, package, target)?;
     312             : 
     313             :         // smoelius: Because of lazy initialization, `cargo build` is run only once. Seeing
     314             :         // "Building example `target`" for one example but not for others is confusing. So instead
     315             :         // say "Building `package` examples".
     316           0 :         dylint_internal::cargo::build(&format!("`{}` examples", package.name))
     317           0 :             .build()
     318           0 :             .envs([(env::CARGO_TERM_COLOR, "never")])
     319           0 :             .args([
     320           0 :                 "--manifest-path",
     321           0 :                 package.manifest_path.as_ref(),
     322           0 :                 "--example",
     323           0 :                 &target.name,
     324           0 :                 "--verbose",
     325           0 :             ])
     326           0 :             .logged_output(true)?
     327             :     };
     328             : 
     329           0 :     let matches = output
     330           0 :         .stderr
     331           0 :         .lines()
     332           0 :         .map(|line| {
     333           0 :             let line =
     334           0 :                 line.with_context(|| format!("Could not read from `{}`", package.manifest_path))?;
     335           0 :             Ok((*RE).captures(&line).and_then(|captures| {
     336           0 :                 let args = captures[1]
     337           0 :                     .split(' ')
     338           0 :                     .map(ToOwned::to_owned)
     339           0 :                     .collect::<Vec<_>>();
     340           0 :                 if args.first().map_or(false, is_rustc)
     341           0 :                     && args
     342           0 :                         .as_slice()
     343           0 :                         .windows(2)
     344           0 :                         .any(|window| window == ["--crate-name", &snake_case(&target.name)])
     345             :                 {
     346           0 :                     Some(args)
     347             :                 } else {
     348           0 :                     None
     349             :                 }
     350           0 :             }))
     351           0 :         })
     352           0 :         .collect::<Result<Vec<Option<Vec<_>>>>>()?;
     353             : 
     354           0 :     let mut matches = matches.into_iter().flatten().collect::<Vec<Vec<_>>>();
     355           0 :     ensure!(
     356           0 :         matches.len() <= 1,
     357           0 :         "Found multiple `rustc` invocations for `{}`",
     358             :         target.name
     359             :     );
     360           0 :     matches
     361           0 :         .pop()
     362           0 :         .ok_or_else(|| anyhow!("Found no `rustc` invocations for `{}`", target.name))
     363           0 : }
     364             : 
     365           0 : fn remove_example(metadata: &Metadata, _package: &Package, target: &Target) -> Result<()> {
     366           0 :     let examples = metadata.target_directory.join("debug/examples");
     367           0 :     for entry in
     368           0 :         read_dir(&examples).with_context(|| format!("`read_dir` failed for `{examples}`"))?
     369             :     {
     370           0 :         let entry = entry.with_context(|| format!("`read_dir` failed for `{examples}`"))?;
     371           0 :         let path = entry.path();
     372             : 
     373           0 :         if let Some(file_name) = path.file_name() {
     374           0 :             let s = file_name.to_string_lossy();
     375           0 :             let target_name = snake_case(&target.name);
     376           0 :             if s == target_name.clone() + consts::EXE_SUFFIX
     377           0 :                 || s.starts_with(&(target_name.clone() + "-"))
     378             :             {
     379           0 :                 remove_file(&path).with_context(|| {
     380           0 :                     format!("`remove_file` failed for `{}`", path.to_string_lossy())
     381           0 :                 })?;
     382           0 :             }
     383           0 :         }
     384             :     }
     385             : 
     386           0 :     Ok(())
     387           0 : }
     388             : 
     389           0 : fn next<I, T>(flag: &str, iter: &mut I) -> Result<T>
     390           0 : where
     391           0 :     I: Iterator<Item = T>,
     392           0 : {
     393           0 :     iter.next()
     394           0 :         .ok_or_else(|| anyhow!("Missing argument for `{}`", flag))
     395           0 : }
     396             : 
     397           0 : fn copy_with_extension<P: AsRef<Path>, Q: AsRef<Path>>(
     398           0 :     from: P,
     399           0 :     to: Q,
     400           0 :     extension: &str,
     401           0 : ) -> Result<u64> {
     402           0 :     let from = from.as_ref().with_extension(extension);
     403           0 :     let to = to.as_ref().with_extension(extension);
     404           0 :     copy(from, to).map_err(Into::into)
     405           0 : }
     406             : 
     407             : static MUTEX: Mutex<()> = Mutex::new(());
     408             : 
     409           0 : fn run_tests(driver: &Path, src_base: &Path, config: &ui::Config) {
     410           0 :     let _lock = MUTEX.lock().unwrap();
     411           0 : 
     412           0 :     // smoelius: There doesn't seem to be a way to set environment variables using `compiletest`'s
     413           0 :     // [`Config`](https://docs.rs/compiletest_rs/0.7.1/compiletest_rs/common/struct.Config.html)
     414           0 :     // struct. For comparison, where Clippy uses `compiletest`, it sets environment variables
     415           0 :     // directly (see: https://github.com/rust-lang/rust-clippy/blob/master/tests/compile-test.rs).
     416           0 :     //
     417           0 :     // Of course, even if `compiletest` had such support, it would need to be incorporated into
     418           0 :     // `dylint_testing`.
     419           0 : 
     420           0 :     let _var = config
     421           0 :         .dylint_toml
     422           0 :         .as_ref()
     423           0 :         .map(|value| VarGuard::set(env::DYLINT_TOML, value));
     424             : 
     425           0 :     let config = compiletest::Config {
     426           0 :         mode: compiletest::common::Mode::Ui,
     427           0 :         rustc_path: driver.to_path_buf(),
     428           0 :         src_base: src_base.to_path_buf(),
     429           0 :         target_rustcflags: Some(
     430           0 :             config.rustc_flags.clone().join(" ")
     431           0 :                 + " --emit=metadata"
     432           0 :                 + if cfg!(feature = "deny_warnings") {
     433           0 :                     " -Dwarnings"
     434             :                 } else {
     435           0 :                     ""
     436             :                 }
     437           0 :                 + " -Zui-testing",
     438           0 :         ),
     439           0 :         ..compiletest::Config::default()
     440           0 :     };
     441           0 : 
     442           0 :     compiletest::run_tests(&config);
     443           0 : }
     444             : 
     445             : // smoelius: `VarGuard` was copied from:
     446             : // https://github.com/rust-lang/rust-clippy/blob/9cc8da222b3893bc13bc13c8827e93f8ea246854/tests/compile-test.rs
     447             : 
     448             : /// Restores an env var on drop
     449             : #[must_use]
     450             : struct VarGuard {
     451             :     key: &'static str,
     452             :     value: Option<OsString>,
     453             : }
     454             : 
     455             : impl VarGuard {
     456           0 :     fn set(key: &'static str, val: impl AsRef<OsStr>) -> Self {
     457           0 :         let value = var_os(key);
     458           0 :         set_var(key, val);
     459           0 :         Self { key, value }
     460           0 :     }
     461             : }
     462             : 
     463             : impl Drop for VarGuard {
     464           0 :     fn drop(&mut self) {
     465           0 :         match self.value.as_deref() {
     466           0 :             None => remove_var(self.key),
     467           0 :             Some(value) => set_var(self.key, value),
     468             :         }
     469           0 :     }
     470             : }
     471             : 
     472           0 : fn snake_case(name: &str) -> String {
     473           0 :     name.replace('-', "_")
     474           0 : }

Generated by: LCOV version 1.14