diff options
| author | JJ Liebig <jonathan.liebig@gmail.com> | 2026-09-17 22:43:35 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-17 22:43:35 +0200 |
| commit | 14351f121f6fc8c84b7c6c6eda78ad210e93cfcb (patch) | |
| tree | ab2773993ac4f012e8dc52f757a7109b3af8b4d5 /src | |
| parent | 101ccc20d3c6483a38ed9e0a1239b2ab908b1704 (diff) | |
test: skip windows symlink tests without symlink privilege (#4311)
Windows refuses file symlinks with ERROR_PRIVILEGE_NOT_HELD (1314) unless
the test process is elevated or Developer Mode is enabled, which broke
non-admin local runs. Share a helper that reports the missing privilege so
the two symlink tests skip only the symlink-dependent assertions.
Diffstat (limited to 'src')
| -rw-r--r-- | src/integration/config_file/tests.rs | 18 | ||||
| -rw-r--r-- | src/integration/mod.rs | 2 | ||||
| -rw-r--r-- | src/integration/test_support.rs | 49 | ||||
| -rw-r--r-- | src/integration/tests.rs | 13 |
4 files changed, 65 insertions, 17 deletions
diff --git a/src/integration/config_file/tests.rs b/src/integration/config_file/tests.rs index 916edc67..687767ee 100644 --- a/src/integration/config_file/tests.rs +++ b/src/integration/config_file/tests.rs @@ -1,4 +1,5 @@ use super::*; +use crate::integration::test_support::symlink_file; struct Directory(PathBuf); @@ -103,15 +104,6 @@ fn hard_links_are_rejected_before_staging_and_rechecked_before_commit() { assert_eq!(fs::read_dir(&dir.0).unwrap().count(), 2); } -#[cfg(unix)] -fn symlink(target: &Path, link: &Path) { - std::os::unix::fs::symlink(target, link).unwrap(); -} -#[cfg(windows)] -fn symlink(target: &Path, link: &Path) { - std::os::windows::fs::symlink_file(target, link).unwrap(); -} - #[test] fn symlink_chains_and_dangling_targets_preserve_links() { let dir = Directory::new(); @@ -120,10 +112,12 @@ fn symlink_chains_and_dangling_targets_preserve_links() { let target = other.join("preferences"); let intermediate = other.join("link"); let entry = dir.0.join("config"); - symlink(&target, &intermediate); + if !symlink_file(&target, &intermediate) { + return; + } // A Windows reparse target needs native separators, unlike ordinary Win32 paths. let relative_target = Path::new("other").join("link"); - symlink(&relative_target, &entry); + assert!(symlink_file(&relative_target, &entry)); let original_intermediate = fs::read_link(&intermediate).unwrap(); assert_eq!( fs::metadata(&entry).unwrap_err().kind(), @@ -142,7 +136,7 @@ fn symlink_chains_and_dangling_targets_preserve_links() { assert_eq!(fs::read_link(&entry).unwrap(), relative_target); let cycle = dir.0.join("cycle"); - symlink(Path::new("cycle"), &cycle); + assert!(symlink_file(Path::new("cycle"), &cycle)); assert!(write_config(&cycle, b"must not replace the link").is_err()); assert_eq!(fs::read_link(&cycle).unwrap(), Path::new("cycle")); } diff --git a/src/integration/mod.rs b/src/integration/mod.rs index daa714e0..349af16f 100644 --- a/src/integration/mod.rs +++ b/src/integration/mod.rs @@ -324,4 +324,6 @@ const GROK_INTEGRATION_VERSION: u32 = 2; pub(crate) const INSTALL_WARNING_PREFIX: &str = "warning:"; #[cfg(test)] +mod test_support; +#[cfg(test)] mod tests; diff --git a/src/integration/test_support.rs b/src/integration/test_support.rs new file mode 100644 index 00000000..638beb00 --- /dev/null +++ b/src/integration/test_support.rs @@ -0,0 +1,49 @@ +//! Shared helpers for integration tests. + +use std::path::Path; + +/// Windows refuses file symlinks with `ERROR_PRIVILEGE_NOT_HELD` when the +/// process is neither elevated nor running with Developer Mode enabled. +#[cfg(windows)] +const ERROR_PRIVILEGE_NOT_HELD: i32 = 1314; + +#[cfg(windows)] +fn symlink_privilege_denied(error: &std::io::Error) -> bool { + error.raw_os_error() == Some(ERROR_PRIVILEGE_NOT_HELD) +} + +/// Create a file symlink, returning `false` when Windows denied the symlink +/// privilege. Callers skip symlink-only assertions on `false` instead of +/// failing an ordinary non-elevated local test run. +#[cfg(unix)] +pub(super) fn symlink_file(target: &Path, link: &Path) -> bool { + std::os::unix::fs::symlink(target, link).expect("create symlink"); + true +} + +#[cfg(windows)] +pub(super) fn symlink_file(target: &Path, link: &Path) -> bool { + match std::os::windows::fs::symlink_file(target, link) { + Ok(()) => true, + Err(error) if symlink_privilege_denied(&error) => { + eprintln!("skipping symlink test: Windows denied SeCreateSymbolicLinkPrivilege"); + false + } + Err(error) => panic!("create symlink {}: {error}", link.display()), + } +} + +#[cfg(all(test, windows))] +mod tests { + use super::*; + + #[test] + fn symlink_privilege_error_is_detected() { + assert!(symlink_privilege_denied( + &std::io::Error::from_raw_os_error(ERROR_PRIVILEGE_NOT_HELD) + )); + assert!(!symlink_privilege_denied( + &std::io::Error::from_raw_os_error(/* ERROR_ACCESS_DENIED */ 5) + )); + } +} diff --git a/src/integration/tests.rs b/src/integration/tests.rs index f7e2154e..bffe2f45 100644 --- a/src/integration/tests.rs +++ b/src/integration/tests.rs @@ -4,6 +4,8 @@ use super::env::*; use super::file_ops::*; use super::registry::*; use super::targets::*; +#[cfg(windows)] +use super::test_support::symlink_file; use super::types::*; use super::version::*; use super::*; @@ -2565,11 +2567,12 @@ fn opencode_recovery_copy_blocks_retry_before_parsing_or_asset_changes() { fs::write(&referent, "{").unwrap(); let linked_backup = base.join("preferences.json.herdr-backup"); fs::rename(&backup, &linked_backup).unwrap(); - std::os::windows::fs::symlink_file(&referent, &config).unwrap(); - let link_before = fs::read_link(&config).unwrap(); - let error = install_target(target).unwrap_err(); - assert!(error.to_string().contains("preferences.json.herdr-backup")); - assert_eq!(fs::read_link(&config).unwrap(), link_before); + if symlink_file(&referent, &config) { + let link_before = fs::read_link(&config).unwrap(); + let error = install_target(target).unwrap_err(); + assert!(error.to_string().contains("preferences.json.herdr-backup")); + assert_eq!(fs::read_link(&config).unwrap(), link_before); + } assert_eq!(fs::read_to_string(&plugin).unwrap(), "previous integration"); assert!(!dir.join("tui.jsonc").exists()); assert!(!dir.join(OPENCODE_V2_TUI_PLUGIN_DIR).exists()); |
