Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:
- name: Clippy
run: cargo clippy --locked --workspace --all-targets --all-features -- -D warnings
- name: Test default features
run: cargo test --locked --workspace
run: cargo test --locked -p sugar_path
- name: Test all features
run: cargo test --locked --workspace --all-features

Expand Down
62 changes: 62 additions & 0 deletions tests/cached_current_dir.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
use std::{
borrow::Cow,
env, fs,
path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
};

use sugar_path::SugarPath;

fn assert_owned_relative(target: &Path, base: &Path, expected: &Path, context: &str) {
let strict = target.relative(base);
assert_eq!(strict.as_os_str(), expected.as_os_str(), "{context} strict");
assert!(matches!(strict, Cow::Owned(_)), "{context} strict should own");

let fallible = target.try_relative(base).expect("fixture should resolve against cwd");
assert_eq!(fallible.as_os_str(), expected.as_os_str(), "{context} try");
assert!(matches!(fallible, Cow::Owned(_)), "{context} try should own");
}

struct CurrentDirGuard {
original: PathBuf,
cleanup: PathBuf,
Expand Down Expand Up @@ -41,11 +52,45 @@ fn current_directory_is_cached_only_when_requested() {

env::set_current_dir(&second).expect("enter the second temporary directory");
let second = env::current_dir().expect("read the second temporary directory");
let root = second.parent().expect("temporary directory has a parent");
let anchor = root.join("anchor");
let second_name = second.file_name().expect("second directory has a name");
let second_target = second.join("absolute-target.js");
assert_owned_relative(
Path::new("entry.js"),
&anchor,
&PathBuf::from("..").join(second_name).join("entry.js"),
"relative receiver initializes cwd",
);
assert_owned_relative(
&second_target,
Path::new("base"),
&PathBuf::from("..").join("absolute-target.js"),
"relative base uses initialized cwd",
);
assert_eq!(Path::new("entry.js").absolutize(), second.join("entry.js"));

env::set_current_dir(&third).expect("enter the third temporary directory");
let third = env::current_dir().expect("read the third temporary directory");
let expected_base = if cfg!(feature = "cached_current_dir") { &second } else { &third };
let expected_base_name = expected_base.file_name().expect("expected base has a name");
assert_owned_relative(
Path::new("entry.js"),
&anchor,
&PathBuf::from("..").join(expected_base_name).join("entry.js"),
"relative receiver observes cwd policy",
);
let expected_from_relative_base = if cfg!(feature = "cached_current_dir") {
PathBuf::from("..").join("absolute-target.js")
} else {
PathBuf::from("..").join("..").join(second_name).join("absolute-target.js")
};
assert_owned_relative(
&second_target,
Path::new("base"),
&expected_from_relative_base,
"relative base observes cwd policy",
);
assert_eq!(Path::new("entry.js").absolutize(), expected_base.join("entry.js"));

#[cfg(target_family = "windows")]
Expand All @@ -59,10 +104,27 @@ fn current_directory_is_cached_only_when_requested() {
};
let drive_relative = format!("{drive}:drive-entry.js");
let oracle = std::path::absolute(&drive_relative).expect("resolve the drive-relative oracle");
assert_eq!(oracle, third.join("drive-entry.js"));
assert_eq!(
Path::new(&drive_relative).absolutize().as_ref(),
oracle.as_path(),
"drive-relative resolution must bypass the single cached cwd",
);
assert_owned_relative(
Path::new(&drive_relative),
&anchor,
&PathBuf::from("..").join("third").join("drive-entry.js"),
"drive-relative receiver bypasses cached cwd",
);

let drive_base = format!("{drive}:drive-base");
let base_oracle = std::path::absolute(&drive_base).expect("resolve drive-relative base oracle");
assert_eq!(base_oracle, third.join("drive-base"));
assert_owned_relative(
&second_target,
Path::new(&drive_base),
&PathBuf::from("..").join("..").join("second").join("absolute-target.js"),
"drive-relative base bypasses cached cwd",
);
}
}
94 changes: 94 additions & 0 deletions tests/cached_current_dir_failure.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#![cfg(unix)]

use std::{
borrow::Cow,
env, fs,
panic::{AssertUnwindSafe, catch_unwind},
path::{Path, PathBuf},
process::Command,
time::{SystemTime, UNIX_EPOCH},
};

use sugar_path::SugarPath;

const CHILD_ENV: &str = "SUGAR_PATH_CACHED_CWD_FAILURE_ROOT";

fn assert_owned_relative(target: &Path, base: &Path, expected: &Path, context: &str) {
let fallible = target.try_relative(base).expect("fixture should resolve against cwd");
assert_eq!(fallible.as_os_str(), expected.as_os_str(), "{context} try");
assert!(matches!(fallible, Cow::Owned(_)), "{context} try should own");

let strict = target.relative(base);
assert_eq!(strict.as_os_str(), expected.as_os_str(), "{context} strict");
assert!(matches!(strict, Cow::Owned(_)), "{context} strict should own");
}

