use std::path::Path;
use tempfile::TempDir;
use super::super::args::UserDepFlags;
use super::canonicalize::{canonicalize_cache_len_for_test, canonicalize_path, strip_win_prefix};
use super::error::DepfileError;
use super::parse::{
find_separator_colon, join_continuations, parse_depfile, parse_depfile_path, split_and_unescape,
};
use super::strategy::{prepare_depfile, user_depfile_destination, DepfileStrategy};
use crate::core::NormalizedPath;
fn touch(dir: &Path, name: &str) -> NormalizedPath {
let p = dir.join(name);
if let Some(parent) = p.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(&p, "").unwrap();
p.into()
}
fn canon(p: &Path) -> NormalizedPath {
strip_win_prefix(
std::fs::canonicalize(p)
.unwrap_or_else(|_| p.to_path_buf())
.into(),
)
}
#[test]
fn parse_single_line() {
let dir = TempDir::new().unwrap();
let cwd = dir.path();
let source = touch(cwd, "foo.c");
let bar_h = touch(cwd, "bar.h");
let content = "foo.o: foo.c bar.h";
let result = parse_depfile(content, &source, cwd).unwrap();
assert_eq!(result.resolved.len(), 1);
assert_eq!(result.resolved[0], canon(&bar_h));
assert!(result.unresolved.is_empty());
assert!(!result.has_computed);
}
#[test]
fn parse_continuations() {
let dir = TempDir::new().unwrap();
let cwd = dir.path();
let source = touch(cwd, "foo.c");
let bar_h = touch(cwd, "bar.h");
let baz_h = touch(cwd, "baz.h");
let content = "foo.o: foo.c bar.h \\\n baz.h";
let result = parse_depfile(content, &source, cwd).unwrap();
assert_eq!(result.resolved.len(), 2);
assert!(result.resolved.contains(&canon(&bar_h)));
assert!(result.resolved.contains(&canon(&baz_h)));
}
#[test]
fn parse_escaped_spaces() {
let content = r"foo.o: foo.c path\ with\ spaces/foo.h";
let source = Path::new("/nonexistent/foo.c");
let cwd = Path::new("/nonexistent");
let result = parse_depfile(content, source, cwd).unwrap();
assert_eq!(result.resolved.len(), 1);
let dep = &result.resolved[0];
let dep_str = dep.to_string_lossy();
assert!(
dep_str.contains("path with spaces"),
"expected unescaped space in path, got: {dep_str}"
);
}
#[test]
fn parse_multiple_targets() {
let dir = TempDir::new().unwrap();
let cwd = dir.path();
let source = touch(cwd, "foo.c");
let bar_h = touch(cwd, "bar.h");
let content = "foo.o foo.d: foo.c bar.h";
let result = parse_depfile(content, &source, cwd).unwrap();
assert_eq!(result.resolved.len(), 1);
assert_eq!(result.resolved[0], canon(&bar_h));
}
#[test]
fn parse_empty_deps() {
let dir = TempDir::new().unwrap();
let cwd = dir.path();
let source = touch(cwd, "foo.c");
let content = "foo.o: foo.c";
let result = parse_depfile(content, &source, cwd).unwrap();
assert!(result.resolved.is_empty());
}
#[test]
fn parse_relative_paths_resolved() {
let dir = TempDir::new().unwrap();
let cwd = dir.path();
let source = touch(cwd, "src/main.c");
let header = touch(cwd, "inc/util.h");
let content = "src/main.o: src/main.c inc/util.h";
let result = parse_depfile(content, &source, cwd).unwrap();
assert_eq!(result.resolved.len(), 1);
assert_eq!(result.resolved[0], canon(&header));
}
#[test]
fn parse_source_excluded() {
let dir = TempDir::new().unwrap();
let cwd = dir.path();
let source = touch(cwd, "main.c");
let alpha = touch(cwd, "alpha.h");
let beta = touch(cwd, "beta.h");
let content = "main.o: main.c alpha.h beta.h";
let result = parse_depfile(content, &source, cwd).unwrap();
assert_eq!(result.resolved.len(), 2);
assert!(result.resolved.contains(&canon(&alpha)));
assert!(result.resolved.contains(&canon(&beta)));
assert!(!result.resolved.contains(&canon(&source)));
}
#[test]
fn parse_deduplicates() {
let dir = TempDir::new().unwrap();
let cwd = dir.path();
let source = touch(cwd, "foo.c");
let bar_h = touch(cwd, "bar.h");
let content = "foo.o: foo.c bar.h bar.h";
let result = parse_depfile(content, &source, cwd).unwrap();
assert_eq!(result.resolved.len(), 1);
assert_eq!(result.resolved[0], canon(&bar_h));
}
#[test]
#[cfg(windows)]
fn parse_windows_drive_letters() {
let content = r"C:\build\foo.o: C:\src\foo.c C:\inc\bar.h";
let source = Path::new(r"C:\src\foo.c");
let cwd = Path::new(r"C:\build");
let result = parse_depfile(content, source, cwd).unwrap();
assert_eq!(result.resolved.len(), 1);
let dep = &result.resolved[0];
let dep_str = dep.to_string_lossy();
assert!(
dep_str.contains("bar.h"),
"expected bar.h in resolved, got: {dep_str}"
);
}
#[test]
#[cfg(not(windows))]
fn parse_windows_drive_letters() {
let content = r"C:\build\foo.o: C:\src\foo.c C:\inc\bar.h";
let source = Path::new(r"C:\src\foo.c");
let cwd = Path::new(r"C:\build");
let result = parse_depfile(content, source, cwd).unwrap();
let dep_strs: Vec<String> = result
.resolved
.iter()
.map(|p| p.to_string_lossy().into_owned())
.collect();
assert!(
dep_strs.iter().any(|s| s.contains("bar.h")),
"expected bar.h in resolved, got: {dep_strs:?}"
);
}
#[test]
fn parse_empty_content_errors() {
let result = parse_depfile("", Path::new("foo.c"), Path::new("/tmp"));
assert!(result.is_err());
match result.unwrap_err() {
DepfileError::Malformed(msg) => {
assert!(msg.contains("empty"), "unexpected message: {msg}");
}
other => panic!("expected Malformed, got: {other:?}"),
}
}
#[test]
fn parse_real_gcc_output() {
let dir = TempDir::new().unwrap();
let cwd = dir.path();
let source = touch(cwd, "main.c");
let config_h = touch(cwd, "config.h");
let stdio_h = touch(cwd, "usr/include/stdio.h");
let stdlib_h = touch(cwd, "usr/include/stdlib.h");
let content = format!(
"main.o: main.c config.h \\\n {} \\\n {}",
stdio_h.display(),
stdlib_h.display(),
);
let result = parse_depfile(&content, &source, cwd).unwrap();
assert_eq!(result.resolved.len(), 3);
assert!(result.resolved.contains(&canon(&config_h)));
assert!(result.resolved.contains(&canon(&stdio_h)));
assert!(result.resolved.contains(&canon(&stdlib_h)));
assert!(!result.has_computed);
assert!(result.unresolved.is_empty());
}
#[test]
fn parse_real_clang_output() {
let dir = TempDir::new().unwrap();
let cwd = dir.path();
let source = touch(cwd, "app.cpp");
let app_h = touch(cwd, "app.h");
let types_h = touch(cwd, "include/types.h");
let vector_h = touch(cwd, "usr/include/c++/vector");
let content = format!(
"app.o app.d: {} {} \\\n {} \\\n {}",
source.display(),
app_h.display(),
types_h.display(),
vector_h.display(),
);
let result = parse_depfile(&content, &source, cwd).unwrap();
assert_eq!(result.resolved.len(), 3);
assert!(result.resolved.contains(&canon(&app_h)));
assert!(result.resolved.contains(&canon(&types_h)));
assert!(result.resolved.contains(&canon(&vector_h)));
assert!(!result.has_computed);
}
#[test]
fn whitespace_only_is_malformed() {
let result = parse_depfile(" \n \t \n", Path::new("x.c"), Path::new("/tmp"));
assert!(matches!(result, Err(DepfileError::Malformed(_))));
}
#[test]
fn no_colon_is_malformed() {
let result = parse_depfile("foo.o foo.c bar.h", Path::new("foo.c"), Path::new("/tmp"));
assert!(matches!(result, Err(DepfileError::Malformed(_))));
}
#[test]
fn parse_depfile_path_reads_file() {
let dir = TempDir::new().unwrap();
let cwd = dir.path();
let source = touch(cwd, "src.c");
let hdr = touch(cwd, "hdr.h");
let depfile = cwd.join("src.d");
std::fs::write(&depfile, "src.o: src.c hdr.h").unwrap();
let result = parse_depfile_path(&depfile, &source, cwd).unwrap();
assert_eq!(result.resolved.len(), 1);
assert_eq!(result.resolved[0], canon(&hdr));
}
#[test]
fn parse_depfile_path_missing_file() {
let result = parse_depfile_path(
Path::new("/nonexistent.d"),
Path::new("x.c"),
Path::new("/tmp"),
);
assert!(matches!(result, Err(DepfileError::Io(_))));
}
#[test]
fn escaped_hash_in_path() {
let content = r"foo.o: foo.c path\#2/bar.h";
let source = Path::new("/nonexistent/foo.c");
let cwd = Path::new("/nonexistent");
let result = parse_depfile(content, source, cwd).unwrap();
assert_eq!(result.resolved.len(), 1);
let dep_str = result.resolved[0].to_string_lossy();
assert!(
dep_str.contains("path#2"),
"expected unescaped '#' in path, got: {dep_str}"
);
}
#[test]
fn crlf_continuations() {
let dir = TempDir::new().unwrap();
let cwd = dir.path();
let source = touch(cwd, "foo.c");
let bar_h = touch(cwd, "bar.h");
let content = "foo.o: foo.c \\\r\n bar.h";
let result = parse_depfile(content, &source, cwd).unwrap();
assert_eq!(result.resolved.len(), 1);
assert_eq!(result.resolved[0], canon(&bar_h));
}
#[test]
fn display_impl_for_errors() {
let io_err = DepfileError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"not found",
));
let msg = format!("{io_err}");
assert!(msg.contains("I/O error"));
let mal_err = DepfileError::Malformed("bad content".to_string());
let msg = format!("{mal_err}");
assert!(msg.contains("malformed"));
assert!(msg.contains("bad content"));
}
#[test]
fn join_continuations_replaces_with_space() {
assert_eq!(join_continuations("a \\\n b"), "a b");
assert_eq!(join_continuations("a \\\r\n b"), "a b");
}
#[test]
fn join_continuations_preserves_other_backslashes() {
assert_eq!(join_continuations(r"C:\path\file"), r"C:\path\file");
}
#[test]
fn find_separator_colon_simple() {
assert_eq!(find_separator_colon("foo.o: bar.c").unwrap(), 5);
}
#[test]
fn find_separator_colon_skips_drive_letter() {
let line = r"C:\build\foo.o: C:\src\bar.c";
let pos = find_separator_colon(line).unwrap();
assert_eq!(&line[pos..pos + 1], ":");
assert_eq!(pos, 14);
}
#[test]
fn split_and_unescape_basic() {
let tokens = split_and_unescape(" foo.c bar.h baz.h ");
assert_eq!(tokens, vec!["foo.c", "bar.h", "baz.h"]);
}
#[test]
fn split_and_unescape_escaped_space() {
let tokens = split_and_unescape(r" path\ with\ spaces/foo.h bar.h ");
assert_eq!(tokens, vec!["path with spaces/foo.h", "bar.h"]);
}
#[test]
fn split_and_unescape_escaped_hash() {
let tokens = split_and_unescape(r" file\#1.h ");
assert_eq!(tokens, vec!["file#1.h"]);
}
#[test]
fn strategy_unsupported() {
let dep_flags = UserDepFlags::default();
let (args, strategy) =
prepare_depfile(false, &dep_flags, Path::new("foo.o"), Path::new("/tmp"));
assert!(args.is_empty());
assert_eq!(strategy, DepfileStrategy::Unsupported);
}
#[test]
fn strategy_user_mf() {
let dep_flags = UserDepFlags {
has_md: true,
mf_path: Some(NormalizedPath::from("/build/deps.d")),
};
let (args, strategy) = prepare_depfile(true, &dep_flags, Path::new("foo.o"), Path::new("/tmp"));
assert!(args.is_empty());
assert_eq!(
strategy,
DepfileStrategy::UserSpecified {
path: NormalizedPath::from("/build/deps.d")
}
);
}
#[test]
fn strategy_user_md_no_mf() {
let dep_flags = UserDepFlags {
has_md: true,
mf_path: None,
};
let (args, strategy) = prepare_depfile(true, &dep_flags, Path::new("foo.o"), Path::new("/tmp"));
assert!(args.is_empty());
assert_eq!(
strategy,
DepfileStrategy::UserDefault {
path: NormalizedPath::from("foo.d")
}
);
}
#[test]
fn user_depfile_destination_returns_mf_path_when_present() {
let dep_flags = UserDepFlags {
has_md: true,
mf_path: Some(NormalizedPath::from("/build/explicit.d")),
};
assert_eq!(
user_depfile_destination(&dep_flags, Path::new("/out/foo.o")),
Some(NormalizedPath::from("/build/explicit.d")),
"explicit -MF must win over the implicit <output>.d default",
);
}
#[test]
fn user_depfile_destination_derives_default_from_output_when_md_only() {
let dep_flags = UserDepFlags {
has_md: true,
mf_path: None,
};
assert_eq!(
user_depfile_destination(&dep_flags, Path::new("/out/foo.o")),
Some(NormalizedPath::from("/out/foo.d")),
"-MD without -MF defaults to <output_stem>.d alongside the object",
);
}
#[test]
fn user_depfile_destination_none_when_user_has_no_dep_flags() {
let dep_flags = UserDepFlags::default();
assert_eq!(
user_depfile_destination(&dep_flags, Path::new("/out/foo.o")),
None,
"no user dep flags = injected strategy = not the user's depfile",
);
}
#[test]
fn strategy_injected() {
let dep_flags = UserDepFlags::default();
let (args, strategy) = prepare_depfile(true, &dep_flags, Path::new("foo.o"), Path::new("/tmp"));
assert_eq!(args.len(), 3);
assert_eq!(args[0], "-MD");
assert_eq!(args[1], "-MF");
assert!(args[2].ends_with(".d"));
match strategy {
DepfileStrategy::Injected { path } => {
assert!(path.to_string_lossy().ends_with(".d"));
assert!(path.starts_with("/tmp"));
}
other => panic!("expected Injected, got: {other:?}"),
}
}
#[test]
fn strategy_injected_adds_args() {
let dep_flags = UserDepFlags::default();
let (args, _) = prepare_depfile(true, &dep_flags, Path::new("bar.o"), Path::new("/tmp"));
assert_eq!(args[0], "-MD");
assert_eq!(args[1], "-MF");
assert!(
args[2].contains("bar"),
"expected 'bar' in path: {}",
args[2]
);
}
#[test]
fn canonicalize_path_caches_results() {
let dir = TempDir::new().unwrap();
let cwd = dir.path();
let hdr = touch(cwd, "shared-header-573a.h");
let first = canonicalize_path(&hdr, cwd);
let second = canonicalize_path(&hdr, cwd);
assert_eq!(first, second, "cached canonical output must match");
assert!(canonicalize_cache_len_for_test() > 0);
}
#[test]
fn canonicalize_path_cache_distinguishes_inputs() {
let dir = TempDir::new().unwrap();
let cwd = dir.path();
let hdr = touch(cwd, "distinct-573b.h");
let abs_input = hdr.clone();
let mut redundant_input = cwd.to_path_buf();
redundant_input.push(".");
redundant_input.push("distinct-573b.h");
let r1 = canonicalize_path(&abs_input, cwd);
let r2 = canonicalize_path(&redundant_input, cwd);
assert_eq!(
r1, r2,
"different inputs of the same file resolve identically"
);
}