use camino::Utf8PathBuf;
use rstest::fixture;
use rstest_bdd_macros::{given, scenario, then, when};
use std::cell::{Cell, RefCell};
use whitaker_installer::crate_name::CrateName;
use whitaker_installer::output::ShellSnippet;
use whitaker_installer::resolution::{
CrateResolutionOptions, EXPERIMENTAL_LINT_CRATES, LINT_CRATES, SUITE_CRATE, resolve_crates,
validate_crate_names,
};
use whitaker_installer::toolchain::parse_toolchain_channel;
#[derive(Default)]
struct CrateResolutionWorld {
specific_lints: RefCell<Vec<CrateName>>,
individual_lints: Cell<bool>,
experimental: Cell<bool>,
resolved: RefCell<Vec<CrateName>>,
}
#[fixture]
fn crate_world() -> CrateResolutionWorld {
CrateResolutionWorld::default()
}
#[given("no specific lints are requested")]
fn given_no_specific_lints(crate_world: &CrateResolutionWorld) {
crate_world.specific_lints.replace(Vec::new());
}
#[given("individual lints mode is enabled")]
fn given_individual_lints(crate_world: &CrateResolutionWorld) {
crate_world.individual_lints.set(true);
}
#[given("specific lints are requested")]
fn given_specific_lints(crate_world: &CrateResolutionWorld) {
crate_world
.specific_lints
.replace(vec![CrateName::from("module_max_lines")]);
}
#[given("experimental lints are enabled")]
fn given_experimental_enabled(crate_world: &CrateResolutionWorld) {
crate_world.experimental.set(true);
}
#[when("the crate list is resolved")]
fn when_crates_resolved(crate_world: &CrateResolutionWorld) {
let lints = crate_world.specific_lints.replace(Vec::new());
let options = CrateResolutionOptions {
individual_lints: crate_world.individual_lints.get(),
experimental: crate_world.experimental.get(),
};
let resolved = resolve_crates(&lints, &options);
crate_world.resolved.replace(resolved);
}
#[then("all lint crates are included")]
fn then_all_lints_included(crate_world: &CrateResolutionWorld) {
let resolved = crate_world.resolved.borrow();
for lint in LINT_CRATES {
assert!(
resolved.contains(&CrateName::from(*lint)),
"expected {lint} to be included"
);
}
}
#[then("the suite crate is included")]
fn then_suite_included(crate_world: &CrateResolutionWorld) {
let resolved = crate_world.resolved.borrow();
assert!(
resolved.contains(&CrateName::from(SUITE_CRATE)),
"expected suite to be included"
);
}
#[then("only the suite crate is included")]
fn then_only_suite(crate_world: &CrateResolutionWorld) {
let resolved = crate_world.resolved.borrow();
assert_eq!(resolved.len(), 1);
assert_eq!(resolved.first().map(CrateName::as_str), Some(SUITE_CRATE));
}
#[then("the suite crate is not included")]
fn then_suite_not_included(crate_world: &CrateResolutionWorld) {
let resolved = crate_world.resolved.borrow();
assert!(
!resolved.contains(&CrateName::from(SUITE_CRATE)),
"expected suite to be excluded"
);
}
#[then("experimental lints are included")]
fn then_experimental_included(crate_world: &CrateResolutionWorld) {
let resolved = crate_world.resolved.borrow();
for lint in EXPERIMENTAL_LINT_CRATES {
assert!(
resolved.contains(&CrateName::from(*lint)),
"expected experimental lint {lint} to be included"
);
}
}
#[then("only the requested lints are included")]
fn then_only_requested(crate_world: &CrateResolutionWorld) {
let resolved = crate_world.resolved.borrow();
assert_eq!(resolved.len(), 1);
assert_eq!(
resolved.first().map(CrateName::as_str),
Some("module_max_lines")
);
}
#[derive(Default)]
struct ValidationWorld {
names: RefCell<Vec<CrateName>>,
experimental: Cell<bool>,
result: Cell<Option<bool>>,
error_message: RefCell<Option<String>>,
}
#[fixture]
fn validation_world() -> ValidationWorld {
ValidationWorld::default()
}
#[given("a list of valid crate names")]
fn given_valid_names(validation_world: &ValidationWorld) {
validation_world.names.replace(vec![
CrateName::from("module_max_lines"),
CrateName::from("whitaker_suite"),
]);
}
#[given("a list containing an experimental crate name")]
fn given_experimental_name(validation_world: &ValidationWorld) {
validation_world
.names
.replace(vec![CrateName::from("rstest_helper_should_be_fixture")]);
}
#[given("experimental validation is enabled")]
fn given_experimental_validation_enabled(validation_world: &ValidationWorld) {
validation_world.experimental.set(true);
}
#[given("a list containing an unknown crate name")]
fn given_unknown_name(validation_world: &ValidationWorld) {
validation_world
.names
.replace(vec![CrateName::from("nonexistent_lint")]);
}
#[when("the names are validated")]
fn when_names_validated(validation_world: &ValidationWorld) {
let names = validation_world.names.borrow();
let options = CrateResolutionOptions {
experimental: validation_world.experimental.get(),
..CrateResolutionOptions::default()
};
match validate_crate_names(&names, &options) {
Ok(()) => validation_world.result.set(Some(true)),
Err(error) => {
validation_world
.error_message
.replace(Some(error.to_string()));
validation_world.result.set(Some(false));
}
}
}
#[then("validation succeeds")]
fn then_validation_succeeds(validation_world: &ValidationWorld) {
assert_eq!(validation_world.result.get(), Some(true));
}
#[then("validation fails with a lint not found error")]
fn then_validation_fails(validation_world: &ValidationWorld) {
assert_eq!(validation_world.result.get(), Some(false));
}
#[then("validation fails with an experimental opt-in error")]
fn then_validation_fails_experimental_opt_in(validation_world: &ValidationWorld) {
assert_eq!(validation_world.result.get(), Some(false));
let error_message = validation_world.error_message.borrow();
let error_message = error_message.as_ref().expect("error message should be set");
assert!(
error_message.contains(
"experimental lint crate rstest_helper_should_be_fixture requires --experimental"
),
"unexpected validation error: {error_message}"
);
}
#[derive(Default)]
struct ToolchainWorld {
contents: RefCell<String>,
channel: RefCell<Option<String>>,
error: Cell<bool>,
}
#[fixture]
fn toolchain_world() -> ToolchainWorld {
ToolchainWorld::default()
}
#[given("a rust-toolchain.toml with standard format")]
fn given_standard_toolchain(toolchain_world: &ToolchainWorld) {
toolchain_world.contents.replace(
r#"
[toolchain]
channel = "nightly-2026-05-28"
components = ["rust-src"]
"#
.to_owned(),
);
}
#[given("a rust-toolchain.toml with top-level channel")]
fn given_top_level_channel(toolchain_world: &ToolchainWorld) {
toolchain_world
.contents
.replace(r#"channel = "nightly-2026-05-28""#.to_owned());
}
#[given("a rust-toolchain.toml without a channel")]
fn given_no_channel(toolchain_world: &ToolchainWorld) {
toolchain_world.contents.replace(
r#"
[toolchain]
components = ["rust-src"]
"#
.to_owned(),
);
}
#[when("the toolchain is detected")]
fn when_toolchain_detected(toolchain_world: &ToolchainWorld) {
let contents = toolchain_world.contents.borrow();
match parse_toolchain_channel(&contents) {
Ok(channel) => {
toolchain_world.channel.replace(Some(channel));
}
Err(_) => toolchain_world.error.set(true),
}
}
#[then("the channel is extracted correctly")]
fn then_channel_extracted(toolchain_world: &ToolchainWorld) {
let channel = toolchain_world.channel.borrow();
assert_eq!(*channel, Some("nightly-2026-05-28".to_owned()));
}
#[then("detection fails with an invalid file error")]
fn then_detection_fails(toolchain_world: &ToolchainWorld) {
assert!(toolchain_world.error.get());
}
#[derive(Default)]
struct SnippetWorld {
path: RefCell<String>,
snippet: RefCell<Option<ShellSnippet>>,
}
#[fixture]
fn snippet_world() -> SnippetWorld {
SnippetWorld::default()
}
#[given("a target library path")]
fn given_library_path(snippet_world: &SnippetWorld) {
snippet_world
.path
.replace("/home/user/.local/share/dylint/lib".to_owned());
}
#[when("shell snippets are generated")]
fn when_snippets_generated(snippet_world: &SnippetWorld) {
let path = snippet_world.path.borrow();
let utf8_path = Utf8PathBuf::from(path.as_str());
let snippet = ShellSnippet::new(&utf8_path);
snippet_world.snippet.replace(Some(snippet));
}
#[then("bash snippet uses export syntax")]
fn then_bash_export(snippet_world: &SnippetWorld) {
let snippet = snippet_world.snippet.borrow();
let s = snippet.as_ref().expect("snippet should exist");
assert!(s.bash.starts_with("export "));
}
#[then("fish snippet uses set -gx syntax")]
fn then_fish_set(snippet_world: &SnippetWorld) {
let snippet = snippet_world.snippet.borrow();
let s = snippet.as_ref().expect("snippet should exist");
assert!(s.fish.starts_with("set -gx "));
}
#[then("PowerShell snippet uses $env syntax")]
fn then_powershell_env(snippet_world: &SnippetWorld) {
let snippet = snippet_world.snippet.borrow();
let s = snippet.as_ref().expect("snippet should exist");
assert!(s.powershell.starts_with("$env:"));
}
#[scenario(path = "tests/features/installer.feature", index = 0)]
fn scenario_resolve_suite_only_by_default(crate_world: CrateResolutionWorld) {
let _ = crate_world;
}
#[scenario(path = "tests/features/installer.feature", index = 1)]
fn scenario_resolve_individual_lints(crate_world: CrateResolutionWorld) {
let _ = crate_world;
}
#[scenario(path = "tests/features/installer.feature", index = 2)]
fn scenario_resolve_individual_lints_with_experimental(crate_world: CrateResolutionWorld) {
let _ = crate_world;
}
#[scenario(path = "tests/features/installer.feature", index = 3)]
fn scenario_resolve_specific_lints(crate_world: CrateResolutionWorld) {
let _ = crate_world;
}
#[scenario(path = "tests/features/installer.feature", index = 4)]
fn scenario_validate_known_names(validation_world: ValidationWorld) {
let _ = validation_world;
}
#[scenario(path = "tests/features/installer.feature", index = 5)]
fn scenario_reject_unknown_names(validation_world: ValidationWorld) {
let _ = validation_world;
}
#[scenario(path = "tests/features/installer.feature", index = 6)]
fn scenario_parse_standard_toolchain(toolchain_world: ToolchainWorld) {
let _ = toolchain_world;
}
#[scenario(path = "tests/features/installer.feature", index = 7)]
fn scenario_parse_top_level_channel(toolchain_world: ToolchainWorld) {
let _ = toolchain_world;
}
#[scenario(path = "tests/features/installer.feature", index = 8)]
fn scenario_reject_missing_channel(toolchain_world: ToolchainWorld) {
let _ = toolchain_world;
}
#[scenario(path = "tests/features/installer.feature", index = 9)]
fn scenario_generate_shell_snippets(snippet_world: SnippetWorld) {
let _ = snippet_world;
}
#[scenario(path = "tests/features/installer.feature", index = 19)]
fn scenario_validate_experimental_names_with_opt_in(validation_world: ValidationWorld) {
let _ = validation_world;
}
#[scenario(path = "tests/features/installer.feature", index = 20)]
fn scenario_reject_experimental_names_without_opt_in(validation_world: ValidationWorld) {
let _ = validation_world;
}