From 585e34eb3e3e58e52d19eed926aaada9f1dafdc3 Mon Sep 17 00:00:00 2001 From: Yunfei He Date: Wed, 15 Jul 2026 22:29:56 +0800 Subject: [PATCH] test: preserve non-UTF-8 ambient cwd --- .agents/docs/testing-strategy.md | 2 + .github/workflows/test.yaml | 1 + tests/non_utf8_ambient_cwd.rs | 90 ++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 tests/non_utf8_ambient_cwd.rs diff --git a/.agents/docs/testing-strategy.md b/.agents/docs/testing-strategy.md index c65179d..feb11e3 100644 --- a/.agents/docs/testing-strategy.md +++ b/.agents/docs/testing-strategy.md @@ -43,6 +43,7 @@ The matrix is interaction-aware. A platform root kind must be crossed with encod - Windows drive-relative rows apply the same exact comparison before shared-context cancellation and preserve invalid-wide components while resolving against explicit borrowed and owned cwd arguments. - Unavailable Unix cwd coverage pins exact successful output and `Cow` variants for cwd-independent fallible calls, preserves the ambient error and panic checks for dependent `Path`, `str`, and `String` calls, and proves that a valid explicit cwd still succeeds. - Known-UTF-8 `str` and `String` receivers prove exact clean-trailing and dirty normalization contracts, receiver-only borrowing, explicit and ambient relative context forwarding, and cached-cwd behavior on Unix and Windows. Unix deleted-cwd rows also prove cwd-independent success and strict cwd-dependent panics for string receivers. +- A Linux child process starts in a real cwd containing an invalid native byte and requires `absolutize`, `try_absolutize`, `relative`, and `try_relative` to preserve that byte exactly under both cwd policies. Linux supplies this executable filesystem case because macOS rejects the invalid-byte directory name; explicit-cwd tests continue to cover native-invalid Unix composition independently of the host filesystem. ## Follow-up coverage status @@ -71,5 +72,6 @@ Any change to a public contract, platform branch, native-encoding comparison, cl - [Relative ownership and lifetime contracts](../../tests/relative_borrowing.rs) - [Unavailable-cwd relative behavior](../../tests/relative_without_cwd.rs) - [Known-UTF-8 string receiver ownership](../../tests/string_receiver_contracts.rs) +- [Native-invalid ambient cwd preservation](../../tests/non_utf8_ambient_cwd.rs) - [Native-invalid exact comparison](../../tests/invalid_encoding.rs) - [Production dispatch and short-path oracle](../../src/impl_sugar_path.rs) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index abcc4d9..0102fb8 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -69,6 +69,7 @@ jobs: current_directory_is_cached_only_when_requested pure_relative_calls_initialize_and_observe_cwd_policy failed_cwd_lookup_does_not_poison_later_relative_calls + non_utf8_ambient_cwd_is_preserved requested_cached_current_dir_configuration_is_active - os: windows-latest expected_host: x86_64-pc-windows-msvc diff --git a/tests/non_utf8_ambient_cwd.rs b/tests/non_utf8_ambient_cwd.rs new file mode 100644 index 0000000..9abe718 --- /dev/null +++ b/tests/non_utf8_ambient_cwd.rs @@ -0,0 +1,90 @@ +#![cfg(target_os = "linux")] + +use std::{ + env, + ffi::OsString, + fs, + os::unix::ffi::{OsStrExt, OsStringExt}, + path::{Path, PathBuf}, + process::Command, + time::{SystemTime, UNIX_EPOCH}, +}; + +use sugar_path::SugarPath; + +const CHILD_ENV: &str = "SUGAR_PATH_NON_UTF8_AMBIENT_CWD_CHILD"; +const NON_UTF8_COMPONENT: &[u8] = b"non_utf8-\x80"; + +fn assert_exact_path(actual: &Path, expected: &Path, context: &str) { + assert_eq!( + actual.as_os_str().as_bytes(), + expected.as_os_str().as_bytes(), + "{context} must preserve native cwd bytes", + ); +} + +#[test] +fn non_utf8_ambient_cwd_is_preserved() { + if env::var_os(CHILD_ENV).is_some() { + let non_utf8_component = OsString::from_vec(NON_UTF8_COMPONENT.to_vec()); + let cwd = env::current_dir().expect("read the child's current directory"); + assert!( + cwd + .as_os_str() + .as_bytes() + .windows(NON_UTF8_COMPONENT.len()) + .any(|window| window == NON_UTF8_COMPONENT), + "test setup must enter the non-UTF-8 cwd", + ); + let root = cwd + .parent() + .and_then(Path::parent) + .expect("the child cwd has the temporary root as a grandparent"); + + let input = Path::new("pkg/entry.js"); + let expected_absolute = cwd.join(input); + assert_exact_path(input.absolutize().as_ref(), &expected_absolute, "absolutize"); + assert_exact_path( + input.try_absolutize().expect("resolve against the native cwd").as_ref(), + &expected_absolute, + "try_absolutize", + ); + + let target = Path::new("target.js"); + let base = root.join("anchor"); + let expected_relative = PathBuf::from("..").join(&non_utf8_component).join("leaf").join(target); + assert_exact_path(target.relative(&base).as_ref(), &expected_relative, "relative"); + assert_exact_path( + target.try_relative(&base).expect("resolve relative paths against the native cwd").as_ref(), + &expected_relative, + "try_relative", + ); + 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-non-utf8-cwd-{}-{unique}", std::process::id())); + let cwd = root.join(OsString::from_vec(NON_UTF8_COMPONENT.to_vec())).join("leaf"); + fs::create_dir_all(&cwd).expect("create the non-UTF-8 child cwd"); + + let output = Command::new(env::current_exe().expect("find the integration-test executable")) + .args(["--exact", "non_utf8_ambient_cwd_is_preserved", "--nocapture"]) + .env(CHILD_ENV, "1") + .current_dir(&cwd) + .output() + .expect("run the integration test in a child process"); + + if root.exists() { + fs::remove_dir_all(&root).expect("clean up the non-UTF-8 child cwd"); + } + assert!( + output.status.success(), + "child test failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); +}