aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJJ Liebig <jonathan.liebig@gmail.com>2026-09-17 22:43:35 +0200
committerGitHub <noreply@github.com>2026-09-17 22:43:35 +0200
commit14351f121f6fc8c84b7c6c6eda78ad210e93cfcb (patch)
treeab2773993ac4f012e8dc52f757a7109b3af8b4d5 /src
parent101ccc20d3c6483a38ed9e0a1239b2ab908b1704 (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.rs18
-rw-r--r--src/integration/mod.rs2
-rw-r--r--src/integration/test_support.rs49
-rw-r--r--src/integration/tests.rs13
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());