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..8d8f519 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/test_symlink.rs b/test_symlink.rs deleted file mode 100644 index c828ff7..0000000 --- a/test_symlink.rs +++ /dev/null @@ -1,9 +0,0 @@ -use testscript_rs::testscript; - -#[test] -fn test_symlink() { - testscript::run("/tmp") - .filter_file("test_symlink.txtar") - .execute() - .unwrap(); -} diff --git a/testdata/test_symlink.txt b/testdata/test_symlink.txt deleted file mode 100644 index cc8a412..0000000 --- a/testdata/test_symlink.txt +++ /dev/null @@ -1,8 +0,0 @@ -# Test symlink functionality -symlink original.txt link.txt -exists link.txt -exec cat link.txt -stdout "original content" - --- original.txt -- -original content \ No newline at end of file diff --git a/tests/builtin_commands.rs b/tests/builtin_commands.rs index 5559aaa..9e18449 100644 --- a/tests/builtin_commands.rs +++ b/tests/builtin_commands.rs @@ -181,3 +181,99 @@ 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); +}