#[test]
fn failed_cwd_lookup_does_not_poison_later_relative_calls() {
if let Some(root) = env::var_os(CHILD_ENV) {
let root = PathBuf::from(root);
let doomed = root.join("doomed");
let recovery = root.join("recovery");
let later = root.join("later");
let anchor = root.join("anchor");

fs::remove_dir(&doomed).expect("remove the child's current directory");
assert!(env::current_dir().is_err());
assert!(Path::new("recovered.js").try_relative(&anchor).is_err());
assert!(
catch_unwind(AssertUnwindSafe(|| Path::new("recovered.js").relative(&anchor))).is_err(),
"strict relative should panic while cwd is unavailable",
);

env::set_current_dir(&recovery).expect("enter the recovery directory");
let recovery = env::current_dir().expect("read the recovery directory");
let anchor = recovery.parent().expect("recovery directory has a parent").join("anchor");
let recovery_name = recovery.file_name().expect("recovery directory has a name");
assert_owned_relative(
Path::new("recovered.js"),
&anchor,
&PathBuf::from("..").join(recovery_name).join("recovered.js"),
"first successful cwd lookup after failures",
);

env::set_current_dir(&later).expect("enter the later directory");
let later = env::current_dir().expect("read the later directory");
let expected_base = if cfg!(feature = "cached_current_dir") { &recovery } else { &later };
let expected_name = expected_base.file_name().expect("expected base has a name");
assert_owned_relative(
Path::new("recovered.js"),
&anchor,
&PathBuf::from("..").join(expected_name).join("recovered.js"),
"failed lookup does not poison cwd policy",
);
return;
}

let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock is after the Unix epoch")
.as_nanos();
let root =
env::temp_dir().join(format!("sugar-path-cached-cwd-failure-{}-{unique}", std::process::id()));
let doomed = root.join("doomed");
fs::create_dir_all(&doomed).expect("create the child's current directory");
fs::create_dir(root.join("recovery")).expect("create the recovery directory");
fs::create_dir(root.join("later")).expect("create the later directory");

let output = Command::new(env::current_exe().expect("find the integration-test executable"))
.args(["--exact", "failed_cwd_lookup_does_not_poison_later_relative_calls", "--nocapture"])
.env(CHILD_ENV, &root)
.current_dir(&doomed)
.output()
.expect("run the integration test in a child process");

if root.exists() {
fs::remove_dir_all(&root).expect("clean up the child's temporary directories");
}
assert!(
output.status.success(),
"child test failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
107 changes: 107 additions & 0 deletions tests/cached_current_dir_relative.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use std::{
borrow::Cow,
env, fs,
path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
};

use sugar_path::{SugarPath, SugarPathBuf};

struct CurrentDirGuard {
original: PathBuf,
cleanup: PathBuf,
}

impl Drop for CurrentDirGuard {
fn drop(&mut self) {
env::set_current_dir(&self.original).expect("restore the original current directory");
fs::remove_dir_all(&self.cleanup).expect("remove the temporary directories");
}
}

fn assert_owned_relative(target: &Path, base: &Path, expected: &Path, context: &str) {
let strict = target.relative(base);
assert_eq!(strict.as_os_str(), expected.as_os_str(), "{context} strict");
assert!(matches!(strict, Cow::Owned(_)), "{context} strict should own");

let fallible = target.try_relative(base).expect("fixture should resolve against cwd");
assert_eq!(fallible.as_os_str(), expected.as_os_str(), "{context} try");
assert!(matches!(fallible, Cow::Owned(_)), "{context} try should own");
}

#[test]
fn pure_relative_calls_initialize_and_observe_cwd_policy() {
let original = env::current_dir().expect("read the original current directory");
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock is after the Unix epoch")
.as_nanos();
let root =
env::temp_dir().join(format!("sugar-path-relative-cwd-{}-{unique}", std::process::id()));
let first = root.join("first");
let second = root.join("second");
let third = root.join("third");
fs::create_dir_all(&first).expect("create the first temporary directory");
fs::create_dir_all(&second).expect("create the second temporary directory");
fs::create_dir_all(&third).expect("create the third temporary directory");
let _guard = CurrentDirGuard { original, cleanup: root };

env::set_current_dir(&first).expect("enter the first temporary directory");
assert_owned_relative(
Path::new("pkg/assets/./index.js"),
Path::new("pkg/chunks"),
&PathBuf::from("..").join("assets").join("index.js"),
"cwd-independent preflight",
);

env::set_current_dir(&second).expect("enter the second temporary directory");
let relative_target = Path::new("../second/relative-target.js");
let relative_base = Path::new("base");
let initialized = relative_target
.try_relative(relative_base)
.expect("unequal-parent fixture should resolve against cwd");
assert_eq!(initialized.as_os_str(), PathBuf::from("..").join("relative-target.js").as_os_str(),);
assert!(matches!(initialized, Cow::Owned(_)), "initial unequal-parent result should own");

env::set_current_dir(&third).expect("enter the third temporary directory");
let third = env::current_dir().expect("read the third temporary directory");
let expected = if cfg!(feature = "cached_current_dir") {
PathBuf::from("..").join("relative-target.js")
} else {
PathBuf::from("..").join("..").join("second").join("relative-target.js")
};
assert_owned_relative(
relative_target,
relative_base,
&expected,
"unequal-parent pair observes cwd policy",
);

let expected_slash = if cfg!(feature = "cached_current_dir") {
"../relative-target.js"
} else {
"../../second/relative-target.js"
};
assert_eq!(
relative_target.relative(relative_base).into_owned().into_slash(),
expected_slash,
"strict relative-to-slash composition observes cwd policy",
);
assert_eq!(
relative_target
.try_relative(relative_base)
.expect("fixture should resolve against cwd")
.into_owned()
.into_slash(),
expected_slash,
"fallible relative-to-slash composition observes cwd policy",
);

let explicit = relative_target.relative_with(relative_base, &third);
assert_eq!(
explicit.as_os_str(),
PathBuf::from("..").join("..").join("second").join("relative-target.js").as_os_str(),
"explicit cwd must bypass ambient caching",
);
assert!(matches!(explicit, Cow::Owned(_)), "explicit unequal-parent result should own");
}
Loading