use super::*;
use std::path::{Path, PathBuf};
fn args(xs: &[&str]) -> Vec<String> {
xs.iter().map(|s| s.to_string()).collect()
}
struct TempDir(PathBuf);
impl TempDir {
fn new() -> Self {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let dir = std::env::temp_dir().join(format!("fdl-env-test-{pid}-{n}"));
std::fs::create_dir_all(&dir).expect("tempdir creation");
TempDir(dir)
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn touch(path: &Path, contents: &str) {
std::fs::write(path, contents).expect("write fixture");
}
#[test]
fn extract_env_flag_absent_returns_none() {
let (out, env) = extract_env_flag(&args(&["fdl", "test"])).unwrap();
assert_eq!(out, args(&["fdl", "test"]));
assert!(env.is_none());
}
#[test]
fn extract_env_flag_long_separated_form() {
let (out, env) = extract_env_flag(&args(&["fdl", "--env", "ci", "test"])).unwrap();
assert_eq!(out, args(&["fdl", "test"]));
assert_eq!(env.as_deref(), Some("ci"));
}
#[test]
fn extract_env_flag_equals_form() {
let (out, env) = extract_env_flag(&args(&["fdl", "--env=ci", "test"])).unwrap();
assert_eq!(out, args(&["fdl", "test"]));
assert_eq!(env.as_deref(), Some("ci"));
}
#[test]
fn extract_env_flag_scans_anywhere() {
let (out, env) = extract_env_flag(&args(&["fdl", "test", "--env", "prod"])).unwrap();
assert_eq!(out, args(&["fdl", "test"]));
assert_eq!(env.as_deref(), Some("prod"));
}
#[test]
fn extract_env_flag_missing_value_errors() {
let err = extract_env_flag(&args(&["fdl", "--env"])).unwrap_err();
assert!(err.contains("--env requires a value"), "got: {err}");
}
#[test]
fn extract_env_flag_empty_equals_errors() {
let err = extract_env_flag(&args(&["fdl", "--env="])).unwrap_err();
assert!(err.contains("requires a value"), "got: {err}");
}
#[test]
fn extract_env_flag_value_looks_like_flag_errors() {
let err = extract_env_flag(&args(&["fdl", "--env", "--help"])).unwrap_err();
assert!(err.contains("--env requires a value"), "got: {err}");
}
#[test]
fn extract_env_flag_duplicate_errors() {
let err = extract_env_flag(&args(&["fdl", "--env", "ci", "--env", "prod"])).unwrap_err();
assert!(err.contains("more than once"), "got: {err}");
}
#[test]
fn extract_env_flag_duplicate_mixed_forms_errors() {
let err = extract_env_flag(&args(&["fdl", "--env=ci", "--env", "prod"])).unwrap_err();
assert!(err.contains("more than once"), "got: {err}");
}
#[test]
fn extract_gpus_flag_absent_returns_none() {
let (out, spec) = extract_gpus_flag(&args(&["fdl", "test"])).unwrap();
assert_eq!(out, args(&["fdl", "test"]));
assert!(spec.is_none());
}
#[test]
fn extract_gpus_flag_long_separated_form() {
let (out, spec) = extract_gpus_flag(&args(&["fdl", "--gpus", "0,1", "train"])).unwrap();
assert_eq!(out, args(&["fdl", "train"]));
assert_eq!(spec, Some(gpus::GpusSpec::List(vec![0, 1])));
}
#[test]
fn extract_gpus_flag_equals_form() {
let (out, spec) = extract_gpus_flag(&args(&["fdl", "--gpus=0,2", "train"])).unwrap();
assert_eq!(out, args(&["fdl", "train"]));
assert_eq!(spec, Some(gpus::GpusSpec::List(vec![0, 2])));
}
#[test]
fn extract_gpus_flag_all_keyword() {
let (out, spec) = extract_gpus_flag(&args(&["fdl", "--gpus", "all", "train"])).unwrap();
assert_eq!(out, args(&["fdl", "train"]));
assert_eq!(spec, Some(gpus::GpusSpec::All));
}
#[test]
fn extract_gpus_flag_scans_anywhere() {
let (out, spec) =
extract_gpus_flag(&args(&["fdl", "train", "--gpus", "0,1", "--epochs=5"])).unwrap();
assert_eq!(out, args(&["fdl", "train", "--epochs=5"]));
assert_eq!(spec, Some(gpus::GpusSpec::List(vec![0, 1])));
}
#[test]
fn extract_gpus_flag_missing_value_errors() {
let err = extract_gpus_flag(&args(&["fdl", "--gpus"])).unwrap_err();
assert!(err.contains("requires a value"), "got: {err}");
}
#[test]
fn extract_gpus_flag_empty_equals_errors() {
let err = extract_gpus_flag(&args(&["fdl", "--gpus="])).unwrap_err();
assert!(err.contains("requires a value"), "got: {err}");
}
#[test]
fn extract_gpus_flag_value_looks_like_flag_errors() {
let err = extract_gpus_flag(&args(&["fdl", "--gpus", "--help"])).unwrap_err();
assert!(err.contains("requires a value"), "got: {err}");
}
#[test]
fn extract_gpus_flag_stops_at_dashdash() {
let (out, spec) =
extract_gpus_flag(&args(&["fdl", "run", "--", "--gpus", "0,1"])).unwrap();
assert_eq!(out, args(&["fdl", "run", "--", "--gpus", "0,1"]));
assert!(spec.is_none());
}
#[test]
fn extract_gpus_flag_duplicate_errors() {
let err =
extract_gpus_flag(&args(&["fdl", "--gpus", "0,1", "--gpus", "2,3"])).unwrap_err();
assert!(err.contains("more than once"), "got: {err}");
}
#[test]
fn extract_gpus_flag_duplicate_mixed_forms_errors() {
let err =
extract_gpus_flag(&args(&["fdl", "--gpus=0,1", "--gpus", "2,3"])).unwrap_err();
assert!(err.contains("more than once"), "got: {err}");
}
#[test]
fn extract_gpus_flag_propagates_parse_error() {
let err = extract_gpus_flag(&args(&["fdl", "--gpus", "0,abc"])).unwrap_err();
assert!(err.contains("cannot parse"), "got: {err}");
assert!(err.contains("abc"), "got: {err}");
}
#[test]
fn extract_at_env_absent_returns_none() {
let (out, env) = extract_at_env(&args(&["fdl", "test"])).unwrap();
assert_eq!(out, args(&["fdl", "test"]));
assert!(env.is_none());
}
#[test]
fn extract_at_env_consumes_leading_token() {
let (out, env) = extract_at_env(&args(&["fdl", "@cluster", "probe"])).unwrap();
assert_eq!(out, args(&["fdl", "probe"]));
assert_eq!(env.as_deref(), Some("cluster"));
}
#[test]
fn extract_at_env_after_command_is_not_consumed() {
let (out, env) = extract_at_env(&args(&["fdl", "probe", "@cluster"])).unwrap();
assert_eq!(out, args(&["fdl", "probe", "@cluster"]));
assert!(env.is_none());
}
#[test]
fn extract_at_env_option_value_not_stolen() {
let (out, env) =
extract_at_env(&args(&["fdl", "train", "--tag", "@best"])).unwrap();
assert_eq!(out, args(&["fdl", "train", "--tag", "@best"]));
assert!(env.is_none());
}
#[test]
fn extract_at_env_stops_at_dashdash() {
let (out, env) =
extract_at_env(&args(&["fdl", "run", "--", "@literal"])).unwrap();
assert_eq!(out, args(&["fdl", "run", "--", "@literal"]));
assert!(env.is_none());
}
#[test]
fn extract_at_env_bare_sigil_errors() {
let err = extract_at_env(&args(&["fdl", "@", "test"])).unwrap_err();
assert!(err.contains("requires an env name"), "got: {err}");
}
#[test]
fn extract_at_env_duplicate_errors() {
let err = extract_at_env(&args(&["fdl", "@ci", "@prod", "test"])).unwrap_err();
assert!(err.contains("more than once"), "got: {err}");
}
#[test]
fn extract_env_flag_stops_at_dashdash() {
let (out, env) =
extract_env_flag(&args(&["fdl", "run", "--", "--env", "x"])).unwrap();
assert_eq!(out, args(&["fdl", "run", "--", "--env", "x"]));
assert!(env.is_none());
}
#[test]
fn resolve_env_at_sigil_resolves_and_strips() {
let (env, rest) =
resolve_env(&args(&["fdl", "@cluster", "probe"]), None).unwrap();
assert_eq!(env.as_deref(), Some("cluster"));
assert_eq!(rest, args(&["fdl", "probe"]));
}
#[test]
fn resolve_env_at_sigil_after_command_not_consumed() {
let (env, rest) =
resolve_env(&args(&["fdl", "probe", "@cluster"]), None).unwrap();
assert!(env.is_none());
assert_eq!(rest, args(&["fdl", "probe", "@cluster"]));
}
#[test]
fn resolve_env_does_not_validate_overlay_existence() {
let (env, rest) = resolve_env(&args(&["fdl", "@nope", "test"]), None).unwrap();
assert_eq!(env.as_deref(), Some("nope"));
assert_eq!(rest, args(&["fdl", "test"]));
let (env, _) =
resolve_env(&args(&["fdl", "--env", "nope", "test"]), None).unwrap();
assert_eq!(env.as_deref(), Some("nope"));
let (env, _) = resolve_env(&args(&["fdl", "test"]), Some("nope")).unwrap();
assert_eq!(env.as_deref(), Some("nope"));
}
#[test]
fn resolve_env_cli_selector_wins_over_env_var() {
let (env, rest) =
resolve_env(&args(&["fdl", "@prod", "test"]), Some("stage")).unwrap();
assert_eq!(env.as_deref(), Some("prod"));
assert_eq!(rest, args(&["fdl", "test"]));
}
#[test]
fn resolve_env_at_and_flag_same_value_ok() {
let (env, rest) =
resolve_env(&args(&["fdl", "@ci", "--env", "ci", "test"]), None).unwrap();
assert_eq!(env.as_deref(), Some("ci"));
assert_eq!(rest, args(&["fdl", "test"]));
}
#[test]
fn resolve_env_at_and_flag_conflict_errors() {
let err =
resolve_env(&args(&["fdl", "@ci", "--env", "prod", "test"]), None).unwrap_err();
assert!(err.contains("conflicting"), "got: {err}");
}
#[test]
fn resolve_env_env_var_used_when_no_cli_selector() {
let (env, rest) =
resolve_env(&args(&["fdl", "test"]), Some("stage")).unwrap();
assert_eq!(env.as_deref(), Some("stage"));
assert_eq!(rest, args(&["fdl", "test"]));
}
#[test]
fn resolve_env_bare_overlay_name_is_not_consumed() {
let (env, rest) = resolve_env(&args(&["fdl", "ci", "test"]), None).unwrap();
assert!(env.is_none());
assert_eq!(rest, args(&["fdl", "ci", "test"]));
}
#[test]
fn resolve_env_at_sigil_after_dashdash_not_consumed() {
let (env, rest) =
resolve_env(&args(&["fdl", "run", "--", "@ci"]), None).unwrap();
assert!(env.is_none());
assert_eq!(rest, args(&["fdl", "run", "--", "@ci"]));
}
#[test]
fn resolve_env_equals_form_consumes_single_token() {
let (env, rest) =
resolve_env(&args(&["fdl", "test", "--env=ci"]), None).unwrap();
assert_eq!(env.as_deref(), Some("ci"));
assert_eq!(rest, args(&["fdl", "test"]));
}
#[test]
fn resolve_env_no_selector_returns_none() {
let (env, rest) =
resolve_env(&args(&["fdl", "deploy", "--now"]), None).unwrap();
assert!(env.is_none());
assert_eq!(rest, args(&["fdl", "deploy", "--now"]));
}
#[test]
fn load_path_still_errors_on_missing_overlay() {
let tmp = TempDir::new();
touch(&tmp.path().join("fdl.yml"), "");
let base = tmp.path().join("fdl.yml");
let err = config::load_project_with_env(&base, Some("nope")).unwrap_err();
assert!(err.contains("nope"), "got: {err}");
assert!(err.contains("not found"), "got: {err}");
}
#[test]
fn help_env_or_note_degrades_on_missing_overlay() {
let tmp = TempDir::new();
touch(&tmp.path().join("fdl.yml"), "");
touch(&tmp.path().join("fdl.ci.yml"), "");
assert_eq!(help_env_or_note(tmp.path(), Some("ci")), Some("ci"));
assert_eq!(help_env_or_note(tmp.path(), Some("nope")), None);
assert_eq!(help_env_or_note(tmp.path(), None), None);
}
#[test]
fn extract_ansi_flags_absent_returns_none() {
let (rest, choice) = extract_ansi_flags(&args(&["fdl", "setup"])).unwrap();
assert_eq!(rest, args(&["fdl", "setup"]));
assert!(choice.is_none());
}
#[test]
fn extract_ansi_flags_ansi_forces_always() {
let (rest, choice) = extract_ansi_flags(&args(&["fdl", "--ansi", "setup"])).unwrap();
assert_eq!(rest, args(&["fdl", "setup"]));
assert_eq!(choice, Some(style::ColorChoice::Always));
}
#[test]
fn extract_ansi_flags_no_ansi_forces_never() {
let (rest, choice) = extract_ansi_flags(&args(&["fdl", "--no-ansi", "setup"])).unwrap();
assert_eq!(rest, args(&["fdl", "setup"]));
assert_eq!(choice, Some(style::ColorChoice::Never));
}
#[test]
fn extract_ansi_flags_scans_anywhere() {
let (rest, choice) =
extract_ansi_flags(&args(&["fdl", "setup", "--no-ansi"])).unwrap();
assert_eq!(rest, args(&["fdl", "setup"]));
assert_eq!(choice, Some(style::ColorChoice::Never));
}
#[test]
fn extract_ansi_flags_both_set_errors() {
let err = extract_ansi_flags(&args(&["fdl", "--ansi", "--no-ansi"])).unwrap_err();
assert!(err.contains("mutually exclusive"), "got: {err}");
}
#[test]
fn extract_ansi_flags_stops_at_dashdash() {
let (rest, choice) =
extract_ansi_flags(&args(&["fdl", "run", "--", "--no-ansi"])).unwrap();
assert_eq!(rest, args(&["fdl", "run", "--", "--no-ansi"]));
assert!(choice.is_none());
}
#[test]
fn extract_verbosity_strips_flag_before_dashdash() {
let (out, level) = extract_verbosity(&args(&["fdl", "-vv", "train"]));
assert_eq!(out, args(&["fdl", "train"]));
assert_eq!(level, Some(3));
}
#[test]
fn extract_verbosity_stops_at_dashdash() {
let (out, level) = extract_verbosity(&args(&["fdl", "run", "--", "-v"]));
assert_eq!(out, args(&["fdl", "run", "--", "-v"]));
assert!(level.is_none());
}
#[test]
fn extract_no_append_absent_returns_false() {
let (out, found) = extract_no_append(&args(&["fdl", "test"]));
assert_eq!(out, args(&["fdl", "test"]));
assert!(!found);
}
#[test]
fn extract_no_append_strips_flag_from_anywhere_before_dashdash() {
let (out, found) =
extract_no_append(&args(&["fdl", "test", "--no-append", "--", "-p", "x"]));
assert_eq!(out, args(&["fdl", "test", "--", "-p", "x"]));
assert!(found);
}
#[test]
fn extract_no_append_preserves_flag_after_dashdash() {
let (out, found) =
extract_no_append(&args(&["fdl", "test", "--", "--no-append"]));
assert_eq!(out, args(&["fdl", "test", "--", "--no-append"]));
assert!(!found);
}
#[test]
fn extract_no_prebuild_strips_flag_from_anywhere_before_dashdash() {
let (out, found) = extract_no_prebuild(&args(&[
"fdl", "@cluster", "ddp-bench", "--no-prebuild", "--mode", "nccl-sync",
]));
assert_eq!(
out,
args(&["fdl", "@cluster", "ddp-bench", "--mode", "nccl-sync"]),
);
assert!(found);
}
#[test]
fn extract_no_prebuild_preserves_flag_after_dashdash() {
let (out, found) = extract_no_prebuild(&args(&[
"fdl", "ddp-bench", "--", "--no-prebuild",
]));
assert_eq!(out, args(&["fdl", "ddp-bench", "--", "--no-prebuild"]));
assert!(!found);
}
#[test]
fn extract_no_prebuild_returns_false_when_absent() {
let (out, found) = extract_no_prebuild(&args(&["fdl", "@cluster", "train"]));
assert_eq!(out, args(&["fdl", "@cluster", "train"]));
assert!(!found);
}