From de73d13eb5aa12b33db55fab65c98d2b3caf9491 Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Sat, 1 Nov 2025 12:00:06 -0400 Subject: [PATCH 01/21] fix: remove outdated comment --- bevy_lint/tests/ui.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/bevy_lint/tests/ui.rs b/bevy_lint/tests/ui.rs index 5bf5a25f..aa527fa0 100644 --- a/bevy_lint/tests/ui.rs +++ b/bevy_lint/tests/ui.rs @@ -1,7 +1,3 @@ -// A convenience feature used in `find_bevy_rlib()` that lets you chain multiple `if let` -// statements together with `&&`. This feature flag is needed in all integration tests that use the -// test_utils module, since each integration test is compiled independently. - use test_utils::base_config; use ui_test::run_tests; From f906ce0bf5f09bd6548bd2282c84f56940755703 Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Sat, 1 Nov 2025 12:03:32 -0400 Subject: [PATCH 02/21] refactor: inline `base_config()` into ui test runner --- bevy_lint/tests/ui.rs | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/bevy_lint/tests/ui.rs b/bevy_lint/tests/ui.rs index aa527fa0..fd62b5f1 100644 --- a/bevy_lint/tests/ui.rs +++ b/bevy_lint/tests/ui.rs @@ -1,9 +1,44 @@ -use test_utils::base_config; -use ui_test::run_tests; +use std::path::{Path, PathBuf}; -mod test_utils; +use ui_test::{CommandBuilder, Config, run_tests}; + +// This is set by Cargo to the absolute path of `bevy_lint_driver`. +const DRIVER_PATH: &str = env!("CARGO_BIN_EXE_bevy_lint_driver"); fn main() { - let config = base_config("ui").unwrap(); + let driver_path = Path::new(DRIVER_PATH); + + assert!( + driver_path.is_file(), + "`bevy_lint_driver` could not be found at {}, make sure to build it with `cargo build -p bevy_lint --bin bevy_lint_driver`", + driver_path.display(), + ); + + let config = Config { + // When `host` is `None`, `ui_test` will attempt to auto-discover the host by calling + // `program -vV`. Unfortunately, `bevy_lint_driver` does not yet support the version flag, + // so we manually specify the host as an empty string. This means that, for now, host- + // specific configuration in UI tests will not work. + host: Some(String::new()), + program: CommandBuilder { + // We don't need `rustup run` here because we're already using the correct toolchain + // due to `rust-toolchain.toml`. + program: driver_path.into(), + args: vec![ + // `bevy_lint_driver` expects the first argument to be the path to `rustc`. + "rustc".into(), + // This is required so that `ui_test` can parse warnings and errors. + "--error-format=json".into(), + todo!("include bevy dependencies"), + ], + out_dir_flag: Some("--out-dir".into()), + input_file_flag: None, + envs: Vec::new(), + cfg_flag: Some("--print=cfg".into()), + }, + out_dir: PathBuf::from("../target/ui"), + ..Config::rustc(Path::new("tests/ui")) + }; + run_tests(config).unwrap(); } From a9b4b1caeed1146d209cabf7fd13b39bccbd4893 Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Sat, 1 Nov 2025 12:14:34 -0400 Subject: [PATCH 03/21] feat: query host tuple through `rustc`, rather than leaving it blank `ui_test` isn't allowed to find the host tuple itself, since it tries running `bevy_lint_driver -vV` and gets very confused when `bevy_lint_driver` silently eats the `-vV` flag because it expected `rustc`'s path instead. Because of that, we need to specify the host tuple with some default. Before, leaving it as an empty string worked fine, but in a future commit I'm going to introduce `DependencyBuilder`, which needs a valid host tuple. As such, the best solution is to properly query `rustc` for the host tuple and pass it to `ui_test`. --- bevy_lint/tests/ui.rs | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/bevy_lint/tests/ui.rs b/bevy_lint/tests/ui.rs index fd62b5f1..40f1398d 100644 --- a/bevy_lint/tests/ui.rs +++ b/bevy_lint/tests/ui.rs @@ -1,4 +1,7 @@ -use std::path::{Path, PathBuf}; +use std::{ + path::{Path, PathBuf}, + process::{Command, Stdio}, +}; use ui_test::{CommandBuilder, Config, run_tests}; @@ -15,11 +18,12 @@ fn main() { ); let config = Config { - // When `host` is `None`, `ui_test` will attempt to auto-discover the host by calling - // `program -vV`. Unfortunately, `bevy_lint_driver` does not yet support the version flag, - // so we manually specify the host as an empty string. This means that, for now, host- - // specific configuration in UI tests will not work. - host: Some(String::new()), + // We need to specify the host tuple manually, because if we don't then `ui_test` will try + // running `bevy_lint_driver -vV` to discover the host and promptly error because it + // doesn't realize `bevy_lint_driver` expects its first argument to be the path to `rustc`. + // If `ui_test` ran `bevy_lint_driver rustc -vV` everything would work, but it's not smart + // enough to do that. + host: Some(host_tuple()), program: CommandBuilder { // We don't need `rustup run` here because we're already using the correct toolchain // due to `rust-toolchain.toml`. @@ -42,3 +46,20 @@ fn main() { run_tests(config).unwrap(); } + +/// Queries the host tuple from `rustc` and returns it as a string. +fn host_tuple() -> String { + let output = Command::new("rustc") + .arg("--print=host-tuple") + // Show errors directly to the user, rather than capturing them. + .stderr(Stdio::inherit()) + .output() + .expect("failed to run `rustc --print=host-tuple`"); + + // `rustc` only works with UTF-8, so it's safe to error if invalid UTF-8 is found. + str::from_utf8(&output.stdout) + .expect("`rustc --print=host-tuple` did not emit valid UTF-8") + // Remove the trailing `\n`. + .trim_end() + .to_string() +} From 17b7d27ed04cf8a621c8e7dd8de39e7ece524fd5 Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Sat, 1 Nov 2025 12:49:43 -0400 Subject: [PATCH 04/21] chore: build `bevy` using `ui_test`'s `DependencyBuilder` This makes `ui_test` build our dependencies in a separate crate, rather than expecting `bevy` to be built as a dev-dependency. This should hopefully fix https://github.com/TheBevyFlock/bevy_cli/pull/568#issuecomment-3310172485, letting us run UI tests on Windows. --- Cargo.lock | 10 +++++++++- Cargo.toml | 2 +- bevy_lint/Cargo.toml | 7 ------- bevy_lint/tests/dependencies/Cargo.toml | 19 +++++++++++++++++++ bevy_lint/tests/dependencies/src/lib.rs | 1 + bevy_lint/tests/ui.rs | 13 ++++++++++--- 6 files changed, 40 insertions(+), 12 deletions(-) create mode 100644 bevy_lint/tests/dependencies/Cargo.toml create mode 100644 bevy_lint/tests/dependencies/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 28ac2ba9..044c6d4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -819,7 +819,6 @@ dependencies = [ "anstream", "anstyle", "anyhow", - "bevy", "cargo_metadata 0.23.0", "clippy_utils", "pico-args", @@ -2696,6 +2695,15 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "lint-ui-dependencies" +version = "0.0.0" +dependencies = [ + "bevy", + "bevy_ecs", + "bevy_reflect", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" diff --git a/Cargo.toml b/Cargo.toml index 0f129660..c90f0c96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["bevy_lint"] +members = ["bevy_lint", "bevy_lint/tests/dependencies"] [workspace.lints.clippy] # Prefer `str.to_owned()`, rather than `str.to_string()`. diff --git a/bevy_lint/Cargo.toml b/bevy_lint/Cargo.toml index 47a82e51..2d56403c 100644 --- a/bevy_lint/Cargo.toml +++ b/bevy_lint/Cargo.toml @@ -53,13 +53,6 @@ toml = { version = "0.9.8", default-features = false, features = [ ] } [dev-dependencies] -# Used when running UI tests. -bevy = { version = "0.17.2", default-features = false, features = [ - "std", - # used for the `camera_modification_in_fixed_update` lint - "bevy_render", -] } - # Used to deserialize `--message-format=json` messages from Cargo. serde_json = "1.0.145" diff --git a/bevy_lint/tests/dependencies/Cargo.toml b/bevy_lint/tests/dependencies/Cargo.toml new file mode 100644 index 00000000..b4808069 --- /dev/null +++ b/bevy_lint/tests/dependencies/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "lint-ui-dependencies" +description = "A dummy crate that builds dependencies that `bevy_lint`'s UI tests need to import" +edition = "2024" +publish = false + +[dependencies] +bevy = { version = "0.17.2", default-features = false, features = [ + "std", + # used for the `camera_modification_in_fixed_update` lint + "bevy_render", +] } + +# Some of Bevy's macros try to intelligently detect whether to import `bevy::ecs` or `bevy_ecs`. +# This works in normal circumstances, but our dependency setup is too complex for the macros to +# understand, so they default to the `bevy_ecs` form. This makes `bevy_ecs` and `bevy_reflect` +# directly importable, and matches the crate version to whatever version `bevy` is pulling in. +bevy_ecs = "*" +bevy_reflect = "*" diff --git a/bevy_lint/tests/dependencies/src/lib.rs b/bevy_lint/tests/dependencies/src/lib.rs new file mode 100644 index 00000000..4653cff5 --- /dev/null +++ b/bevy_lint/tests/dependencies/src/lib.rs @@ -0,0 +1 @@ +//! This file is intentionally blank. :) diff --git a/bevy_lint/tests/ui.rs b/bevy_lint/tests/ui.rs index 40f1398d..caf96c06 100644 --- a/bevy_lint/tests/ui.rs +++ b/bevy_lint/tests/ui.rs @@ -3,7 +3,7 @@ use std::{ process::{Command, Stdio}, }; -use ui_test::{CommandBuilder, Config, run_tests}; +use ui_test::{CommandBuilder, Config, dependencies::DependencyBuilder, run_tests}; // This is set by Cargo to the absolute path of `bevy_lint_driver`. const DRIVER_PATH: &str = env!("CARGO_BIN_EXE_bevy_lint_driver"); @@ -17,7 +17,7 @@ fn main() { driver_path.display(), ); - let config = Config { + let mut config = Config { // We need to specify the host tuple manually, because if we don't then `ui_test` will try // running `bevy_lint_driver -vV` to discover the host and promptly error because it // doesn't realize `bevy_lint_driver` expects its first argument to be the path to `rustc`. @@ -33,7 +33,6 @@ fn main() { "rustc".into(), // This is required so that `ui_test` can parse warnings and errors. "--error-format=json".into(), - todo!("include bevy dependencies"), ], out_dir_flag: Some("--out-dir".into()), input_file_flag: None, @@ -44,6 +43,14 @@ fn main() { ..Config::rustc(Path::new("tests/ui")) }; + // Give UI tests access to all crate dependencies in the `dependencies` folder. This lets UI + // tests import `bevy`. + let revisioned = config.comment_defaults.base(); + revisioned.set_custom("dependencies", DependencyBuilder { + crate_manifest_path: PathBuf::from("tests/dependencies/Cargo.toml"), + ..Default::default() + }); + run_tests(config).unwrap(); } From 0eed476df9648ec59944d10a29b28919b9939a0e Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Sat, 1 Nov 2025 12:56:30 -0400 Subject: [PATCH 05/21] chore: inline `base_config()` into cargo ui test runner --- bevy_lint/tests/ui_cargo.rs | 49 +++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/bevy_lint/tests/ui_cargo.rs b/bevy_lint/tests/ui_cargo.rs index 82620203..1e15ac30 100644 --- a/bevy_lint/tests/ui_cargo.rs +++ b/bevy_lint/tests/ui_cargo.rs @@ -2,12 +2,14 @@ // statements together with `&&`. This feature flag is needed in all integration tests that use the // test_utils module, since each integration test is compiled independently. -use std::env; +use std::{env, path::{Path, PathBuf}}; -use test_utils::base_config; -use ui_test::{CommandBuilder, status_emitter}; +use ui_test::{CommandBuilder, Config, status_emitter}; + +// This is set by Cargo to the absolute paths of `bevy_lint` and `bevy_lint_driver`. +const LINTER_PATH: &str = env!("CARGO_BIN_EXE_bevy_lint"); +const DRIVER_PATH: &str = env!("CARGO_BIN_EXE_bevy_lint_driver"); -mod test_utils; /// This [`Config`] will run the `bevy_lint` command for all paths that end in `Cargo.toml` /// # Example: /// ```sh @@ -15,7 +17,44 @@ mod test_utils; /// "../target/ui/0/tests/ui-cargo/duplicate_bevy_dependencies/fail" "--manifest-path" /// "tests/ui-cargo/duplicate_bevy_dependencies/fail/Cargo.toml"``` fn main() { - let mut config = base_config("ui-cargo").unwrap(); + let linter_path = Path::new(LINTER_PATH); + let driver_path = Path::new(DRIVER_PATH); + + assert!( + linter_path.is_file(), + "`bevy_lint` could not be found at {}, make sure to build it with `cargo build -p bevy_lint --bin bevy_lint`", + linter_path.display(), + ); + assert!( + driver_path.is_file(), + "`bevy_lint_driver` could not be found at {}, make sure to build it with `cargo build -p bevy_lint --bin bevy_lint_driver`", + driver_path.display(), + ); + + let mut config = Config { + // When `host` is `None`, `ui_test` will attempt to auto-discover the host by calling + // `program -vV`. Unfortunately, `bevy_lint_driver` does not yet support the version flag, + // so we manually specify the host as an empty string. This means that, for now, host- + // specific configuration in UI tests will not work. + host: Some(String::new()), + program: CommandBuilder { + // We don't need `rustup run` here because we're already using the correct toolchain + // due to `rust-toolchain.toml`. + program: driver_path.into(), + args: vec![ + // `bevy_lint_driver` expects the first argument to be the path to `rustc`. + "rustc".into(), + // This is required so that `ui_test` can parse warnings and errors. + "--error-format=json".into(), + ], + out_dir_flag: Some("--out-dir".into()), + input_file_flag: None, + envs: Vec::new(), + cfg_flag: Some("--print=cfg".into()), + }, + out_dir: PathBuf::from("../target/ui"), + ..Config::rustc(Path::new("tests/ui-cargo")) + }; let defaults = config.comment_defaults.base(); // The driver returns a '101' on error. From 6719961e4da0dbab8757d605f4ef26afc5879bc8 Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Sat, 1 Nov 2025 12:56:41 -0400 Subject: [PATCH 06/21] chore!: delete `test_utils` --- Cargo.lock | 1 - bevy_lint/Cargo.toml | 3 - bevy_lint/tests/test_utils/mod.rs | 137 ------------------------------ 3 files changed, 141 deletions(-) delete mode 100644 bevy_lint/tests/test_utils/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 044c6d4b..137d9916 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -823,7 +823,6 @@ dependencies = [ "clippy_utils", "pico-args", "serde", - "serde_json", "toml", "ui_test", ] diff --git a/bevy_lint/Cargo.toml b/bevy_lint/Cargo.toml index 2d56403c..1a543cc4 100644 --- a/bevy_lint/Cargo.toml +++ b/bevy_lint/Cargo.toml @@ -53,9 +53,6 @@ toml = { version = "0.9.8", default-features = false, features = [ ] } [dev-dependencies] -# Used to deserialize `--message-format=json` messages from Cargo. -serde_json = "1.0.145" - # Ensures the error messages for lints do not regress. ui_test = "0.30.1" diff --git a/bevy_lint/tests/test_utils/mod.rs b/bevy_lint/tests/test_utils/mod.rs deleted file mode 100644 index e7048d9a..00000000 --- a/bevy_lint/tests/test_utils/mod.rs +++ /dev/null @@ -1,137 +0,0 @@ -use std::{ - ffi::OsStr, - path::{Path, PathBuf}, - process::{Command, Stdio}, -}; - -use serde::Deserialize; -use ui_test::{ - CommandBuilder, Config, - color_eyre::{self, eyre::ensure}, -}; - -// This is set by Cargo to the absolute path of `bevy_lint_driver`. -const DRIVER_PATH: &str = env!("CARGO_BIN_EXE_bevy_lint_driver"); - -/// Generates a custom [`Config`] for `bevy_lint`'s UI tests. -pub fn base_config(test_dir: &str) -> color_eyre::Result { - let driver_path = Path::new(DRIVER_PATH); - - ensure!( - driver_path.is_file(), - "`bevy_lint_driver` could not be found at {}, make sure to build it with `cargo build -p bevy_lint --bin bevy_lint_driver`", - driver_path.display(), - ); - - let config = Config { - // When `host` is `None`, `ui_test` will attempt to auto-discover the host by calling - // `program -vV`. Unfortunately, `bevy_lint_driver` does not yet support the version flag, - // so we manually specify the host as an empty string. This means that, for now, host- - // specific configuration in UI tests will not work. - host: Some(String::new()), - program: CommandBuilder { - // We don't need `rustup run` here because we're already using the correct toolchain - // due to `rust-toolchain.toml`. - program: driver_path.into(), - args: vec![ - // `bevy_lint_driver` expects the first argument to be the path to `rustc`. - "rustc".into(), - // This is required so that `ui_test` can parse warnings and errors. - "--error-format=json".into(), - // These two lines tell `rustc` to search in `target/debug/deps` for dependencies. - // This is required for UI tests to import `bevy`. - "-L".into(), - "all=../target/debug/deps".into(), - // Make the `bevy` crate directly importable from the UI tests. - format!("--extern=bevy={}", find_bevy_rlib()?.display()).into(), - ], - out_dir_flag: Some("--out-dir".into()), - input_file_flag: None, - envs: Vec::new(), - cfg_flag: Some("--print=cfg".into()), - }, - out_dir: PathBuf::from("../target/ui"), - ..Config::rustc(Path::new("tests").join(test_dir)) - }; - - Ok(config) -} - -/// An artifact message printed to stdout by Cargo. -/// -/// This only deserializes the fields necessary to run UI tests, the rest of skipped. -/// -/// See for more -/// information on the exact format. -#[derive(Deserialize, Debug)] -#[serde(rename = "compiler-artifact", tag = "reason")] -struct ArtifactMessage<'a> { - #[serde(borrow)] - target: ArtifactTarget<'a>, - - #[serde(borrow)] - filenames: Vec<&'a Path>, -} - -/// The `"target"` field of an [`ArtifactMessage`]. -#[derive(Deserialize, Debug)] -struct ArtifactTarget<'a> { - name: &'a str, - - #[serde(borrow)] - kind: Vec<&'a str>, -} - -/// Tries to find the path to `libbevy.rlib` that UI tests import. -/// -/// `bevy` is a dev-dependency, and as such is only built for tests and examples. We can force it -/// to be built by calling `cargo build --test=ui --message-format=json`, then scan the printed -/// JSON for the artifact message with the path to `libbevy.rlib`. -/// -/// The reason we specify `--extern bevy=PATH` instead of just `--extern bevy` is because `rustc` -/// will fail to compile if multiple `libbevy.rlib` files are found, which usually is the case. -fn find_bevy_rlib() -> color_eyre::Result { - // `bevy` is a dev-dependency, so building a test will require it to be built as well. - let output = Command::new("cargo") - .arg("build") - .arg("--test=ui") - .arg("--message-format=json") - // Show error messages to the user for easier debugging. - .stderr(Stdio::inherit()) - .output()?; - - ensure!(output.status.success(), "`cargo build --test=ui` failed."); - - // It's theoretically possible for there to be multiple messages about building `libbevy.rlib`. - // We support this, but optimize for just 1 message. - let mut messages = Vec::with_capacity(1); - - // Convert the `stdout` to a string, replacing invalid characters with `�`. - let stdout = String::from_utf8_lossy(&output.stdout); - - // Iterate over each line in stdout, trying to deserialize it from JSON. - for line in stdout.lines() { - if let Ok(message) = serde_json::from_str::(line) - // If the message passes the following conditions, it's probably the one we want. - && message.target.name == "bevy" - && message.target.kind.contains(&"lib") - { - messages.push(message); - } - } - - ensure!( - messages.len() == 1, - "More than one `libbevy.rlib` was built for UI tests. Please ensure there is not more than 1 version of Bevy in `Cargo.lock`.", - ); - - // The message usually has multiple files, often `libbevy.rlib` and `libbevy.rmeta`. Filter - // through these to find the `rlib`. - let rlib = messages[0] - .filenames - .iter() - .find(|p| p.extension() == Some(OsStr::new("rlib"))) - .expect("`libbevy.rlib` not found within artifact message filenames."); - - Ok(rlib.to_path_buf()) -} From 747c7b9c49da17e8938d450c2b250848747fd7d0 Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Sat, 1 Nov 2025 13:17:15 -0400 Subject: [PATCH 07/21] chore: simplify cargo ui test runner I ended up duplicating `host_tuple()`, but I personally am fine with it. --- bevy_lint/tests/ui_cargo.rs | 94 +++++++++++++------------------------ 1 file changed, 32 insertions(+), 62 deletions(-) diff --git a/bevy_lint/tests/ui_cargo.rs b/bevy_lint/tests/ui_cargo.rs index 1e15ac30..1e6451ed 100644 --- a/bevy_lint/tests/ui_cargo.rs +++ b/bevy_lint/tests/ui_cargo.rs @@ -1,59 +1,38 @@ -// A convenience feature used in `find_bevy_rlib()` that lets you chain multiple `if let` -// statements together with `&&`. This feature flag is needed in all integration tests that use the -// test_utils module, since each integration test is compiled independently. - -use std::{env, path::{Path, PathBuf}}; +use std::{ + env, + path::{Path, PathBuf}, + process::{Command, Stdio}, +}; use ui_test::{CommandBuilder, Config, status_emitter}; // This is set by Cargo to the absolute paths of `bevy_lint` and `bevy_lint_driver`. const LINTER_PATH: &str = env!("CARGO_BIN_EXE_bevy_lint"); -const DRIVER_PATH: &str = env!("CARGO_BIN_EXE_bevy_lint_driver"); -/// This [`Config`] will run the `bevy_lint` command for all paths that end in `Cargo.toml` -/// # Example: -/// ```sh -/// bevy_lint" "--quiet" "--target-dir" -/// "../target/ui/0/tests/ui-cargo/duplicate_bevy_dependencies/fail" "--manifest-path" -/// "tests/ui-cargo/duplicate_bevy_dependencies/fail/Cargo.toml"``` fn main() { let linter_path = Path::new(LINTER_PATH); - let driver_path = Path::new(DRIVER_PATH); assert!( linter_path.is_file(), "`bevy_lint` could not be found at {}, make sure to build it with `cargo build -p bevy_lint --bin bevy_lint`", linter_path.display(), ); - assert!( - driver_path.is_file(), - "`bevy_lint_driver` could not be found at {}, make sure to build it with `cargo build -p bevy_lint --bin bevy_lint_driver`", - driver_path.display(), - ); let mut config = Config { - // When `host` is `None`, `ui_test` will attempt to auto-discover the host by calling - // `program -vV`. Unfortunately, `bevy_lint_driver` does not yet support the version flag, - // so we manually specify the host as an empty string. This means that, for now, host- - // specific configuration in UI tests will not work. - host: Some(String::new()), + // We need to specify the host tuple manually, because if we don't then `ui_test` will try + // running `bevy_lint -vV` to discover the host and promptly error because `bevy_lint` + // doesn't recognize the `-vV` flag. + host: Some(host_tuple()), program: CommandBuilder { - // We don't need `rustup run` here because we're already using the correct toolchain - // due to `rust-toolchain.toml`. - program: driver_path.into(), - args: vec![ - // `bevy_lint_driver` expects the first argument to be the path to `rustc`. - "rustc".into(), - // This is required so that `ui_test` can parse warnings and errors. - "--error-format=json".into(), - ], - out_dir_flag: Some("--out-dir".into()), - input_file_flag: None, + program: linter_path.into(), + args: vec!["--color=never".into(), "--quiet".into()], + out_dir_flag: Some("--target-dir".into()), + input_file_flag: Some("--manifest-path".into()), envs: Vec::new(), - cfg_flag: Some("--print=cfg".into()), + cfg_flag: None, }, out_dir: PathBuf::from("../target/ui"), - ..Config::rustc(Path::new("tests/ui-cargo")) + ..Config::cargo(Path::new("tests/ui-cargo")) }; let defaults = config.comment_defaults.base(); @@ -63,32 +42,6 @@ fn main() { defaults.require_annotations = None.into(); - // This sets the '--manifest-path' flag - config.program.input_file_flag = CommandBuilder::cargo().input_file_flag; - config.program.out_dir_flag = CommandBuilder::cargo().out_dir_flag; - // Do not print cargo log messages - config.program.args = vec!["--quiet".into(), "--color".into(), "never".into()]; - - let current_exe_path = env::current_exe().unwrap(); - let deps_path = current_exe_path.parent().unwrap(); - let profile_path = deps_path.parent().unwrap(); - - // Specify the binary to use when executing tests with this `Config` - config.program.program = profile_path.join(if cfg!(windows) { - "bevy_lint_driver.exe" - } else { - "bevy_lint_driver" - }); - - config.program.program.set_file_name(if cfg!(windows) { - "bevy_lint.exe" - } else { - "bevy_lint" - }); - - // this clears the default `--edition` flag - config.comment_defaults.base().custom.clear(); - // Run this `Config` for all paths that end with `Cargo.toml` resulting // only in the `Cargo` lints. ui_test::run_tests_generic( @@ -102,3 +55,20 @@ fn main() { ) .unwrap(); } + +/// Queries the host tuple from `rustc` and returns it as a string. +fn host_tuple() -> String { + let output = Command::new("rustc") + .arg("--print=host-tuple") + // Show errors directly to the user, rather than capturing them. + .stderr(Stdio::inherit()) + .output() + .expect("failed to run `rustc --print=host-tuple`"); + + // `rustc` only works with UTF-8, so it's safe to error if invalid UTF-8 is found. + str::from_utf8(&output.stdout) + .expect("`rustc --print=host-tuple` did not emit valid UTF-8") + // Remove the trailing `\n`. + .trim_end() + .to_string() +} From 7a818085553f604d1d88fba3be4638eecb108df0 Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Sat, 1 Nov 2025 13:35:35 -0400 Subject: [PATCH 08/21] chore: create the `#@exit-status: CODE` annotation This lets us verify a specific exit code is set by `bevy_lint`, rather than just ignoring it completely. --- .../long_version_format/fail/Cargo.stderr | 8 ++++---- .../long_version_format/fail/Cargo.toml | 2 ++ .../long_version_format/pass/Cargo.toml | 2 ++ .../short_version_format/fail/Cargo.stderr | 8 ++++---- .../short_version_format/fail/Cargo.toml | 2 ++ .../short_version_format/pass/Cargo.toml | 2 ++ bevy_lint/tests/ui_cargo.rs | 19 ++++++++++++++----- 7 files changed, 30 insertions(+), 13 deletions(-) diff --git a/bevy_lint/tests/ui-cargo/duplicate_bevy_dependencies/long_version_format/fail/Cargo.stderr b/bevy_lint/tests/ui-cargo/duplicate_bevy_dependencies/long_version_format/fail/Cargo.stderr index e9ee6af4..a48e266c 100644 --- a/bevy_lint/tests/ui-cargo/duplicate_bevy_dependencies/long_version_format/fail/Cargo.stderr +++ b/bevy_lint/tests/ui-cargo/duplicate_bevy_dependencies/long_version_format/fail/Cargo.stderr @@ -1,13 +1,13 @@ error: multiple versions of the `bevy` crate found - --> Cargo.toml:12:26 + --> Cargo.toml:14:26 | -12 | leafwing-input-manager = "0.13" +14 | leafwing-input-manager = "0.13" | ^^^^^^ | help: expected all crates to use `bevy` 0.17.2, but `leafwing-input-manager` uses `bevy` ^0.13 - --> Cargo.toml:11:8 + --> Cargo.toml:13:8 | -11 | bevy = { version = "0.17.2", default-features = false } +13 | bevy = { version = "0.17.2", default-features = false } | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ note: the lint level is defined here --> src/main.rs:3:9 diff --git a/bevy_lint/tests/ui-cargo/duplicate_bevy_dependencies/long_version_format/fail/Cargo.toml b/bevy_lint/tests/ui-cargo/duplicate_bevy_dependencies/long_version_format/fail/Cargo.toml index 99ce600b..217fa32a 100644 --- a/bevy_lint/tests/ui-cargo/duplicate_bevy_dependencies/long_version_format/fail/Cargo.toml +++ b/bevy_lint/tests/ui-cargo/duplicate_bevy_dependencies/long_version_format/fail/Cargo.toml @@ -1,3 +1,5 @@ +#@exit-status: 101 + [package] name = "multiple-bevy-versions" publish = false diff --git a/bevy_lint/tests/ui-cargo/duplicate_bevy_dependencies/long_version_format/pass/Cargo.toml b/bevy_lint/tests/ui-cargo/duplicate_bevy_dependencies/long_version_format/pass/Cargo.toml index 8e3bd433..cd84e590 100644 --- a/bevy_lint/tests/ui-cargo/duplicate_bevy_dependencies/long_version_format/pass/Cargo.toml +++ b/bevy_lint/tests/ui-cargo/duplicate_bevy_dependencies/long_version_format/pass/Cargo.toml @@ -1,3 +1,5 @@ +#@check-pass + [package] name = "single-bevy-version" publish = false diff --git a/bevy_lint/tests/ui-cargo/duplicate_bevy_dependencies/short_version_format/fail/Cargo.stderr b/bevy_lint/tests/ui-cargo/duplicate_bevy_dependencies/short_version_format/fail/Cargo.stderr index fadaf6c5..dd3b96a8 100644 --- a/bevy_lint/tests/ui-cargo/duplicate_bevy_dependencies/short_version_format/fail/Cargo.stderr +++ b/bevy_lint/tests/ui-cargo/duplicate_bevy_dependencies/short_version_format/fail/Cargo.stderr @@ -1,13 +1,13 @@ error: multiple versions of the `bevy` crate found - --> Cargo.toml:12:26 + --> Cargo.toml:14:26 | -12 | leafwing-input-manager = "0.13" +14 | leafwing-input-manager = "0.13" | ^^^^^^ | help: expected all crates to use `bevy` 0.17.2, but `leafwing-input-manager` uses `bevy` ^0.13 - --> Cargo.toml:11:8 + --> Cargo.toml:13:8 | -11 | bevy = "0.17.2" +13 | bevy = "0.17.2" | ^^^^^^^^ note: the lint level is defined here --> src/main.rs:3:9 diff --git a/bevy_lint/tests/ui-cargo/duplicate_bevy_dependencies/short_version_format/fail/Cargo.toml b/bevy_lint/tests/ui-cargo/duplicate_bevy_dependencies/short_version_format/fail/Cargo.toml index 8509627f..6d8324d3 100644 --- a/bevy_lint/tests/ui-cargo/duplicate_bevy_dependencies/short_version_format/fail/Cargo.toml +++ b/bevy_lint/tests/ui-cargo/duplicate_bevy_dependencies/short_version_format/fail/Cargo.toml @@ -1,3 +1,5 @@ +#@exit-status: 101 + [package] name = "multiple-bevy-versions" publish = false diff --git a/bevy_lint/tests/ui-cargo/duplicate_bevy_dependencies/short_version_format/pass/Cargo.toml b/bevy_lint/tests/ui-cargo/duplicate_bevy_dependencies/short_version_format/pass/Cargo.toml index ef9d53bb..accd764f 100644 --- a/bevy_lint/tests/ui-cargo/duplicate_bevy_dependencies/short_version_format/pass/Cargo.toml +++ b/bevy_lint/tests/ui-cargo/duplicate_bevy_dependencies/short_version_format/pass/Cargo.toml @@ -1,3 +1,5 @@ +#@check-pass + [package] name = "single-bevy-version" publish = false diff --git a/bevy_lint/tests/ui_cargo.rs b/bevy_lint/tests/ui_cargo.rs index 1e6451ed..77009cff 100644 --- a/bevy_lint/tests/ui_cargo.rs +++ b/bevy_lint/tests/ui_cargo.rs @@ -4,7 +4,7 @@ use std::{ process::{Command, Stdio}, }; -use ui_test::{CommandBuilder, Config, status_emitter}; +use ui_test::{CommandBuilder, Config, OptWithLine, status_emitter}; // This is set by Cargo to the absolute paths of `bevy_lint` and `bevy_lint_driver`. const LINTER_PATH: &str = env!("CARGO_BIN_EXE_bevy_lint"); @@ -36,12 +36,21 @@ fn main() { }; let defaults = config.comment_defaults.base(); - // The driver returns a '101' on error. - // This allows for any status code to be considered a success. - defaults.exit_status = None.into(); - defaults.require_annotations = None.into(); + // Create the `#@exit-status: CODE` annotation. This can be used to ensure a UI test exits with + // a specific exit code (e.g. `bevy_lint` exits with code 101 when a denied lint is found). + config + .custom_comments + .insert("exit-status", |parser, args, _span| { + parser.exit_status = OptWithLine::new( + args.content + .parse() + .expect("expected `i32` as input for `exit-status`"), + args.span, + ); + }); + // Run this `Config` for all paths that end with `Cargo.toml` resulting // only in the `Cargo` lints. ui_test::run_tests_generic( From 331f2e3b77dd9a853b5a1e84d7967b5a9c407f28 Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Sat, 1 Nov 2025 13:45:53 -0400 Subject: [PATCH 09/21] chore: add comment why we disable `require_annotations` --- bevy_lint/tests/ui_cargo.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bevy_lint/tests/ui_cargo.rs b/bevy_lint/tests/ui_cargo.rs index 77009cff..16d08edc 100644 --- a/bevy_lint/tests/ui_cargo.rs +++ b/bevy_lint/tests/ui_cargo.rs @@ -35,8 +35,9 @@ fn main() { ..Config::cargo(Path::new("tests/ui-cargo")) }; - let defaults = config.comment_defaults.base(); - defaults.require_annotations = None.into(); + // We haven't found a way to get error annotations like `#~v ERROR: msg` to work, so we disable + // the requirement for them. + config.comment_defaults.base().require_annotations = None.into(); // Create the `#@exit-status: CODE` annotation. This can be used to ensure a UI test exits with // a specific exit code (e.g. `bevy_lint` exits with code 101 when a denied lint is found). From d80936b9a39c107bb963957e4bf9bab9fdac2e23 Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Sat, 1 Nov 2025 13:52:39 -0400 Subject: [PATCH 10/21] chore: support argument parsing in cargo ui tests Since `run_tests_generic()` doesn't parse arguments, I took the contents of `run_tests()` and inlined them here. --- bevy_lint/tests/ui_cargo.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/bevy_lint/tests/ui_cargo.rs b/bevy_lint/tests/ui_cargo.rs index 16d08edc..e54f392c 100644 --- a/bevy_lint/tests/ui_cargo.rs +++ b/bevy_lint/tests/ui_cargo.rs @@ -4,7 +4,7 @@ use std::{ process::{Command, Stdio}, }; -use ui_test::{CommandBuilder, Config, OptWithLine, status_emitter}; +use ui_test::{Args, CommandBuilder, Config, Format, OptWithLine, status_emitter::{self, StatusEmitter}}; // This is set by Cargo to the absolute paths of `bevy_lint` and `bevy_lint_driver`. const LINTER_PATH: &str = env!("CARGO_BIN_EXE_bevy_lint"); @@ -52,6 +52,21 @@ fn main() { ); }); + let args = Args::test().unwrap(); + + if let Format::Pretty = args.format { + println!( + "Compiler: {}", + config.program.display().to_string().replace('\\', "/") + ); + } + + let name = config.root_dir.display().to_string().replace('\\', "/"); + + let emitter: Box = args.format.into(); + + config.with_args(&args); + // Run this `Config` for all paths that end with `Cargo.toml` resulting // only in the `Cargo` lints. ui_test::run_tests_generic( @@ -61,7 +76,7 @@ fn main() { .then(|| ui_test::default_any_file_filter(path, config)) }, |_config, _file_contents| {}, - status_emitter::Text::verbose(), + (emitter, status_emitter::Gha { name, group: true }), ) .unwrap(); } From e8ca61c64c9d75fc2605f82d2b9905e99bd74a6e Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Wed, 12 Nov 2025 09:58:10 -0500 Subject: [PATCH 11/21] refactor: rustfmt --- bevy_lint/tests/ui.rs | 11 +++++++---- bevy_lint/tests/ui_cargo.rs | 5 ++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/bevy_lint/tests/ui.rs b/bevy_lint/tests/ui.rs index caf96c06..1d83df57 100644 --- a/bevy_lint/tests/ui.rs +++ b/bevy_lint/tests/ui.rs @@ -46,10 +46,13 @@ fn main() { // Give UI tests access to all crate dependencies in the `dependencies` folder. This lets UI // tests import `bevy`. let revisioned = config.comment_defaults.base(); - revisioned.set_custom("dependencies", DependencyBuilder { - crate_manifest_path: PathBuf::from("tests/dependencies/Cargo.toml"), - ..Default::default() - }); + revisioned.set_custom( + "dependencies", + DependencyBuilder { + crate_manifest_path: PathBuf::from("tests/dependencies/Cargo.toml"), + ..Default::default() + }, + ); run_tests(config).unwrap(); } diff --git a/bevy_lint/tests/ui_cargo.rs b/bevy_lint/tests/ui_cargo.rs index e54f392c..a91d79ba 100644 --- a/bevy_lint/tests/ui_cargo.rs +++ b/bevy_lint/tests/ui_cargo.rs @@ -4,7 +4,10 @@ use std::{ process::{Command, Stdio}, }; -use ui_test::{Args, CommandBuilder, Config, Format, OptWithLine, status_emitter::{self, StatusEmitter}}; +use ui_test::{ + Args, CommandBuilder, Config, Format, OptWithLine, + status_emitter::{self, StatusEmitter}, +}; // This is set by Cargo to the absolute paths of `bevy_lint` and `bevy_lint_driver`. const LINTER_PATH: &str = env!("CARGO_BIN_EXE_bevy_lint"); From 7f468f617a21a227a6cb0a49b30ba406eafdc3e5 Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Wed, 12 Nov 2025 10:12:39 -0500 Subject: [PATCH 12/21] refactor: put `host_tuple()` into `test_utils` module --- bevy_lint/tests/test_utils/mod.rs | 18 ++++++++++++++++++ bevy_lint/tests/ui.rs | 26 ++++---------------------- bevy_lint/tests/ui_cargo.rs | 22 +++------------------- 3 files changed, 25 insertions(+), 41 deletions(-) create mode 100644 bevy_lint/tests/test_utils/mod.rs diff --git a/bevy_lint/tests/test_utils/mod.rs b/bevy_lint/tests/test_utils/mod.rs new file mode 100644 index 00000000..082d1f61 --- /dev/null +++ b/bevy_lint/tests/test_utils/mod.rs @@ -0,0 +1,18 @@ +use std::process::{Command, Stdio}; + +/// Queries the host tuple from `rustc` and returns it as a string. +pub fn host_tuple() -> String { + let output = Command::new("rustc") + .arg("--print=host-tuple") + // Show errors directly to the user, rather than capturing them. + .stderr(Stdio::inherit()) + .output() + .expect("failed to run `rustc --print=host-tuple`"); + + // `rustc` only works with UTF-8, so it's safe to error if invalid UTF-8 is found. + str::from_utf8(&output.stdout) + .expect("`rustc --print=host-tuple` did not emit valid UTF-8") + // Remove the trailing `\n`. + .trim_end() + .to_string() +} diff --git a/bevy_lint/tests/ui.rs b/bevy_lint/tests/ui.rs index 1d83df57..a2e524e0 100644 --- a/bevy_lint/tests/ui.rs +++ b/bevy_lint/tests/ui.rs @@ -1,7 +1,6 @@ -use std::{ - path::{Path, PathBuf}, - process::{Command, Stdio}, -}; +mod test_utils; + +use std::path::{Path, PathBuf}; use ui_test::{CommandBuilder, Config, dependencies::DependencyBuilder, run_tests}; @@ -23,7 +22,7 @@ fn main() { // doesn't realize `bevy_lint_driver` expects its first argument to be the path to `rustc`. // If `ui_test` ran `bevy_lint_driver rustc -vV` everything would work, but it's not smart // enough to do that. - host: Some(host_tuple()), + host: Some(test_utils::host_tuple()), program: CommandBuilder { // We don't need `rustup run` here because we're already using the correct toolchain // due to `rust-toolchain.toml`. @@ -56,20 +55,3 @@ fn main() { run_tests(config).unwrap(); } - -/// Queries the host tuple from `rustc` and returns it as a string. -fn host_tuple() -> String { - let output = Command::new("rustc") - .arg("--print=host-tuple") - // Show errors directly to the user, rather than capturing them. - .stderr(Stdio::inherit()) - .output() - .expect("failed to run `rustc --print=host-tuple`"); - - // `rustc` only works with UTF-8, so it's safe to error if invalid UTF-8 is found. - str::from_utf8(&output.stdout) - .expect("`rustc --print=host-tuple` did not emit valid UTF-8") - // Remove the trailing `\n`. - .trim_end() - .to_string() -} diff --git a/bevy_lint/tests/ui_cargo.rs b/bevy_lint/tests/ui_cargo.rs index a91d79ba..c28696ab 100644 --- a/bevy_lint/tests/ui_cargo.rs +++ b/bevy_lint/tests/ui_cargo.rs @@ -1,7 +1,8 @@ +mod test_utils; + use std::{ env, path::{Path, PathBuf}, - process::{Command, Stdio}, }; use ui_test::{ @@ -25,7 +26,7 @@ fn main() { // We need to specify the host tuple manually, because if we don't then `ui_test` will try // running `bevy_lint -vV` to discover the host and promptly error because `bevy_lint` // doesn't recognize the `-vV` flag. - host: Some(host_tuple()), + host: Some(test_utils::host_tuple()), program: CommandBuilder { program: linter_path.into(), args: vec!["--color=never".into(), "--quiet".into()], @@ -83,20 +84,3 @@ fn main() { ) .unwrap(); } - -/// Queries the host tuple from `rustc` and returns it as a string. -fn host_tuple() -> String { - let output = Command::new("rustc") - .arg("--print=host-tuple") - // Show errors directly to the user, rather than capturing them. - .stderr(Stdio::inherit()) - .output() - .expect("failed to run `rustc --print=host-tuple`"); - - // `rustc` only works with UTF-8, so it's safe to error if invalid UTF-8 is found. - str::from_utf8(&output.stdout) - .expect("`rustc --print=host-tuple` did not emit valid UTF-8") - // Remove the trailing `\n`. - .trim_end() - .to_string() -} From 1a279ce6f4150cc1d478004eca7e419f07b62e8f Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Wed, 12 Nov 2025 10:33:09 -0500 Subject: [PATCH 13/21] fix: try converting unix to windows paths on windows --- bevy_lint/tests/test_utils/mod.rs | 26 +++++++++++++++++++++++++- bevy_lint/tests/ui.rs | 10 +++++++--- bevy_lint/tests/ui_cargo.rs | 6 ++++-- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/bevy_lint/tests/test_utils/mod.rs b/bevy_lint/tests/test_utils/mod.rs index 082d1f61..7679a8ac 100644 --- a/bevy_lint/tests/test_utils/mod.rs +++ b/bevy_lint/tests/test_utils/mod.rs @@ -1,4 +1,7 @@ -use std::process::{Command, Stdio}; +use std::{ + path::{Path, PathBuf}, + process::{Command, Stdio}, +}; /// Queries the host tuple from `rustc` and returns it as a string. pub fn host_tuple() -> String { @@ -16,3 +19,24 @@ pub fn host_tuple() -> String { .trim_end() .to_string() } + +pub trait PathExt { + /// Converts a UTF-8 Unix path to a native path. + /// + /// If run on Windows, this will replace all forward slashes `/` with backslashes `\`. Else, + /// this will do nothing. + /// + /// This will return [`None`] if the Unix path is not valid UTF-8. + fn unix_to_native(&self) -> Option; +} + +impl PathExt for Path { + fn unix_to_native(&self) -> Option { + if cfg!(windows) { + self.to_str() + .map(|path| PathBuf::from(path.replace("/", "\\"))) + } else { + Some(self.to_path_buf()) + } + } +} diff --git a/bevy_lint/tests/ui.rs b/bevy_lint/tests/ui.rs index a2e524e0..351ed7d1 100644 --- a/bevy_lint/tests/ui.rs +++ b/bevy_lint/tests/ui.rs @@ -4,6 +4,8 @@ use std::path::{Path, PathBuf}; use ui_test::{CommandBuilder, Config, dependencies::DependencyBuilder, run_tests}; +use self::test_utils::PathExt; + // This is set by Cargo to the absolute path of `bevy_lint_driver`. const DRIVER_PATH: &str = env!("CARGO_BIN_EXE_bevy_lint_driver"); @@ -38,8 +40,8 @@ fn main() { envs: Vec::new(), cfg_flag: Some("--print=cfg".into()), }, - out_dir: PathBuf::from("../target/ui"), - ..Config::rustc(Path::new("tests/ui")) + out_dir: Path::new("../target/ui").unix_to_native().unwrap(), + ..Config::rustc(Path::new("tests/ui").unix_to_native().unwrap()) }; // Give UI tests access to all crate dependencies in the `dependencies` folder. This lets UI @@ -48,7 +50,9 @@ fn main() { revisioned.set_custom( "dependencies", DependencyBuilder { - crate_manifest_path: PathBuf::from("tests/dependencies/Cargo.toml"), + crate_manifest_path: PathBuf::from("tests/dependencies/Cargo.toml") + .unix_to_native() + .unwrap(), ..Default::default() }, ); diff --git a/bevy_lint/tests/ui_cargo.rs b/bevy_lint/tests/ui_cargo.rs index c28696ab..eba46185 100644 --- a/bevy_lint/tests/ui_cargo.rs +++ b/bevy_lint/tests/ui_cargo.rs @@ -10,6 +10,8 @@ use ui_test::{ status_emitter::{self, StatusEmitter}, }; +use self::test_utils::PathExt; + // This is set by Cargo to the absolute paths of `bevy_lint` and `bevy_lint_driver`. const LINTER_PATH: &str = env!("CARGO_BIN_EXE_bevy_lint"); @@ -35,8 +37,8 @@ fn main() { envs: Vec::new(), cfg_flag: None, }, - out_dir: PathBuf::from("../target/ui"), - ..Config::cargo(Path::new("tests/ui-cargo")) + out_dir: PathBuf::from("../target/ui").unix_to_native().unwrap(), + ..Config::cargo(Path::new("tests/ui-cargo").unix_to_native().unwrap()) }; // We haven't found a way to get error annotations like `#~v ERROR: msg` to work, so we disable From 30386b14dd008346cd35eb9c2bb7ef9b76f54d5d Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Wed, 12 Nov 2025 10:33:34 -0500 Subject: [PATCH 14/21] fix: use `to_owned()` rather than `to_string()` --- bevy_lint/tests/test_utils/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bevy_lint/tests/test_utils/mod.rs b/bevy_lint/tests/test_utils/mod.rs index 7679a8ac..48e2978b 100644 --- a/bevy_lint/tests/test_utils/mod.rs +++ b/bevy_lint/tests/test_utils/mod.rs @@ -17,7 +17,7 @@ pub fn host_tuple() -> String { .expect("`rustc --print=host-tuple` did not emit valid UTF-8") // Remove the trailing `\n`. .trim_end() - .to_string() + .to_owned() } pub trait PathExt { From 71991322320b35a2ce2b6baa82163dcede18d0f0 Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Wed, 12 Nov 2025 10:39:06 -0500 Subject: [PATCH 15/21] fix: quiet the `disallowed_macros` lint for tests The linter's `assert!()` only works when the Rust compiler's diagnostics infra is setup. --- bevy_lint/tests/ui.rs | 5 +++++ bevy_lint/tests/ui_cargo.rs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/bevy_lint/tests/ui.rs b/bevy_lint/tests/ui.rs index 351ed7d1..b3ac3fe7 100644 --- a/bevy_lint/tests/ui.rs +++ b/bevy_lint/tests/ui.rs @@ -1,3 +1,8 @@ +#![expect( + clippy::disallowed_macros, + reason = "`bevy_lint`'s macros are intended for lints, not tests", +)] + mod test_utils; use std::path::{Path, PathBuf}; diff --git a/bevy_lint/tests/ui_cargo.rs b/bevy_lint/tests/ui_cargo.rs index eba46185..022dc9dd 100644 --- a/bevy_lint/tests/ui_cargo.rs +++ b/bevy_lint/tests/ui_cargo.rs @@ -1,3 +1,8 @@ +#![expect( + clippy::disallowed_macros, + reason = "`bevy_lint`'s macros are intended for lints, not tests", +)] + mod test_utils; use std::{ From cf7acdf710a1ffd43f7dca449a6ca23439af1c8f Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Wed, 12 Nov 2025 13:12:10 -0500 Subject: [PATCH 16/21] Revert "fix: try converting unix to windows paths on windows" This reverts commit 1a279ce6f4150cc1d478004eca7e419f07b62e8f. --- bevy_lint/tests/test_utils/mod.rs | 26 +------------------------- bevy_lint/tests/ui.rs | 10 +++------- bevy_lint/tests/ui_cargo.rs | 6 ++---- 3 files changed, 6 insertions(+), 36 deletions(-) diff --git a/bevy_lint/tests/test_utils/mod.rs b/bevy_lint/tests/test_utils/mod.rs index 48e2978b..d96c1b55 100644 --- a/bevy_lint/tests/test_utils/mod.rs +++ b/bevy_lint/tests/test_utils/mod.rs @@ -1,7 +1,4 @@ -use std::{ - path::{Path, PathBuf}, - process::{Command, Stdio}, -}; +use std::process::{Command, Stdio}; /// Queries the host tuple from `rustc` and returns it as a string. pub fn host_tuple() -> String { @@ -19,24 +16,3 @@ pub fn host_tuple() -> String { .trim_end() .to_owned() } - -pub trait PathExt { - /// Converts a UTF-8 Unix path to a native path. - /// - /// If run on Windows, this will replace all forward slashes `/` with backslashes `\`. Else, - /// this will do nothing. - /// - /// This will return [`None`] if the Unix path is not valid UTF-8. - fn unix_to_native(&self) -> Option; -} - -impl PathExt for Path { - fn unix_to_native(&self) -> Option { - if cfg!(windows) { - self.to_str() - .map(|path| PathBuf::from(path.replace("/", "\\"))) - } else { - Some(self.to_path_buf()) - } - } -} diff --git a/bevy_lint/tests/ui.rs b/bevy_lint/tests/ui.rs index b3ac3fe7..c888c132 100644 --- a/bevy_lint/tests/ui.rs +++ b/bevy_lint/tests/ui.rs @@ -9,8 +9,6 @@ use std::path::{Path, PathBuf}; use ui_test::{CommandBuilder, Config, dependencies::DependencyBuilder, run_tests}; -use self::test_utils::PathExt; - // This is set by Cargo to the absolute path of `bevy_lint_driver`. const DRIVER_PATH: &str = env!("CARGO_BIN_EXE_bevy_lint_driver"); @@ -45,8 +43,8 @@ fn main() { envs: Vec::new(), cfg_flag: Some("--print=cfg".into()), }, - out_dir: Path::new("../target/ui").unix_to_native().unwrap(), - ..Config::rustc(Path::new("tests/ui").unix_to_native().unwrap()) + out_dir: PathBuf::from("../target/ui"), + ..Config::rustc(Path::new("tests/ui")) }; // Give UI tests access to all crate dependencies in the `dependencies` folder. This lets UI @@ -55,9 +53,7 @@ fn main() { revisioned.set_custom( "dependencies", DependencyBuilder { - crate_manifest_path: PathBuf::from("tests/dependencies/Cargo.toml") - .unix_to_native() - .unwrap(), + crate_manifest_path: PathBuf::from("tests/dependencies/Cargo.toml"), ..Default::default() }, ); diff --git a/bevy_lint/tests/ui_cargo.rs b/bevy_lint/tests/ui_cargo.rs index 022dc9dd..167b89af 100644 --- a/bevy_lint/tests/ui_cargo.rs +++ b/bevy_lint/tests/ui_cargo.rs @@ -15,8 +15,6 @@ use ui_test::{ status_emitter::{self, StatusEmitter}, }; -use self::test_utils::PathExt; - // This is set by Cargo to the absolute paths of `bevy_lint` and `bevy_lint_driver`. const LINTER_PATH: &str = env!("CARGO_BIN_EXE_bevy_lint"); @@ -42,8 +40,8 @@ fn main() { envs: Vec::new(), cfg_flag: None, }, - out_dir: PathBuf::from("../target/ui").unix_to_native().unwrap(), - ..Config::cargo(Path::new("tests/ui-cargo").unix_to_native().unwrap()) + out_dir: PathBuf::from("../target/ui"), + ..Config::cargo(Path::new("tests/ui-cargo")) }; // We haven't found a way to get error annotations like `#~v ERROR: msg` to work, so we disable From 4fb3350a46f60cf16df7ae984ec6840ffdebffdc Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Wed, 12 Nov 2025 13:12:58 -0500 Subject: [PATCH 17/21] refactor: rustfmt --- bevy_lint/tests/ui.rs | 2 +- bevy_lint/tests/ui_cargo.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bevy_lint/tests/ui.rs b/bevy_lint/tests/ui.rs index c888c132..f949bace 100644 --- a/bevy_lint/tests/ui.rs +++ b/bevy_lint/tests/ui.rs @@ -1,6 +1,6 @@ #![expect( clippy::disallowed_macros, - reason = "`bevy_lint`'s macros are intended for lints, not tests", + reason = "`bevy_lint`'s macros are intended for lints, not tests" )] mod test_utils; diff --git a/bevy_lint/tests/ui_cargo.rs b/bevy_lint/tests/ui_cargo.rs index 167b89af..41161084 100644 --- a/bevy_lint/tests/ui_cargo.rs +++ b/bevy_lint/tests/ui_cargo.rs @@ -1,6 +1,6 @@ #![expect( clippy::disallowed_macros, - reason = "`bevy_lint`'s macros are intended for lints, not tests", + reason = "`bevy_lint`'s macros are intended for lints, not tests" )] mod test_utils; From c11c493006e193a2749e043be415f4abf93e2bf4 Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Wed, 12 Nov 2025 13:20:35 -0500 Subject: [PATCH 18/21] chore: add comments on workspace members in root `Cargo.toml` --- Cargo.toml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c90f0c96..2700eb53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,11 @@ [workspace] -members = ["bevy_lint", "bevy_lint/tests/dependencies"] +members = [ + # The Bevy Linter + "bevy_lint", + + # A dummy crate used to build dependencies that the linter needs for its UI tests. + "bevy_lint/tests/dependencies", +] [workspace.lints.clippy] # Prefer `str.to_owned()`, rather than `str.to_string()`. From ec3c0210fc13d15c407fc8c7dacd2a4abc0a8d3e Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Wed, 12 Nov 2025 13:49:49 -0500 Subject: [PATCH 19/21] chore: handle LF and CRLF platform differences --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..3a6f7a0c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# From: https://docs.github.com/en/github/getting-started-with-github/configuring-git-to-handle-line-endings +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto From 963e82495e2cf56fd2469adb767523c11167462f Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Wed, 12 Nov 2025 14:03:36 -0500 Subject: [PATCH 20/21] fix: re-add `bevy` to dev-dependencies for doc tests --- Cargo.lock | 1 + bevy_lint/Cargo.toml | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 137d9916..62a65749 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -819,6 +819,7 @@ dependencies = [ "anstream", "anstyle", "anyhow", + "bevy", "cargo_metadata 0.23.0", "clippy_utils", "pico-args", diff --git a/bevy_lint/Cargo.toml b/bevy_lint/Cargo.toml index 1a543cc4..5493aab1 100644 --- a/bevy_lint/Cargo.toml +++ b/bevy_lint/Cargo.toml @@ -56,6 +56,13 @@ toml = { version = "0.9.8", default-features = false, features = [ # Ensures the error messages for lints do not regress. ui_test = "0.30.1" +# Used in doc tests. +bevy = { version = "0.17.2", default-features = false, features = [ + "std", + # used for the `camera_modification_in_fixed_update` lint + "bevy_render", +] } + [lints] workspace = true From d46c6d2512130203ada91c9a8f134076773eb3d7 Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Wed, 12 Nov 2025 14:05:08 -0500 Subject: [PATCH 21/21] fix: split test ci step into two separate steps On Windows if the first `cargo test` fails, it will not be caught as long as the second `cargo test` passes. It's super jank. --- .github/workflows/ci.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb02da7a..7190ab9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,11 +48,12 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - name: Run tests - run: | - cargo test --workspace --all-features --all-targets - # Workaround for https://github.com/rust-lang/cargo/issues/6669. `--doc` is incompatible - # with `--all-targets`, so we run them separately. - cargo test --workspace --all-features --doc + run: cargo test --workspace --all-features --all-targets + + # Workaround for https://github.com/rust-lang/cargo/issues/6669. `--doc` is incompatible + # with `--all-targets`, so we run them separately. + - name: Run doc tests + run: cargo test --workspace --all-features --doc clippy: name: Check with Clippy