From 2705eb5ff200e60e801a579d6cfd588d67525b73 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Sep 2025 03:41:07 +0000 Subject: [PATCH] Fix symlink command implementation and add comprehensive tests (#22) --- CLAUDE.md | 2 +- README.md | 1 + src/run/commands.rs | 6 ++- tests/builtin_commands.rs | 104 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5129572..07236c5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,6 +87,6 @@ testscript::run("testdata") ## Built-in Commands -Core: `exec`, `cmp`, `stdout`, `stderr`, `cd`, `wait`, `exists`, `mkdir`, `cp`, `mv`, `rm`, `chmod`, `env`, `cmpenv`, `stdin`, `skip`, `stop`, `kill`, `unquote`, `grep` +Core: `exec`, `cmp`, `stdout`, `stderr`, `cd`, `wait`, `exists`, `mkdir`, `cp`, `mv`, `rm`, `chmod`, `env`, `cmpenv`, `stdin`, `skip`, `stop`, `kill`, `unquote`, `grep`, `symlink` The command dispatch checks custom commands first, then falls back to built-ins, enabling easy extension. diff --git a/README.md b/README.md index 0727fae..f58443a 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ Test scripts use the [`txtar`](https://pkg.go.dev/github.com/rogpeppe/go-interna - **stop** - Stop test early (pass) - **unquote** - Remove leading `>` from file lines - **grep** - Search files with regex +- **symlink** - Create symbolic links Commands can be prefixed with conditions (`[unix]`) or negated (`!`). diff --git a/src/run/commands.rs b/src/run/commands.rs index 2ce1f97..0e4c8ed 100644 --- a/src/run/commands.rs +++ b/src/run/commands.rs @@ -271,12 +271,12 @@ impl TestEnvironment { /// Create a symbolic link pub fn create_symlink(&self, target: &str, link_name: &str) -> Result<()> { - let target_path = self.work_dir.join(target); let link_path = self.work_dir.join(link_name); #[cfg(unix)] { - std::os::unix::fs::symlink(&target_path, &link_path).map_err(|e| { + // For Unix systems, preserve the target path as-is to support relative links + std::os::unix::fs::symlink(target, &link_path).map_err(|e| { Error::command_error( "symlink", format!( @@ -292,6 +292,8 @@ impl TestEnvironment { // On Windows, try to create a symlink but fall back gracefully use std::os::windows::fs::symlink_file; + // For Windows, we need to resolve the path to check if it's a file + let target_path = self.work_dir.join(target); if target_path.is_file() { symlink_file(&target_path, &link_path).map_err(|e| { Error::command_error( diff --git a/tests/builtin_commands.rs b/tests/builtin_commands.rs index 5559aaa..e889464 100644 --- a/tests/builtin_commands.rs +++ b/tests/builtin_commands.rs @@ -181,3 +181,107 @@ hello from stdout"#; let result = run_test(&script_path); assert!(result.is_ok(), "CP stdout test failed: {:?}", result); } + +#[cfg(unix)] +#[test] +fn test_symlink_command() { + let temp_dir = TempDir::new().unwrap(); + let script_path = temp_dir.path().join("symlink_test.txt"); + + let script_content = r#"# Test symlink command +symlink original.txt link.txt +exists link.txt +exec cat link.txt +stdout "original content" + +-- original.txt -- +original content"#; + + fs::write(&script_path, script_content).unwrap(); + + let result = run_test(&script_path); + assert!(result.is_ok(), "Symlink test failed: {:?}", result); +} + +#[cfg(unix)] +#[test] +fn test_symlink_directory() { + let temp_dir = TempDir::new().unwrap(); + let script_path = temp_dir.path().join("symlink_dir_test.txt"); + + let script_content = r#"# Test symlink with directory +mkdir test_dir +symlink test_dir link_dir +exists link_dir +exec ls link_dir"#; + + fs::write(&script_path, script_content).unwrap(); + + let result = run_test(&script_path); + assert!( + result.is_ok(), + "Directory symlink test failed: {:?}", + result + ); +} + +#[cfg(unix)] +#[test] +fn test_symlink_relative_path() { + let temp_dir = TempDir::new().unwrap(); + let script_path = temp_dir.path().join("symlink_relative_test.txt"); + + let script_content = r#"# Test symlink with relative path +mkdir subdir +symlink ../original.txt subdir/link.txt +exists subdir/link.txt +exec cat subdir/link.txt +stdout "relative content" + +-- original.txt -- +relative content"#; + + fs::write(&script_path, script_content).unwrap(); + + let result = run_test(&script_path); + assert!( + result.is_ok(), + "Relative path symlink test failed: {:?}", + result + ); +} + +#[test] +fn test_symlink_error_cases() { + let temp_dir = TempDir::new().unwrap(); + let script_path = temp_dir.path().join("symlink_error_test.txt"); + + let script_content = r#"# Test symlink error cases +! symlink +! symlink only_one_arg"#; + + fs::write(&script_path, script_content).unwrap(); + + let result = run_test(&script_path); + assert!(result.is_ok(), "Symlink error test failed: {:?}", result); +} + +#[test] +fn test_issue_example() { + let temp_dir = TempDir::new().unwrap(); + let script_path = temp_dir.path().join("issue_example.txt"); + + let script_content = r#"# Create a symlink +symlink original.txt link.txt +exists link.txt +exec cat link.txt +stdout "original content" + +-- original.txt -- +original content"#; + + fs::write(&script_path, script_content).unwrap(); + + let result = run_test(&script_path); + assert!(result.is_ok(), "Issue example test failed: {:?}", result); +}