commit 6bf55414c191dbdd1655c817701a0b9d02512325
parent ef67865a71a4d6d529eb0f63bbdf0dface3c7f6f
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date: Thu, 4 Jul 2024 14:44:05 -0700
Merge branch 'ssh'
Implemented dotfiles installation on remote machines over SSH. Also refactored
e2e tests, changed Windows shell from PowerShell to cmd, and restructured local
script execution.
Diffstat:
36 files changed, 1676 insertions(+), 869 deletions(-)
diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
@@ -17,6 +17,11 @@ jobs:
- name: Build
run: cargo build --verbose
+ - name: Start SSH server
+ run: |
+ echo 'PUID=1001' > tests/server/.env
+ docker compose -f tests/server/compose.yml up -d
+
- name: Run tests
run: cargo test --verbose
diff --git a/.gitignore b/.gitignore
@@ -1 +1,7 @@
/target
+
+# Temporary directories used for testing
+/tests/.temp/home/*
+/tests/.temp/local/*
+/tests/.temp/ssh/*
+!/tests/.temp/ssh/.gitkeep
diff --git a/Cargo.lock b/Cargo.lock
@@ -111,6 +111,7 @@ dependencies = [
"serde",
"serde_yaml",
"shellexpand",
+ "tempfile",
]
[[package]]
@@ -147,6 +148,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
+name = "errno"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
+
+[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -208,6 +225,12 @@ dependencies = [
]
[[package]]
+name = "linux-raw-sys"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
+
+[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -243,6 +266,19 @@ dependencies = [
]
[[package]]
+name = "rustix"
+version = "0.38.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -308,6 +344,18 @@ dependencies = [
]
[[package]]
+name = "tempfile"
+version = "3.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "rustix",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
name = "thiserror"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
@@ -8,3 +8,4 @@ clap = { version = "4.5.7", features = ["derive"] }
shellexpand = "3.0"
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.9"
+tempfile = "3"
diff --git a/README.md b/README.md
@@ -2,20 +2,26 @@
A minimal, flexible, dotfile installer
## Installation
-To install `coliru`, clone the repository and run `cargo install --path coliru/`
+To install coliru, clone the repository and run `cargo install --path coliru/`
-To uninstall `coliru`, run `cargo uninstall coliru`
+To uninstall coliru, run `cargo uninstall coliru`
## Usage
Dotfiles are defined as a series of steps inside a manifest file that are
executed conditionally based on tag rules. To install dotfiles, pass the
-manifest file and tag rules to `coliru`:
+manifest file and tag rules to coliru:
```
-coliru path/to/manifest.yml --tag-rules tag1 tag2,tag3 ^tag4
+coliru path/to/manifest.yml --tag-rules tag1 tag2 ^tag3
```
-Some helpful options include:
+Coliru can also install dotfiles on remote machines over SSH:
+
+```
+coliru path/to/manifest.yml --tag-rules tag1 tag2 ^tag3 --host user@hostname
+```
+
+Some other helpful options include:
- `--help`, `-h`: Print full help information
- `--dry-run`, `-n`: Do a trial run without any permanent changes
@@ -29,13 +35,13 @@ directory containing the manifest file. The copy command copies a file from a
source (`src`) to a (`dst`). The link command links a file from a source (`src`)
to a (`dst`) using symbolic links on Unix platforms and hard links on Windows.
Finally, the run command executes a script (`src`) from the command line, using
-`sh` on Unix platforms and `powershell` on Windows, with an optional `prefix`
-(e.g. `python3`) or `postfix` (e.g. `arg1 arg2 arg3`) string. Inside `postfix`,
+`sh` on Unix platforms and `cmd` on Windows, with an optional `prefix` (e.g.
+`python3`) or `postfix` (e.g. `arg1 arg2 arg3`) string. Inside `postfix`,
`$COLIRU_RULES` will be expanded into a space-delimited list of the current tag
rules.
-Example YAML manifest (see [`examples/manifest.yml`](examples/manifest.yml) for
-more details):
+Example YAML manifest (see
+[`examples/basic/manifest.yml`](examples/basic/manifest.yml) for more details):
```yml
steps:
diff --git a/examples/bashrc b/examples/basic/bashrc
diff --git a/examples/gitconfig b/examples/basic/gitconfig
diff --git a/examples/manifest.yml b/examples/basic/manifest.yml
diff --git a/examples/basic/script.bat b/examples/basic/script.bat
@@ -0,0 +1,2 @@
+@ECHO OFF
+ECHO script.bat called with %*
diff --git a/examples/basic/script.sh b/examples/basic/script.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env sh
+
+echo "script.sh called with $@"
diff --git a/examples/vimrc b/examples/basic/vimrc
diff --git a/examples/manifest-windows-test.yml b/examples/manifest-windows-test.yml
@@ -1,27 +0,0 @@
-# Identical to manifest.yml, but uses relative paths instead of paths with
-# tildes becuase $HOME is difficult to mock on windows.
-
-steps:
- - copy:
- - src: gitconfig
- dst: .gitconfig.coliru
- tags: [ windows, linux, macos ]
-
- - link:
- - src: bashrc
- dst: .bashrc.coliru
- - src: vimrc
- dst: .vimrc.coliru # Will create symbolic links on Linux & MacOS
- run:
- - src: script.sh
- prefix: sh # unecessary on Unix if script.sh is executable
- postfix: arg1 $COLIRU_RULES
- tags: [ linux, macos ]
-
- - link:
- - src: vimrc
- dst: _vimrc.coliru # Will create hard link on Windows
- run:
- - src: script.bat
- postfix: arg1 $COLIRU_RULES
- tags: [ windows ]
diff --git a/examples/script.bat b/examples/script.bat
@@ -1,3 +0,0 @@
-@ECHO OFF
-ECHO script.bat called with %*
-ECHO script.bat called with %* > log.txt
diff --git a/examples/script.sh b/examples/script.sh
@@ -1,4 +0,0 @@
-#!/usr/bin/env sh
-
-echo "script.sh called with $@"
-echo "script.sh called with $@" > log.txt
diff --git a/examples/test/bashrc b/examples/test/bashrc
@@ -0,0 +1 @@
+bash #1
diff --git a/examples/test/gitconfig b/examples/test/gitconfig
@@ -0,0 +1 @@
+git #1
diff --git a/examples/manifest-invalid.yml b/examples/test/invalid.yml
diff --git a/examples/test/manifest.yml b/examples/test/manifest.yml
@@ -0,0 +1,27 @@
+# Note: copy_manifest() updates replaces ~/ and scripts/ with unique paths for
+# ssh e2e tests and Windows tests to ensure test dotfiles are isolated
+
+steps:
+ - copy:
+ - src: gitconfig
+ dst: ~/.gitconfig
+ tags: [ windows, linux, macos ]
+
+ - link:
+ - src: bashrc
+ dst: ~/.bashrc
+ - src: vimrc
+ dst: ~/.vimrc
+ run:
+ - src: scripts/script.sh
+ prefix: sh
+ postfix: arg1 $COLIRU_RULES
+ tags: [ linux, macos ]
+
+ - link:
+ - src: vimrc
+ dst: ~/_vimrc
+ run:
+ - src: scripts/script.bat
+ postfix: arg1 $COLIRU_RULES
+ tags: [ windows ]
diff --git a/examples/test/scripts/script.bat b/examples/test/scripts/script.bat
@@ -0,0 +1,3 @@
+@ECHO OFF
+ECHO script.bat called with %*
+ECHO script.bat called with %* > scripts/log.txt
diff --git a/examples/test/scripts/script.sh b/examples/test/scripts/script.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env sh
+
+echo "script.sh called with $@"
+echo "script.sh called with $@" > scripts/log.txt
diff --git a/examples/test/vimrc b/examples/test/vimrc
@@ -0,0 +1 @@
+vim #1
diff --git a/src/cli.rs b/src/cli.rs
@@ -14,6 +14,10 @@ struct Args {
#[arg(short, long, num_args=0..)]
pub tag_rules: Vec<String>,
+ /// Install dotfiles on another machine over SSH
+ #[arg(long, default_value="", hide_default_value=true)]
+ pub host: String,
+
/// Interpret link commands as copy commands
#[arg(short, long)]
pub copy: bool,
@@ -27,6 +31,6 @@ struct Args {
pub fn run() {
let args = Args::parse();
let manifest_path = Path::new(&args.manifest);
- execute_manifest_file(&manifest_path, args.tag_rules, args.dry_run,
- args.copy);
+ execute_manifest_file(&manifest_path, args.tag_rules, &args.host,
+ args.dry_run, args.copy);
}
diff --git a/src/core.rs b/src/core.rs
@@ -1,22 +1,29 @@
use std::env::set_current_dir;
use std::path::Path;
-use super::manifest::{CopyLinkOptions, RunOptions, Manifest, parse_manifest_file
-};
+use super::manifest::{CopyLinkOptions, RunOptions, parse_manifest_file};
use super::tags::tags_match;
-use super::utils::{copy_file, link_file, run_script};
+use super::local::{copy_file, link_file, run_command};
+use super::ssh::{send_command, send_staged_files, stage_file};
+use tempfile::tempdir;
/// Execute the steps in a coliru manifest file according to a set of tag rules
-pub fn execute_manifest_file(path: &Path, tag_rules: Vec<String>, dry_run: bool,
- copy: bool) {
- match parse_manifest_file(path) {
- Ok(manifest) => execute_manifest(manifest, tag_rules, dry_run, copy),
- Err(why) => eprintln!("Error: {}", why),
- };
-}
+pub fn execute_manifest_file(path: &Path, tag_rules: Vec<String>, host: &str,
+ dry_run: bool, copy: bool) {
+
+ let _manifest = parse_manifest_file(path);
+ if let Err(why) = _manifest {
+ eprintln!("Error: {}", why);
+ return;
+ }
+ let manifest = _manifest.unwrap();
+
+ let _temp_dir = tempdir();
+ if let Err(why) = _temp_dir {
+ eprintln!("Error: {}", why);
+ return;
+ }
+ let temp_dir = _temp_dir.unwrap();
-/// Execute the steps in a coliru manifest according to a set of tag rules
-fn execute_manifest(manifest: Manifest, tag_rules: Vec<String>, dry_run: bool,
- copy: bool) {
if let Err(why) = set_current_dir(manifest.base_dir) {
eprintln!("Error: {}", why);
return;
@@ -27,20 +34,30 @@ fn execute_manifest(manifest: Manifest, tag_rules: Vec<String>, dry_run: bool,
let step_str = format!("[{}/{}]", i+1, manifest.steps.len());
- execute_copies(&step.copy, dry_run, &step_str);
- if copy {
- execute_copies(&step.link, dry_run, &step_str);
- } else {
+ execute_copies(&step.copy, host, temp_dir.path(), dry_run, &step_str);
+
+ if !copy && host == "" {
execute_links(&step.link, dry_run, &step_str);
+ } else {
+ execute_copies(&step.link, host, temp_dir.path(), dry_run,
+ &step_str);
}
- execute_runs(&step.run, &tag_rules, dry_run, &step_str);
+
+ execute_runs(&step.run, &tag_rules, host, temp_dir.path(), dry_run,
+ &step_str);
}
}
-/// Execute the copy commands specified in a coliru manifest step
-fn execute_copies(copies: &[CopyLinkOptions], dry_run: bool, step_str: &str) {
+/// Execute a set of copy commands
+fn execute_copies(copies: &[CopyLinkOptions], host: &str, staging_dir: &Path,
+ dry_run: bool, step_str: &str) {
+
for copy in copies {
- print!("{} Copy {} to {}", step_str, copy.src, copy.dst);
+ if host == "" {
+ print!("{} Copy {} to {}", step_str, copy.src, copy.dst);
+ } else {
+ print!("{} Copy {} to {}:{}", step_str, copy.src, host, copy.dst);
+ }
if dry_run {
println!(" (DRY RUN)");
@@ -48,13 +65,25 @@ fn execute_copies(copies: &[CopyLinkOptions], dry_run: bool, step_str: &str) {
}
println!("");
- if let Err(why) = copy_file(©.src, ©.dst) {
+ if host == "" {
+ if let Err(why) = copy_file(©.src, ©.dst) {
+ eprintln!(" Error: {}", why);
+ }
+ } else {
+ if let Err(why) = stage_file(©.src, ©.dst, staging_dir) {
+ eprintln!(" Error: {}", why);
+ }
+ }
+ }
+
+ if !dry_run {
+ if let Err(why) = send_staged_files(staging_dir, host) {
eprintln!(" Error: {}", why);
}
}
}
-/// Execute the link commands specified in a coliru manifest step
+/// Execute a set of link commands
fn execute_links(links: &[CopyLinkOptions], dry_run: bool, step_str: &str) {
for link in links {
print!("{} Link {} to {}", step_str, link.src, link.dst);
@@ -71,13 +100,30 @@ fn execute_links(links: &[CopyLinkOptions], dry_run: bool, step_str: &str) {
}
}
-/// Execute the run commands specified in a coliru manifest step
-fn execute_runs(runs: &[RunOptions], tag_rules: &[String], dry_run: bool,
- step_str: &str) {
+/// Execute a set of run commands
+fn execute_runs(runs: &[RunOptions], tag_rules: &[String], host: &str,
+ staging_dir: &Path, dry_run: bool, step_str: &str) {
+
+ if !dry_run && host != "" {
+ for run in runs {
+ if let Err(why) = stage_file(&run.src, &run.src, staging_dir) {
+ eprintln!("Error: {}", why);
+ }
+ }
+
+ if let Err(why) = send_staged_files(staging_dir, host) {
+ eprintln!("Error: {}", why);
+ }
+ }
for run in runs {
let postfix = run.postfix.replace("$COLIRU_RULES", &tag_rules.join(" "));
- print!("{} Run {} {} {}", step_str, run.prefix, run.src, postfix);
+ let cmd = format!("{} {} {}", run.prefix, run.src, postfix);
+ if host == "" {
+ print!("{} Run {}", step_str, cmd);
+ } else {
+ print!("{} Run {} on {}", step_str, cmd, host);
+ }
if dry_run {
println!(" (DRY RUN)");
@@ -85,8 +131,15 @@ fn execute_runs(runs: &[RunOptions], tag_rules: &[String], dry_run: bool,
}
println!("");
- if let Err(why) = run_script(&run.src, &run.prefix, &postfix) {
- eprintln!(" Error: {}", why);
+ if host == "" {
+ if let Err(why) = run_command(&cmd) {
+ eprintln!(" Error: {}", why);
+ }
+ } else {
+ let ssh_cmd = format!("cd .coliru && {}", &cmd);
+ if let Err(why) = send_command(&ssh_cmd, host) {
+ eprintln!(" Error: {}", why);
+ }
}
}
}
diff --git a/src/local.rs b/src/local.rs
@@ -0,0 +1,358 @@
+use shellexpand::tilde;
+use std::io;
+use std::fs;
+#[cfg(target_family = "unix")]
+use std::os::unix::fs::symlink;
+use std::path::{PathBuf, absolute};
+use std::process::Command;
+
+/// Copies the contents of a local file to another local file.
+///
+/// Tildes are expanded if present and the destination file is overwritten if
+/// necessary.
+pub fn copy_file(src: &str, dst: &str) -> io::Result<()> {
+ if absolute(src)? == absolute(dst)? { return Ok(()); }
+ let _dst = prepare_path(dst)?;
+ fs::copy(src, _dst)?;
+ Ok(())
+}
+
+/// Creates a symbolic link to a local file.
+///
+/// Tildes are expanded if present and the destination file is overwritten if
+/// necessary. On non-Unix platforms, a hard link will be created instead.
+#[cfg(target_family = "unix")]
+pub fn link_file(src: &str, dst: &str) -> io::Result<()> {
+ if absolute(src)? == absolute(dst)? { return Ok(()); }
+ let _dst = prepare_path(dst)?;
+ symlink(fs::canonicalize(src)?, _dst)?;
+ Ok(())
+}
+#[cfg(not(target_family = "unix"))]
+pub fn link_file(src: &str, dst: &str) -> io::Result<()> {
+ if absolute(src)? == absolute(dst)? { return Ok(()); }
+ let _dst = prepare_path(dst)?;
+ fs::hard_link(src, _dst)?;
+ Ok(())
+}
+
+/// Creates the parent directories of a path and return the path with tildes
+/// expanded.
+fn prepare_path(path: &str) -> io::Result<PathBuf> {
+ let _dst: PathBuf = (&tilde(path).to_mut()).into();
+ if let Some(_path) = _dst.parent() {
+ fs::create_dir_all(_path)?;
+ }
+ if fs::symlink_metadata(&_dst).is_ok() {
+ // Check for existing files, including broken symlinks
+ fs::remove_file(&_dst)?;
+ }
+ Ok(_dst)
+}
+
+/// Executes a local command using sh on Unix and cmd on Windows
+pub fn run_command(command: &str) -> Result<(), String>
+{
+ let status;
+ if cfg!(target_family = "unix") {
+ status = Command::new("sh")
+ .args(["-c", command])
+ .status()
+ .map_err(|why| why.to_string())?;
+ } else {
+ status = Command::new("cmd.exe")
+ .args(["/C", command])
+ .status()
+ .map_err(|why| why.to_string())?;
+ }
+ if status.success() {
+ Ok(())
+ } else {
+ Err(format!("Process exited with {status}"))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::common::{setup_integration, write_file};
+
+ #[test]
+ fn test_copy_file_create_dirs() {
+ let tmp = setup_integration("test_copy_file_create_dirs");
+
+ let src = &tmp.local.join("foo");
+ let dst = &tmp.local.join("dir1").join("dir2").join("bar");
+ write_file(src, "old contents of foo");
+
+ let result = copy_file(src.to_str().unwrap(), dst.to_str().unwrap());
+
+ write_file(src, "new contents of foo");
+ let contents = fs::read_to_string(dst).unwrap();
+ assert_eq!(result.is_ok(), true);
+ assert_eq!(contents, "old contents of foo");
+ }
+
+ #[test]
+ fn test_copy_file_same_file() {
+ let tmp = setup_integration("test_copy_file_same_file");
+
+ let src = &tmp.local.join("foo");
+ let dst = &tmp.local.join("foo");
+ write_file(src, "contents of foo");
+
+ let result = copy_file(src.to_str().unwrap(), dst.to_str().unwrap());
+
+ let contents = fs::read_to_string(dst).unwrap();
+ assert_eq!(result.is_ok(), true);
+ assert_eq!(contents, "contents of foo");
+ }
+
+ #[test]
+ fn test_copy_file_existing_file() {
+ let tmp = setup_integration("test_copy_file_existing_file");
+
+ let src = &tmp.local.join("foo");
+ let dst = &tmp.local.join("bar");
+ write_file(src, "old contents of foo");
+ write_file(dst, "old contents of bar");
+
+ let result = copy_file(src.to_str().unwrap(), dst.to_str().unwrap());
+
+ write_file(src, "new contents of foo");
+ let contents = fs::read_to_string(dst).unwrap();
+ assert_eq!(result.is_ok(), true);
+ assert_eq!(contents, "old contents of foo");
+ }
+
+ #[test]
+ #[cfg(target_family = "unix")]
+ fn test_copy_file_existing_broken_symlink() {
+ let tmp = setup_integration("test_copy_file_existing_broken_symlink");
+
+ let src = &tmp.local.join("foo");
+ let dst = &tmp.local.join("bar");
+ write_file(src, "old contents of foo");
+ symlink("missing", dst).unwrap();
+
+ let result = copy_file(src.to_str().unwrap(), dst.to_str().unwrap());
+
+ write_file(src, "new contents of foo");
+ let contents = fs::read_to_string(dst).unwrap();
+ assert_eq!(result.is_ok(), true);
+ assert_eq!(contents, "old contents of foo");
+ }
+
+ #[test]
+ #[cfg(target_family = "unix")]
+ fn test_copy_file_tilde_expansion() {
+ let tmp = setup_integration("test_copy_file_tilde_expansion");
+
+ let src = &tmp.local.join("foo");
+ let dst = &tmp.home.join("dir").join("bar");
+ let dst_tilde = "~/test_copy_file_tilde_expansion/dir/bar";
+ write_file(src, "old contents of foo");
+
+ let result = copy_file(src.to_str().unwrap(), dst_tilde);
+
+ write_file(src, "new contents of foo");
+ let contents = fs::read_to_string(dst).unwrap();
+ assert_eq!(result.is_ok(), true);
+ assert_eq!(contents, "old contents of foo");
+ }
+
+ #[test]
+ fn test_link_file_create_dirs() {
+ let tmp = setup_integration("test_link_file_create_dirs");
+
+ let src = &tmp.local.join("foo");
+ let dst = &tmp.local.join("dir1").join("dir2").join("bar");
+ write_file(src, "old contents of foo");
+
+ let result = link_file(src.to_str().unwrap(), dst.to_str().unwrap());
+
+ write_file(src, "new contents of foo");
+ let contents = fs::read_to_string(dst).unwrap();
+ assert_eq!(result.is_ok(), true);
+ assert_eq!(contents, "new contents of foo");
+ }
+
+ #[test]
+ fn test_link_file_same_file() {
+ let tmp = setup_integration("test_link_file_same_file");
+
+ let src = &tmp.local.join("foo");
+ let dst = &tmp.local.join("foo");
+ write_file(src, "contents of foo");
+
+ let result = link_file(src.to_str().unwrap(), dst.to_str().unwrap());
+
+ let contents = fs::read_to_string(dst).unwrap();
+ assert_eq!(result.is_ok(), true);
+ assert_eq!(contents, "contents of foo");
+ }
+
+ #[test]
+ fn test_link_file_existing_file() {
+ let tmp = setup_integration("test_link_file_existing_file");
+
+ let src = &tmp.local.join("foo");
+ let dst = &tmp.local.join("bar");
+ write_file(src, "old contents of foo");
+ write_file(dst, "old contents of bar");
+
+ let result = link_file(src.to_str().unwrap(), dst.to_str().unwrap());
+
+ write_file(src, "new contents of foo");
+ let contents = fs::read_to_string(dst).unwrap();
+ assert_eq!(result.is_ok(), true);
+ assert_eq!(contents, "new contents of foo");
+ }
+
+ #[test]
+ #[cfg(target_family = "unix")]
+ fn test_link_file_existing_broken_symlink() {
+ let tmp = setup_integration("test_link_file_existing_broken_symlink");
+
+ let src = &tmp.local.join("foo");
+ let dst = &tmp.local.join("bar");
+ write_file(src, "old contents of foo");
+ symlink("missing", dst).unwrap();
+
+ let result = link_file(src.to_str().unwrap(), dst.to_str().unwrap());
+
+ write_file(src, "new contents of foo");
+ let contents = fs::read_to_string(dst).unwrap();
+ assert_eq!(result.is_ok(), true);
+ assert_eq!(contents, "new contents of foo");
+ }
+
+ #[test]
+ #[cfg(target_family = "unix")]
+ fn test_link_file_tilde_expansion() {
+ let tmp = setup_integration("test_link_file_tilde_expansion");
+
+ let src = &tmp.local.join("foo");
+ let dst = &tmp.home.join("dir").join("bar");
+ let dst_tilde = "~/test_link_file_tilde_expansion/dir/bar";
+ write_file(src, "old contents of foo");
+
+ let result = link_file(src.to_str().unwrap(), dst_tilde);
+
+ write_file(src, "new contents of foo");
+ let contents = fs::read_to_string(dst).unwrap();
+ assert_eq!(result.is_ok(), true);
+ assert_eq!(contents, "new contents of foo");
+ }
+
+ #[test]
+ #[cfg(target_family = "unix")]
+ fn test_link_file_relative_source() {
+ let dir = PathBuf::from("tests/.temp/ssh/test_link_file_relative_source");
+ fs::create_dir_all(&dir).unwrap();
+
+ let src = absolute(&dir.join("foo")).unwrap();
+ let src_rel = "tests/.temp/ssh/test_link_file_relative_source/foo";
+ let dst = &dir.join("dir1").join("dir2").join("bar");
+ write_file(&src, "old contents of foo");
+
+ let result = link_file(src_rel, dst.to_str().unwrap());
+
+ write_file(&src, "new contents of foo");
+ let contents = fs::read_to_string(dst).unwrap();
+ let link = fs::read_link(dst).unwrap();
+ assert_eq!(result.is_ok(), true);
+ assert_eq!(contents, "new contents of foo");
+ assert_eq!(link, src); // src changed to absolute path
+
+ fs::remove_dir_all(&dir).unwrap();
+ }
+
+ #[test]
+ #[cfg(target_family = "unix")]
+ fn test_run_command_successful() {
+ let tmp = setup_integration("test_run_command_successful");
+
+ let src = &tmp.local.join("foo");
+ write_file(src, "exit 0");
+
+ let result = run_command(&format!("sh {}", src.to_str().unwrap()));
+
+ assert_eq!(result.is_ok(), true);
+ }
+
+ #[test]
+ #[cfg(target_family = "windows")]
+ fn test_run_command_successful() {
+ let tmp = setup_integration("test_run_command_successful");
+
+ let src = &tmp.local.join("foo.bat");
+ write_file(src, "exit 0");
+
+ let result = run_command(src.to_str().unwrap());
+
+ assert_eq!(result.is_ok(), true);
+ }
+
+ #[test]
+ #[cfg(target_family = "unix")]
+ fn test_run_command_failure() {
+ let tmp = setup_integration("test_run_command_failure");
+
+ let src = &tmp.local.join("foo");
+ write_file(src, "exit 2");
+
+ let result = run_command(&format!("sh {}", src.to_str().unwrap()));
+
+ assert_eq!(result.is_ok(), false);
+ assert_eq!(result.unwrap_err(), "Process exited with exit status: 2");
+ }
+
+ #[test]
+ #[cfg(target_family = "windows")]
+ fn test_run_command_failure() {
+ let tmp = setup_integration("test_run_command_failure");
+
+ let src = &tmp.local.join("foo.bat");
+ write_file(src, "exit 1");
+
+ let result = run_command(src.to_str().unwrap());
+
+ assert_eq!(result.is_ok(), false);
+ assert_eq!(result.unwrap_err(), "Process exited with exit code: 1");
+ }
+
+ #[test]
+ #[cfg(target_family = "unix")]
+ fn test_run_command_arguments() {
+ let tmp = setup_integration("test_run_command_arguments");
+
+ let src = &tmp.local.join("foo");
+ let dst = &tmp.local.join("bar");
+ write_file(src, &format!("echo $@ > {}", dst.to_str().unwrap()));
+
+ let result = run_command(&format!("sh {} arg1 arg2",
+ src.to_str().unwrap()));
+
+ let contents = fs::read_to_string(dst).unwrap();
+ assert_eq!(result.is_ok(), true);
+ assert_eq!(contents, "arg1 arg2\n");
+ }
+
+ #[test]
+ #[cfg(target_family = "windows")]
+ fn test_run_command_arguments() {
+ let tmp = setup_integration("test_run_command_arguments");
+
+ let src = &tmp.local.join("foo.bat");
+ let dst = &tmp.local.join("bar");
+ write_file(src, &format!("echo %* > {}", dst.to_str().unwrap()));
+
+ let result = run_command(&format!("{} arg1 arg2",
+ src.to_str().unwrap()));
+
+ let contents = fs::read_to_string(dst).unwrap();
+ assert_eq!(result.is_ok(), true);
+ assert_eq!(contents, "arg1 arg2 \r\n");
+ }
+}
diff --git a/src/main.rs b/src/main.rs
@@ -1,8 +1,13 @@
mod cli;
mod core;
+mod local;
mod manifest;
+mod ssh;
mod tags;
-mod utils;
+
+#[cfg(test)]
+#[path = "../tests/common/mod.rs"]
+mod common; // Re-use e2e test utils for integration tests
fn main() {
cli::run();
diff --git a/src/manifest.rs b/src/manifest.rs
@@ -69,36 +69,39 @@ mod tests {
#[test]
#[cfg(target_family = "unix")]
fn parse_manifest_file_missing() {
+ let manifest_path = Path::new("examples/test/missing.yml");
let expected = "No such file or directory (os error 2)";
- let actual = parse_manifest_file(Path::new("examples/missing.yml"));
+ let actual = parse_manifest_file(manifest_path);
assert_eq!(actual, Err(String::from(expected)));
}
#[test]
#[cfg(target_family = "windows")]
fn parse_manifest_file_missing() {
+ let manifest_path = Path::new("examples/test/missing.yml");
let exp = "The system cannot find the file specified. (os error 2)";
- let actual = parse_manifest_file(Path::new("examples/missing.yml"));
+ let actual = parse_manifest_file(manifest_path);
assert_eq!(actual, Err(String::from(exp)));
}
#[test]
fn parse_manifest_file_invalid() {
- let path = Path::new("examples/manifest-invalid.yml");
+ let manifest_path = Path::new("examples/test/invalid.yml");
let exp = "steps[0].copy[0]: missing field `src` at line 5 column 7";
- let actual = parse_manifest_file(path);
+ let actual = parse_manifest_file(manifest_path);
assert_eq!(actual, Err(String::from(exp)));
}
#[test]
fn parse_manifest_file_valid() {
+ let manifest_path = Path::new("examples/test/manifest.yml");
let expected = Manifest {
steps: vec![
Step {
copy: vec![
CopyLinkOptions {
src: String::from("gitconfig"),
- dst: String::from("~/.gitconfig.coliru"),
+ dst: String::from("~/.gitconfig"),
},
],
link: vec![],
@@ -114,16 +117,16 @@ mod tests {
link: vec![
CopyLinkOptions {
src: String::from("bashrc"),
- dst: String::from("~/.bashrc.coliru"),
+ dst: String::from("~/.bashrc"),
},
CopyLinkOptions {
src: String::from("vimrc"),
- dst: String::from("~/.vimrc.coliru"),
+ dst: String::from("~/.vimrc"),
},
],
run: vec![
RunOptions {
- src: String::from("script.sh"),
+ src: String::from("scripts/script.sh"),
prefix: String::from("sh"),
postfix: String::from("arg1 $COLIRU_RULES"),
},
@@ -135,12 +138,12 @@ mod tests {
link: vec![
CopyLinkOptions {
src: String::from("vimrc"),
- dst: String::from("~/_vimrc.coliru"),
+ dst: String::from("~/_vimrc"),
},
],
run: vec![
RunOptions {
- src: String::from("script.bat"),
+ src: String::from("scripts/script.bat"),
prefix: String::from(""),
postfix: String::from("arg1 $COLIRU_RULES"),
},
@@ -148,9 +151,9 @@ mod tests {
tags: vec![String::from("windows")],
},
],
- base_dir: PathBuf::from("examples"),
+ base_dir: PathBuf::from("examples/test"),
};
- let actual = parse_manifest_file(Path::new("examples/manifest.yml"));
+ let actual = parse_manifest_file(manifest_path);
assert_eq!(actual, Ok(expected));
}
}
diff --git a/src/ssh.rs b/src/ssh.rs
@@ -0,0 +1,311 @@
+use shellexpand::tilde_with_context;
+use std::fs::{read_dir, remove_dir_all};
+use std::path::{MAIN_SEPARATOR_STR, Path, PathBuf};
+use std::process::Command;
+use super::local::copy_file;
+
+/// Copy a file to an SCP staging directory
+///
+/// The destination directory structure will be recreated in the staging
+/// directory under either the home or root subdirectories. This staging system
+/// allows missing directories to be created automatically on the remote
+/// machine.
+pub fn stage_file(src: &str, dst: &str, staging_dir: &Path) -> Result<(), String> {
+ let home_dir = staging_dir.join("home");
+ let root_dir = staging_dir.join("root");
+ let get_home_dir = || {
+ Some::<String>(home_dir.to_string_lossy().into())
+ };
+
+ // Resolve ~/... paths to home staging directory:
+ let mut _dst: PathBuf = (&tilde_with_context(dst, get_home_dir).to_mut())
+ .into();
+
+ // Resolve relative paths to home staging directory:
+ _dst = home_dir.join(".coliru").join(_dst);
+
+ // Resolve other absolute paths to root staging directory:
+ if !_dst.starts_with(home_dir) {
+ // Root should be / and C:\ on Unix and Windows respectively, but
+ // iter().next() will return / and C:, so we must manually add another
+ // path separator. (Duplicate slashes are ignored on Unix).
+ let root = PathBuf::from(match _dst.iter().next() {
+ Some(x) => Ok(x),
+ None => Err(String::from("Destination path does not have root")),
+ }?).join(MAIN_SEPARATOR_STR);
+ _dst = root_dir.join(_dst.strip_prefix(root)
+ .map_err(|why| why.to_string())?);
+ }
+
+ copy_file(src, _dst.to_string_lossy().to_mut())
+ .map_err(|why| why.to_string())
+}
+
+/// Transfer the files in an SCP staging directory to a remote machine
+pub fn send_staged_files(staging_dir: &Path, host: &str) -> Result<(), String> {
+ let home_dir = staging_dir.join("home");
+ if home_dir.exists() {
+ send_dir(home_dir.to_string_lossy().to_mut(), "~", host)?;
+ remove_dir_all(home_dir).map_err(|why| why.to_string())?;
+ }
+ let root_dir = staging_dir.join("root");
+ if root_dir.exists() {
+ send_dir(root_dir.to_string_lossy().to_mut(), "/", host)?;
+ remove_dir_all(root_dir).map_err(|why| why.to_string())?;
+ }
+ Ok(())
+}
+
+/// Copy a directory to another machine via SCP and merge it with a destination
+/// directory
+fn send_dir(src: &str, dst: &str, host: &str) -> Result<(), String> {
+ // To avoid the source directory being copied as a subdirectory of the
+ // destination directory, we must send the contents of the directory
+ // item by item.
+ for item in read_dir(&src).map_err(|why| why.to_string())? {
+ let _src = item.map_err(|why| why.to_string())?.path();
+
+ let mut cmd = Command::new("scp");
+ if host == "test@localhost" {
+ // SSH options and port for test server hard coded for now
+ cmd.args(["-o", "StrictHostKeyChecking=no", "-P", "2222"]);
+ }
+ cmd.args(["-r", &_src.to_string_lossy(), &format!("{host}:{dst}")]);
+
+ let status = cmd.status().map_err(|why| why.to_string())?;
+ if !status.success() {
+ return Err(format!("SCP exited with {status}"));
+ }
+ }
+ Ok(())
+}
+
+/// Execute a command on another machine via SSH
+#[allow(dead_code)]
+pub fn send_command(command: &str, host: &str) -> Result<(), String> {
+ let mut cmd = Command::new("ssh");
+ if host == "test@localhost" {
+ // SSH options and port for test server hard coded for now
+ cmd.args(["-o", "StrictHostKeyChecking=no", "-p", "2222"]);
+ }
+ cmd.args([host, command]);
+
+ let status = cmd.status().map_err(|why| why.to_string())?;
+ if !status.success() {
+ return Err(format!("SSH exited with {status}"));
+ }
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ #![allow(unused_imports)]
+
+ use super::*;
+ use crate::common::{SSH_HOST, read_file, setup_integration, write_file};
+
+ use std::fs;
+
+ #[test]
+ fn test_stage_file_tilde() {
+ let tmp = setup_integration("test_stage_file_tilde");
+
+ let src = tmp.local.join("foo");
+ let dst = "~/dir/bar";
+ let dst_real = tmp.local.join("home").join("dir").join("bar");
+ let staging = &tmp.local;
+ write_file(&src, "contents of foo");
+
+ let result = stage_file(src.to_str().unwrap(), dst, staging);
+
+ assert_eq!(result, Ok(()));
+ assert_eq!(dst_real.exists(), true);
+ assert_eq!(read_file(&dst_real), "contents of foo");
+ }
+
+ #[test]
+ fn test_stage_file_relative() {
+ let tmp = setup_integration("test_stage_file_relative");
+
+ let src = tmp.local.join("foo");
+ let dst = "dir/bar";
+ let dst_real = tmp.local.join("home").join(".coliru").join("dir")
+ .join("bar");
+ let staging = &tmp.local;
+ write_file(&src, "contents of foo");
+
+ let result = stage_file(src.to_str().unwrap(), dst, staging);
+
+ assert_eq!(result, Ok(()));
+ assert_eq!(dst_real.exists(), true);
+ assert_eq!(read_file(&dst_real), "contents of foo");
+ }
+
+ #[test]
+ fn test_stage_file_absolute() {
+ let tmp = setup_integration("test_stage_file_absolute");
+
+ let src = tmp.local.join("foo");
+ let dst = "/dir/bar";
+ let dst_real = tmp.local.join("root").join("dir").join("bar");
+ let staging = &tmp.local;
+ write_file(&src, "contents of foo");
+
+ let result = stage_file(src.to_str().unwrap(), dst, staging);
+
+ assert_eq!(result, Ok(()));
+ assert_eq!(dst_real.exists(), true);
+ assert_eq!(read_file(&dst_real), "contents of foo");
+ }
+
+ #[test]
+ #[cfg(target_family = "unix")]
+ fn test_send_staged_files_no_files() {
+ let tmp = setup_integration("test_send_staged_files_no_files");
+
+ let result = send_staged_files(&tmp.local, SSH_HOST);
+
+ assert_eq!(result, Ok(()));
+ }
+
+ #[test]
+ #[cfg(target_family = "unix")]
+ fn test_send_staged_files_home() {
+ let tmp = setup_integration("test_send_staged_files_home");
+
+ let src = tmp.local.join("home").join("test_send_staged_files_home");
+ let src_foo = src.join("foo");
+ let src_bar = src.join("dir").join("bar");
+ fs::create_dir_all(&src_bar.parent().unwrap()).unwrap();
+ write_file(&src_foo, "contents of foo");
+ write_file(&src_bar, "contents of bar");
+
+ let result = send_staged_files(&tmp.local, SSH_HOST);
+
+ let dst_foo = tmp.ssh.join("foo");
+ let dst_bar = tmp.ssh.join("dir").join("bar");
+ assert_eq!(result, Ok(()));
+ assert_eq!(dst_foo.exists(), true);
+ assert_eq!(read_file(&dst_foo), "contents of foo");
+ assert_eq!(dst_bar.exists(), true);
+ assert_eq!(read_file(&dst_bar), "contents of bar");
+ assert_eq!(tmp.local.join("home").exists(), false);
+ assert_eq!(tmp.local.join("root").exists(), false);
+ }
+
+ #[test]
+ #[cfg(target_family = "unix")]
+ fn test_send_staged_files_root() {
+ let tmp = setup_integration("test_send_staged_files_root");
+
+ let src = tmp.local.join("root").join("home").join("test")
+ .join("test_send_staged_files_root");
+ let src_foo = src.join("foo");
+ let src_bar = src.join("dir").join("bar");
+ fs::create_dir_all(&src_bar.parent().unwrap()).unwrap();
+ write_file(&src_foo, "contents of foo");
+ write_file(&src_bar, "contents of bar");
+
+ let result = send_staged_files(&tmp.local, SSH_HOST);
+
+ let dst_foo = tmp.ssh.join("foo");
+ let dst_bar = tmp.ssh.join("dir").join("bar");
+ assert_eq!(result, Ok(()));
+ assert_eq!(dst_foo.exists(), true);
+ assert_eq!(read_file(&dst_foo), "contents of foo");
+ assert_eq!(dst_bar.exists(), true);
+ assert_eq!(read_file(&dst_bar), "contents of bar");
+ assert_eq!(tmp.local.join("home").exists(), false);
+ assert_eq!(tmp.local.join("root").exists(), false);
+ }
+
+ #[test]
+ #[cfg(target_family = "unix")]
+ fn test_send_dir_basic() {
+ let tmp = setup_integration("test_send_dir_basic");
+
+ write_file(&tmp.local.join("foo"), "contents of foo");
+ write_file(&tmp.local.join("bar"), "contents of bar");
+
+ let dst = "~/test_send_dir_basic";
+ let dst_foo = tmp.ssh.join("foo");
+ let dst_bar = tmp.ssh.join("bar");
+
+ let result = send_dir(tmp.local.to_str().unwrap(), dst, SSH_HOST);
+
+ assert_eq!(result, Ok(()));
+ assert_eq!(dst_foo.exists(), true);
+ assert_eq!(read_file(&dst_foo), "contents of foo");
+ assert_eq!(dst_bar.exists(), true);
+ assert_eq!(read_file(&dst_bar), "contents of bar");
+ }
+
+ #[test]
+ #[cfg(target_family = "unix")]
+ fn test_send_dir_nested_dir() {
+ let tmp = setup_integration("test_send_dir_nested_dir");
+
+ let src_foo = tmp.local.join("foo");
+ let src_bar = tmp.local.join("dir").join("bar");
+ write_file(&src_foo, "contents of foo");
+ fs::create_dir_all(&src_bar.parent().unwrap()).unwrap();
+ write_file(&src_bar, "contents of bar");
+
+ let dst = "~/test_send_dir_nested_dir";
+ let dst_foo = tmp.ssh.join("foo");
+ let dst_bar = tmp.ssh.join("dir").join("bar");
+
+ let result = send_dir(tmp.local.to_str().unwrap(), dst, SSH_HOST);
+
+ assert_eq!(result, Ok(()));
+ assert_eq!(dst_foo.exists(), true);
+ assert_eq!(read_file(&dst_foo), "contents of foo");
+ assert_eq!(dst_bar.exists(), true);
+ assert_eq!(read_file(&dst_bar), "contents of bar");
+ }
+
+ #[test]
+ #[cfg(target_family = "unix")]
+ fn test_send_dir_merge_dir() {
+ let tmp = setup_integration("test_send_dir_merge_dir");
+
+ let src_bar = tmp.local.join("dir").join("bar");
+ fs::create_dir_all(src_bar.parent().unwrap()).unwrap();
+ write_file(&src_bar, "new contents of bar");
+
+ let dst = "~/test_send_dir_merge_dir";
+ let dst_foo = tmp.ssh.join("foo");
+ let dst_bar = tmp.ssh.join("dir").join("bar");
+ let dst_baz = tmp.ssh.join("dir").join("baz");
+ write_file(&dst_foo, "old contents of foo");
+ fs::create_dir_all(&dst_bar.parent().unwrap()).unwrap();
+ write_file(&dst_bar, "old contents of bar");
+ write_file(&dst_baz, "old contents of baz");
+
+ let result = send_dir(tmp.local.to_str().unwrap(), dst, SSH_HOST);
+
+ assert_eq!(result, Ok(()));
+ assert_eq!(dst_foo.exists(), true);
+ assert_eq!(read_file(&dst_foo), "old contents of foo");
+ assert_eq!(dst_bar.exists(), true);
+ assert_eq!(read_file(&dst_bar), "new contents of bar");
+ assert_eq!(dst_baz.exists(), true);
+ assert_eq!(read_file(&dst_baz), "old contents of baz");
+ }
+
+ #[test]
+ #[cfg(target_family = "unix")]
+ fn test_send_command_basic() {
+ let tmp = setup_integration("test_send_command_basic");
+
+ let dst = "~/test_send_command_basic/foo";
+ let dst_real = tmp.ssh.join("foo");
+ let cmd = format!("echo 'contents of foo' > {}", dst);
+
+ let result = send_command(&cmd, SSH_HOST);
+
+ assert_eq!(result, Ok(()));
+ assert_eq!(dst_real.exists(), true);
+ assert_eq!(read_file(&dst_real), "contents of foo\n");
+ }
+}
diff --git a/src/utils.rs b/src/utils.rs
@@ -1,364 +0,0 @@
-use shellexpand::tilde;
-use std::io;
-use std::fs;
-#[cfg(target_family = "unix")]
-use std::os::unix::fs::symlink;
-use std::path::{PathBuf, absolute};
-use std::process::Command;
-
-/// Copies the contents of a local file to another local file.
-///
-/// Tildes are expanded if present and the destination file is overwritten if
-/// necessary.
-pub fn copy_file(src: &str, dst: &str) -> io::Result<()> {
- if absolute(src)? == absolute(dst)? { return Ok(()); }
- let _dst = prepare_path(dst)?;
- fs::copy(src, _dst)?;
- Ok(())
-}
-
-/// Creates a symbolic link to a local file.
-///
-/// Tildes are expanded if present and the destination file is overwritten if
-/// necessary. On non-Unix platforms, a hard link will be created instead.
-#[cfg(target_family = "unix")]
-pub fn link_file(src: &str, dst: &str) -> io::Result<()> {
- if absolute(src)? == absolute(dst)? { return Ok(()); }
- let _dst = prepare_path(dst)?;
- symlink(fs::canonicalize(src)?, _dst)?;
- Ok(())
-}
-#[cfg(not(target_family = "unix"))]
-pub fn link_file(src: &str, dst: &str) -> io::Result<()> {
- if absolute(src)? == absolute(dst)? { return Ok(()); }
- let _dst = prepare_path(dst)?;
- fs::hard_link(src, _dst)?;
- Ok(())
-}
-
-/// Creates the parent directories of a path and return the path with tildes
-/// expanded.
-fn prepare_path(path: &str) -> io::Result<PathBuf> {
- let _dst: PathBuf = (&tilde(path).to_mut()).into();
- if let Some(_path) = _dst.parent() {
- fs::create_dir_all(_path)?;
- }
- if fs::symlink_metadata(&_dst).is_ok() {
- // Check for existing files, including broken symlinks
- fs::remove_file(&_dst)?;
- }
- Ok(_dst)
-}
-
-/// Executes a local shell script, optionally with a command prefix or postfix.
-///
-/// Uses sh on Unix and PowerShell on Windows.
-pub fn run_script(path: &str, prefix: &str, postfix: &str) -> Result<(), String>
-{
- // Use absolute() to avoid incompatible "UNC" paths on Windows:
- // https://github.com/rust-lang/rust/issues/42869
- let _path = absolute(path).map_err(|why| why.to_string())?;
- let status;
- if cfg!(target_family = "unix") {
- status = Command::new("sh")
- .arg("-c")
- .arg(format!("{} {} {}", prefix, _path.display(), postfix))
- .status()
- .map_err(|why| why.to_string())?;
- } else {
- status = Command::new("powershell")
- .args(["-ExecutionPolicy", "Bypass", "-Command"])
- .arg(format!("{} {} {}", prefix, _path.display(), postfix))
- .status()
- .map_err(|why| why.to_string())?;
- }
- if status.success() {
- Ok(())
- } else {
- Err(format!("Process exited with {status}"))
- }
-}
-
-#[cfg(test)]
-#[path = "../tests/common/mod.rs"]
-mod common;
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use common::{setup_integration, write_file};
-
- #[test]
- fn test_copy_file_create_dirs() {
- let tmp = setup_integration("test_copy_file_create_dirs");
-
- let src = &tmp.dir.join("foo");
- let dst = &tmp.dir.join("dir1").join("dir2").join("bar");
- write_file(src, "old contents of foo");
-
- let result = copy_file(src.to_str().unwrap(), dst.to_str().unwrap());
-
- write_file(src, "new contents of foo");
- let contents = fs::read_to_string(dst).unwrap();
- assert_eq!(result.is_ok(), true);
- assert_eq!(contents, "old contents of foo");
- }
-
- #[test]
- fn test_copy_file_same_file() {
- let tmp = setup_integration("test_copy_file_same_file");
-
- let src = &tmp.dir.join("foo");
- let dst = &tmp.dir.join("foo");
- write_file(src, "contents of foo");
-
- let result = copy_file(src.to_str().unwrap(), dst.to_str().unwrap());
-
- let contents = fs::read_to_string(dst).unwrap();
- assert_eq!(result.is_ok(), true);
- assert_eq!(contents, "contents of foo");
- }
-
- #[test]
- fn test_copy_file_existing_file() {
- let tmp = setup_integration("test_copy_file_existing_file");
-
- let src = &tmp.dir.join("foo");
- let dst = &tmp.dir.join("bar");
- write_file(src, "old contents of foo");
- write_file(dst, "old contents of bar");
-
- let result = copy_file(src.to_str().unwrap(), dst.to_str().unwrap());
-
- write_file(src, "new contents of foo");
- let contents = fs::read_to_string(dst).unwrap();
- assert_eq!(result.is_ok(), true);
- assert_eq!(contents, "old contents of foo");
- }
-
- #[test]
- #[cfg(target_family = "unix")]
- fn test_copy_file_existing_broken_symlink() {
- let tmp = setup_integration("test_copy_file_existing_broken_symlink");
-
- let src = &tmp.dir.join("foo");
- let dst = &tmp.dir.join("bar");
- write_file(src, "old contents of foo");
- symlink("missing", dst).unwrap();
-
- let result = copy_file(src.to_str().unwrap(), dst.to_str().unwrap());
-
- write_file(src, "new contents of foo");
- let contents = fs::read_to_string(dst).unwrap();
- assert_eq!(result.is_ok(), true);
- assert_eq!(contents, "old contents of foo");
- }
-
- #[test]
- #[cfg(target_family = "unix")]
- fn test_copy_file_tilde_expansion() {
- let tmp = setup_integration("test_copy_file_tilde_expansion");
-
- let src = &tmp.dir.join("foo");
- let dst = &tmp.dir.join("dir").join("bar");
- let dst_tilde = "~/test_copy_file_tilde_expansion/dir/bar";
- write_file(src, "old contents of foo");
-
- let result = copy_file(src.to_str().unwrap(), dst_tilde);
-
- write_file(src, "new contents of foo");
- let contents = fs::read_to_string(dst).unwrap();
- assert_eq!(result.is_ok(), true);
- assert_eq!(contents, "old contents of foo");
- }
-
- #[test]
- fn test_link_file_create_dirs() {
- let tmp = setup_integration("test_link_file_create_dirs");
-
- let src = &tmp.dir.join("foo");
- let dst = &tmp.dir.join("dir1").join("dir2").join("bar");
- write_file(src, "old contents of foo");
-
- let result = link_file(src.to_str().unwrap(), dst.to_str().unwrap());
-
- write_file(src, "new contents of foo");
- let contents = fs::read_to_string(dst).unwrap();
- assert_eq!(result.is_ok(), true);
- assert_eq!(contents, "new contents of foo");
- }
-
- #[test]
- fn test_link_file_same_file() {
- let tmp = setup_integration("test_link_file_same_file");
-
- let src = &tmp.dir.join("foo");
- let dst = &tmp.dir.join("foo");
- write_file(src, "contents of foo");
-
- let result = link_file(src.to_str().unwrap(), dst.to_str().unwrap());
-
- let contents = fs::read_to_string(dst).unwrap();
- assert_eq!(result.is_ok(), true);
- assert_eq!(contents, "contents of foo");
- }
-
- #[test]
- fn test_link_file_existing_file() {
- let tmp = setup_integration("test_link_file_existing_file");
-
- let src = &tmp.dir.join("foo");
- let dst = &tmp.dir.join("bar");
- write_file(src, "old contents of foo");
- write_file(dst, "old contents of bar");
-
- let result = link_file(src.to_str().unwrap(), dst.to_str().unwrap());
-
- write_file(src, "new contents of foo");
- let contents = fs::read_to_string(dst).unwrap();
- assert_eq!(result.is_ok(), true);
- assert_eq!(contents, "new contents of foo");
- }
-
- #[test]
- #[cfg(target_family = "unix")]
- fn test_link_file_existing_broken_symlink() {
- let tmp = setup_integration("test_link_file_existing_broken_symlink");
-
- let src = &tmp.dir.join("foo");
- let dst = &tmp.dir.join("bar");
- write_file(src, "old contents of foo");
- symlink("missing", dst).unwrap();
-
- let result = link_file(src.to_str().unwrap(), dst.to_str().unwrap());
-
- write_file(src, "new contents of foo");
- let contents = fs::read_to_string(dst).unwrap();
- assert_eq!(result.is_ok(), true);
- assert_eq!(contents, "new contents of foo");
- }
-
- #[test]
- #[cfg(target_family = "unix")]
- fn test_link_file_tilde_expansion() {
- let tmp = setup_integration("test_link_file_tilde_expansion");
-
- let src = &tmp.dir.join("foo");
- let dst = &tmp.dir.join("dir").join("bar");
- let dst_tilde = "~/test_link_file_tilde_expansion/dir/bar";
- write_file(src, "old contents of foo");
-
- let result = link_file(src.to_str().unwrap(), dst_tilde);
-
- write_file(src, "new contents of foo");
- let contents = fs::read_to_string(dst).unwrap();
- assert_eq!(result.is_ok(), true);
- assert_eq!(contents, "new contents of foo");
- }
-
- #[test]
- #[cfg(target_family = "unix")]
- fn test_link_file_relative_source() {
- let tmp = setup_integration("test_link_file_relative_source");
-
- let src = &tmp.dir.join("foo");
- let src_rel = "test_link_file_relative_source/foo";
- let dst = &tmp.dir.join("dir1").join("dir2").join("bar");
- write_file(src, "old contents of foo");
-
- let result = link_file(src_rel, dst.to_str().unwrap());
-
- write_file(src, "new contents of foo");
- let contents = fs::read_to_string(dst).unwrap();
- let link = fs::read_link(dst).unwrap();
- assert_eq!(result.is_ok(), true);
- assert_eq!(contents, "new contents of foo");
- assert_eq!(&link, src); // src changed to absolute path
- }
-
- #[test]
- #[cfg(target_family = "unix")]
- fn test_run_script_successful() {
- let tmp = setup_integration("test_run_script_successful");
-
- let src = &tmp.dir.join("foo");
- write_file(src, "exit 0");
-
- let result = run_script(src.to_str().unwrap(), "sh", "");
-
- assert_eq!(result.is_ok(), true);
- }
-
- #[test]
- #[cfg(target_family = "windows")]
- fn test_run_script_successful() {
- let tmp = setup_integration("test_run_script_successful");
-
- let src = &tmp.dir.join("foo.bat");
- write_file(src, "exit 0");
-
- let result = run_script(src.to_str().unwrap(), "", "");
-
- assert_eq!(result.is_ok(), true);
- }
-
- #[test]
- #[cfg(target_family = "unix")]
- fn test_run_script_failure() {
- let tmp = setup_integration("test_run_script_failure");
-
- let src = &tmp.dir.join("foo");
- write_file(src, "exit 2");
-
- let result = run_script(src.to_str().unwrap(), "sh", "");
-
- assert_eq!(result.is_ok(), false);
- assert_eq!(result.unwrap_err(), "Process exited with exit status: 2");
- }
-
- #[test]
- #[cfg(target_family = "windows")]
- fn test_run_script_failure() {
- let tmp = setup_integration("test_run_script_failure");
-
- let src = &tmp.dir.join("foo.bat");
- write_file(src, "exit 1");
-
- let result = run_script(src.to_str().unwrap(), "", "");
-
- assert_eq!(result.is_ok(), false);
- assert_eq!(result.unwrap_err(), "Process exited with exit code: 1");
- }
-
- #[test]
- #[cfg(target_family = "unix")]
- fn test_run_script_postfix() {
- let tmp = setup_integration("test_run_script_postfix");
-
- let src = &tmp.dir.join("foo");
- let dst = &tmp.dir.join("bar");
- write_file(src, &format!("echo $@ > {}", dst.to_str().unwrap()));
-
- let result = run_script(src.to_str().unwrap(), "sh", "arg1 arg2");
-
- let contents = fs::read_to_string(dst).unwrap();
- assert_eq!(result.is_ok(), true);
- assert_eq!(contents, "arg1 arg2\n");
- }
-
- #[test]
- #[cfg(target_family = "windows")]
- fn test_run_script_postfix() {
- let tmp = setup_integration("test_run_script_postfix");
-
- let src = &tmp.dir.join("foo.bat");
- let dst = &tmp.dir.join("bar");
- write_file(src, &format!("echo %* > {}", dst.to_str().unwrap()));
-
- let result = run_script(src.to_str().unwrap(), "", "arg1 arg2");
-
- let contents = fs::read_to_string(dst).unwrap();
- assert_eq!(result.is_ok(), true);
- assert_eq!(contents, "arg1 arg2 \r\n");
- }
-}
diff --git a/tests/.temp/ssh/.gitkeep b/tests/.temp/ssh/.gitkeep
@@ -0,0 +1,3 @@
+# This directory is mounted to the SSH docker container used for E2E tests. All
+# children are ignored, but this file guarantees that the directory will exist
+# on a fresh clone.
diff --git a/tests/basic.rs b/tests/basic.rs
@@ -1,33 +1,14 @@
+/// End to end tests that do not test general CLI behavior rather than specific
+/// installation behavior
+
mod common;
use common::*;
-use std::env::{current_exe, consts::EXE_SUFFIX};
-use std::fs::{copy, remove_file};
-use std::path::Path;
-
-/// Create a basic manifest file and its associated dotfiles in a directory
-fn manifest_1(dir: &Path) {
- // Copy files from examples
- let examples = current_exe().unwrap().parent().unwrap().to_path_buf()
- .join("../../../examples");
- let copy_file = |name: &str| {
- copy(examples.join(name), &dir.join(name)).unwrap();
- };
- copy_file("script.bat");
- copy_file("script.sh");
- copy_file("manifest.yml");
- copy_file("manifest-windows-test.yml");
-
- // Create simplified config files
- write_file(&dir.join("bashrc"), "bash #1");
- write_file(&dir.join("gitconfig"), "git #1");
- write_file(&dir.join("vimrc"), "vim #1");
-
-}
+use std::env::consts::EXE_SUFFIX;
#[test]
-fn test_help() {
- let (_dir, mut cmd) = setup_e2e("test_help");
+fn test_basic_help() {
+ let (_dirs, mut cmd) = setup_e2e_local("test_basic_help");
cmd.arg("--help");
let expected = format!("\
A minimal, flexible, dotfile installer
@@ -39,6 +20,7 @@ Arguments:
Options:
-t, --tag-rules [<TAG_RULES>...] The set of tag rules to enforce
+ --host <HOST> Install dotfiles on another machine over SSH
-c, --copy Interpret link commands as copy commands
-n, --dry-run Do a trial run without any permanent changes
-h, --help Print help
@@ -49,154 +31,61 @@ Options:
}
#[test]
-#[cfg(target_family = "unix")]
-fn test_standard() {
- let (dir, mut cmd) = setup_e2e("test_standard");
- cmd.args(["manifest.yml", "-t", "linux"]);
- manifest_1(&dir.dir);
-
- let expected = "\
-[1/3] Copy gitconfig to ~/.gitconfig.coliru
-[2/3] Link bashrc to ~/.bashrc.coliru
-[2/3] Link vimrc to ~/.vimrc.coliru
-[2/3] Run sh script.sh arg1 linux
-script.sh called with arg1 linux
-";
- assert_eq!(&stdout_to_string(&mut cmd), expected);
- assert_eq!(&stderr_to_string(&mut cmd), "");
-
- // Assert files are correctly copied/linked/run
- write_file(&dir.dir.join("bashrc"), "bash #2");
- write_file(&dir.dir.join("gitconfig"), "git #2");
- write_file(&dir.dir.join("vimrc"), "vim #2");
- let bash_contents = read_file(&dir.dir.join(".bashrc.coliru"));
- let git_contents = read_file(&dir.dir.join(".gitconfig.coliru"));
- let vim1_contents = read_file(&dir.dir.join(".vimrc.coliru"));
- let vim2_exists = dir.dir.join("_vimrc.coliru").exists();
- let log_contents = read_file(&dir.dir.join("log.txt"));
- assert_eq!(bash_contents, "bash #2");
- assert_eq!(git_contents, "git #1");
- assert_eq!(vim1_contents, "vim #2");
- assert_eq!(vim2_exists, false);
- assert_eq!(log_contents, "script.sh called with arg1 linux\n");
-}
-
-#[test]
-#[cfg(target_family = "windows")]
-fn test_standard() {
- let (dir, mut cmd) = setup_e2e("test_standard");
- cmd.args(["manifest-windows-test.yml", "-t", "windows"]);
- manifest_1(&dir.dir);
-
- let expected = "\
-[1/3] Copy gitconfig to .gitconfig.coliru
-[3/3] Link vimrc to _vimrc.coliru
-[3/3] Run script.bat arg1 windows
-script.bat called with arg1 windows\r
-";
- assert_eq!(&stdout_to_string(&mut cmd), expected);
- assert_eq!(&stderr_to_string(&mut cmd), "");
+fn test_basic_empty_manifest() {
+ let (dirs, mut cmd) = setup_e2e_local("test_basic_empty_manifest");
+ cmd.args(["manifest.yml"]);
+ write_file(&dirs.local.join("manifest.yml"), "");
- // Assert files are correctly copied/linked/run
- write_file(&dir.dir.join("gitconfig"), "git #2");
- write_file(&dir.dir.join("vimrc"), "vim #2");
- let bash_exists = dir.dir.join(".bashrc.coliru").exists();
- let git_contents = read_file(&dir.dir.join(".gitconfig.coliru"));
- let vim1_exists = dir.dir.join(".vimrc.coliru").exists();
- let vim2_contents = read_file(&dir.dir.join("_vimrc.coliru"));
- let log_contents = read_file(&dir.dir.join("log.txt"));
- assert_eq!(bash_exists, false);
- assert_eq!(git_contents, "git #1");
- assert_eq!(vim1_exists, false);
- assert_eq!(vim2_contents, "vim #2");
- assert_eq!(log_contents, "script.bat called with arg1 windows \r\n");
+ let expected = "Error: missing field `steps`\n";
+ assert_eq!(&stderr_to_string(&mut cmd), expected);
+ assert_eq!(&stdout_to_string(&mut cmd), "");
}
#[test]
#[cfg(target_family = "unix")]
-fn test_run_alternate_tag_rules_1() {
- let (dir, mut cmd) = setup_e2e("test_run_alternate_tag_rules_1");
- cmd.args(["manifest.yml", "-t", "linux", "^windows"]);
- manifest_1(&dir.dir);
-
- let expected = "\
-[2/3] Link bashrc to ~/.bashrc.coliru
-[2/3] Link vimrc to ~/.vimrc.coliru
-[2/3] Run sh script.sh arg1 linux ^windows
-script.sh called with arg1 linux ^windows
-";
- assert_eq!(&stdout_to_string(&mut cmd), expected);
- assert_eq!(&stderr_to_string(&mut cmd), "");
+fn test_basic_missing_manifest() {
+ let (_dirs, mut cmd) = setup_e2e_local("test_basic_missing_manifest");
+ cmd.args(["missing.yml"]);
- // Assert files are correctly copied/linked/run
- write_file(&dir.dir.join("bashrc"), "bash #2");
- write_file(&dir.dir.join("vimrc"), "vim #2");
- let bash_contents = read_file(&dir.dir.join(".bashrc.coliru"));
- let git_exists = dir.dir.join(".gitconfig.coliru").exists();
- let vim1_contents = read_file(&dir.dir.join(".vimrc.coliru"));
- let vim2_exists = dir.dir.join("_vimrc.coliru").exists();
- let log_contents = read_file(&dir.dir.join("log.txt"));
- assert_eq!(bash_contents, "bash #2");
- assert_eq!(git_exists, false);
- assert_eq!(vim1_contents, "vim #2");
- assert_eq!(vim2_exists, false);
- assert_eq!(log_contents, "script.sh called with arg1 linux ^windows\n");
+ let expected = "Error: No such file or directory (os error 2)\n";
+ assert_eq!(&stderr_to_string(&mut cmd), expected);
+ assert_eq!(&stdout_to_string(&mut cmd), "");
}
#[test]
-#[cfg(target_family = "unix")]
-fn test_run_alternate_tag_rules_2() {
- let (dir, mut cmd) = setup_e2e("test_run_alternate_tag_rules_2");
- cmd.args(["manifest.yml", "-t", "macos"]);
- manifest_1(&dir.dir);
-
- let expected = "\
-[1/3] Copy gitconfig to ~/.gitconfig.coliru
-[2/3] Link bashrc to ~/.bashrc.coliru
-[2/3] Link vimrc to ~/.vimrc.coliru
-[2/3] Run sh script.sh arg1 macos
-script.sh called with arg1 macos
-";
- assert_eq!(&stdout_to_string(&mut cmd), expected);
- assert_eq!(&stderr_to_string(&mut cmd), "");
+#[cfg(target_family = "windows")]
+fn test_basic_missing_manifest() {
+ let (_dirs, mut cmd) = setup_e2e_local("test_basic_missing_manifest");
+ cmd.args(["missing.yml"]);
- // Assert files are correctly copied/linked/run
- write_file(&dir.dir.join("bashrc"), "bash #2");
- write_file(&dir.dir.join("gitconfig"), "git #2");
- write_file(&dir.dir.join("vimrc"), "vim #2");
- let bash_contents = read_file(&dir.dir.join(".bashrc.coliru"));
- let git_contents = read_file(&dir.dir.join(".gitconfig.coliru"));
- let vim1_contents = read_file(&dir.dir.join(".vimrc.coliru"));
- let vim2_exists = dir.dir.join("_vimrc.coliru").exists();
- let log_contents = read_file(&dir.dir.join("log.txt"));
- assert_eq!(bash_contents, "bash #2");
- assert_eq!(git_contents, "git #1");
- assert_eq!(vim1_contents, "vim #2");
- assert_eq!(vim2_exists, false);
- assert_eq!(log_contents, "script.sh called with arg1 macos\n");
+ let expected = "Error: The system cannot find the file specified. \
+ (os error 2)\n";
+ assert_eq!(&stderr_to_string(&mut cmd), expected);
+ assert_eq!(&stdout_to_string(&mut cmd), "");
}
#[test]
-fn test_dry_run() {
- let (dir, mut cmd) = setup_e2e("test_dry_run");
- cmd.args(["manifest.yml", "--dry-run", "-t", "linux"]);
- manifest_1(&dir.dir);
+#[cfg(target_family = "unix")]
+fn test_basic_absolute_manifest() {
+ let (dirs, mut cmd) = setup_e2e_local("test_basic_absolute_manifest");
+ let manifest_path = dirs.local.join("manifest.yml");
+ cmd.args([&manifest_path.to_str().unwrap(), "--dry-run", "-t", "linux"]);
let expected = "\
-[1/3] Copy gitconfig to ~/.gitconfig.coliru (DRY RUN)
-[2/3] Link bashrc to ~/.bashrc.coliru (DRY RUN)
-[2/3] Link vimrc to ~/.vimrc.coliru (DRY RUN)
+[1/3] Copy gitconfig to ~/.gitconfig (DRY RUN)
+[2/3] Link bashrc to ~/.bashrc (DRY RUN)
+[2/3] Link vimrc to ~/.vimrc (DRY RUN)
[2/3] Run sh script.sh arg1 linux (DRY RUN)
";
- assert_eq!(&stdout_to_string(&mut cmd), expected);
assert_eq!(&stderr_to_string(&mut cmd), "");
+ assert_eq!(&stdout_to_string(&mut cmd), expected);
// Assert files are correctly copied/linked/run
- let bash_exists = dir.dir.join(".bashrc.coliru").exists();
- let git_exists = dir.dir.join(".gitconfig.coliru").exists();
- let vim1_exists = dir.dir.join(".vimrc.coliru").exists();
- let vim2_exists = dir.dir.join("_vimrc.coliru").exists();
- let log_exists = dir.dir.join("log.txt").exists();
+ let bash_exists = dirs.home.join(".bashrc").exists();
+ let git_exists = dirs.home.join(".gitconfig").exists();
+ let vim1_exists = dirs.home.join(".vimrc").exists();
+ let vim2_exists = dirs.home.join("_vimrc").exists();
+ let log_exists = dirs.home.join("log.txt").exists();
assert_eq!(bash_exists, false);
assert_eq!(git_exists, false);
assert_eq!(vim1_exists, false);
@@ -205,247 +94,27 @@ fn test_dry_run() {
}
#[test]
-#[cfg(target_family = "unix")]
-fn test_copy() {
- let (dir, mut cmd) = setup_e2e("test_copy");
- cmd.args(["manifest.yml", "--copy", "-t", "linux"]);
- manifest_1(&dir.dir);
-
- let expected = "\
-[1/3] Copy gitconfig to ~/.gitconfig.coliru
-[2/3] Copy bashrc to ~/.bashrc.coliru
-[2/3] Copy vimrc to ~/.vimrc.coliru
-[2/3] Run sh script.sh arg1 linux
-script.sh called with arg1 linux
-";
- assert_eq!(&stdout_to_string(&mut cmd), expected);
- assert_eq!(&stderr_to_string(&mut cmd), "");
-
- // Assert files are correctly copied/linked/run
- write_file(&dir.dir.join("bashrc"), "bash #2");
- write_file(&dir.dir.join("gitconfig"), "git #2");
- write_file(&dir.dir.join("vimrc"), "vim #2");
- let bash_contents = read_file(&dir.dir.join(".bashrc.coliru"));
- let git_contents = read_file(&dir.dir.join(".gitconfig.coliru"));
- let vim1_contents = read_file(&dir.dir.join(".vimrc.coliru"));
- let vim2_exists = dir.dir.join("_vimrc.coliru").exists();
- let log_contents = read_file(&dir.dir.join("log.txt"));
- assert_eq!(bash_contents, "bash #1");
- assert_eq!(git_contents, "git #1");
- assert_eq!(vim1_contents, "vim #1");
- assert_eq!(vim2_exists, false);
- assert_eq!(log_contents, "script.sh called with arg1 linux\n");
-}
-
-#[test]
#[cfg(target_family = "windows")]
-fn test_copy() {
- let (dir, mut cmd) = setup_e2e("test_copy");
- cmd.args(["manifest-windows-test.yml", "--copy", "-t", "windows"]);
- manifest_1(&dir.dir);
-
- let expected = "\
-[1/3] Copy gitconfig to .gitconfig.coliru
-[3/3] Copy vimrc to _vimrc.coliru
-[3/3] Run script.bat arg1 windows
-script.bat called with arg1 windows\r
-";
- assert_eq!(&stdout_to_string(&mut cmd), expected);
- assert_eq!(&stderr_to_string(&mut cmd), "");
-
- // Assert files are correctly copied/linked/run
- write_file(&dir.dir.join("gitconfig"), "git #2");
- write_file(&dir.dir.join("vimrc"), "vim #2");
- let bash_exists = dir.dir.join(".bashrc.coliru").exists();
- let git_contents = read_file(&dir.dir.join(".gitconfig.coliru"));
- let vim1_exists = dir.dir.join(".vimrc.coliru").exists();
- let vim2_contents = read_file(&dir.dir.join("_vimrc.coliru"));
- let log_contents = read_file(&dir.dir.join("log.txt"));
- assert_eq!(bash_exists, false);
- assert_eq!(git_contents, "git #1");
- assert_eq!(vim1_exists, false);
- assert_eq!(vim2_contents, "vim #1");
- assert_eq!(log_contents, "script.bat called with arg1 windows \r\n");
-}
-
-#[test]
-#[cfg(target_family = "unix")]
-fn test_run_failure() {
- let (dir, mut cmd) = setup_e2e("test_run_failure");
- cmd.args(["manifest.yml", "-t", "linux"]);
- manifest_1(&dir.dir);
- write_file(&dir.dir.join("script.sh"), "exit 1");
-
- let expected_stdout = "\
-[1/3] Copy gitconfig to ~/.gitconfig.coliru
-[2/3] Link bashrc to ~/.bashrc.coliru
-[2/3] Link vimrc to ~/.vimrc.coliru
-[2/3] Run sh script.sh arg1 linux
-";
- let expected_stderr = " Error: Process exited with exit status: 1\n";
- assert_eq!(&stdout_to_string(&mut cmd), expected_stdout);
- assert_eq!(&stderr_to_string(&mut cmd), expected_stderr);
-
- // Assert files are correctly copied/linked/run
- write_file(&dir.dir.join("bashrc"), "bash #2");
- write_file(&dir.dir.join("gitconfig"), "git #2");
- write_file(&dir.dir.join("vimrc"), "vim #2");
- let bash_contents = read_file(&dir.dir.join(".bashrc.coliru"));
- let git_contents = read_file(&dir.dir.join(".gitconfig.coliru"));
- let vim1_contents = read_file(&dir.dir.join(".vimrc.coliru"));
- let vim2_exists = dir.dir.join("_vimrc.coliru").exists();
- assert_eq!(bash_contents, "bash #2");
- assert_eq!(git_contents, "git #1");
- assert_eq!(vim1_contents, "vim #2");
- assert_eq!(vim2_exists, false);
-}
-
-#[test]
-#[cfg(target_family = "windows")]
-fn test_run_failure() {
- let (dir, mut cmd) = setup_e2e("test_run_failure");
- cmd.args(["manifest-windows-test.yml", "-t", "windows"]);
- manifest_1(&dir.dir);
- write_file(&dir.dir.join("script.bat"), "@echo off\r\nexit 1");
-
- let expected_stdout = "\
-[1/3] Copy gitconfig to .gitconfig.coliru
-[3/3] Link vimrc to _vimrc.coliru
-[3/3] Run script.bat arg1 windows
-";
- let expected_stderr = " Error: Process exited with exit code: 1\n";
- assert_eq!(&stdout_to_string(&mut cmd), expected_stdout);
- assert_eq!(&stderr_to_string(&mut cmd), expected_stderr);
-
- // Assert files are correctly copied/linked/run
- write_file(&dir.dir.join("gitconfig"), "git #2");
- write_file(&dir.dir.join("vimrc"), "vim #2");
- let bash_exists = dir.dir.join(".bashrc.coliru").exists();
- let git_contents = read_file(&dir.dir.join(".gitconfig.coliru"));
- let vim1_exists = dir.dir.join(".vimrc.coliru").exists();
- let vim2_contents = read_file(&dir.dir.join("_vimrc.coliru"));
- assert_eq!(bash_exists, false);
- assert_eq!(git_contents, "git #1");
- assert_eq!(vim1_exists, false);
- assert_eq!(vim2_contents, "vim #2");
-}
-
-#[test]
-#[cfg(target_family = "unix")]
-fn test_missing_file() {
- let (dir, mut cmd) = setup_e2e("test_missing_file");
- cmd.args(["manifest.yml", "-t", "linux"]);
- manifest_1(&dir.dir);
- remove_file(&dir.dir.join("vimrc")).unwrap();
-
- let expected_stdout = "\
-[1/3] Copy gitconfig to ~/.gitconfig.coliru
-[2/3] Link bashrc to ~/.bashrc.coliru
-[2/3] Link vimrc to ~/.vimrc.coliru
-[2/3] Run sh script.sh arg1 linux
-script.sh called with arg1 linux
-";
- let expected_stderr = " Error: No such file or directory \
- (os error 2)\n";
- assert_eq!(&stdout_to_string(&mut cmd), expected_stdout);
- assert_eq!(&stderr_to_string(&mut cmd), expected_stderr);
-
- // Assert files are correctly copied/linked/run
- write_file(&dir.dir.join("bashrc"), "bash #2");
- write_file(&dir.dir.join("gitconfig"), "git #2");
- let bash_contents = read_file(&dir.dir.join(".bashrc.coliru"));
- let git_contents = read_file(&dir.dir.join(".gitconfig.coliru"));
- let log_contents = read_file(&dir.dir.join("log.txt"));
- assert_eq!(bash_contents, "bash #2");
- assert_eq!(git_contents, "git #1");
- assert_eq!(log_contents, "script.sh called with arg1 linux\n");
-}
-
-#[test]
-#[cfg(target_family = "windows")]
-fn test_missing_file() {
- let (dir, mut cmd) = setup_e2e("test_missing_file");
- cmd.args(["manifest-windows-test.yml", "-t", "windows"]);
- manifest_1(&dir.dir);
- remove_file(&dir.dir.join("vimrc")).unwrap();
-
- let expected_stdout = "\
-[1/3] Copy gitconfig to .gitconfig.coliru
-[3/3] Link vimrc to _vimrc.coliru
-[3/3] Run script.bat arg1 windows
-script.bat called with arg1 windows\r
-";
- let expected_stderr = " Error: The system cannot find the file specified. \
- (os error 2)\n";
- assert_eq!(&stdout_to_string(&mut cmd), expected_stdout);
- assert_eq!(&stderr_to_string(&mut cmd), expected_stderr);
-
- // Assert files are correctly copied/linked/run
- write_file(&dir.dir.join("gitconfig"), "git #2");
- let bash_exists = dir.dir.join(".bashrc.coliru").exists();
- let git_contents = read_file(&dir.dir.join(".gitconfig.coliru"));
- let log_contents = read_file(&dir.dir.join("log.txt"));
- assert_eq!(bash_exists, false);
- assert_eq!(git_contents, "git #1");
- assert_eq!(log_contents, "script.bat called with arg1 windows \r\n");
-}
-
-#[test]
-fn test_empty_manifest() {
- let (dir, mut cmd) = setup_e2e("test_empty_manifest");
- cmd.args(["manifest.yml"]);
- write_file(&dir.dir.join("manifest.yml"), "");
-
- let expected = "Error: missing field `steps`\n";
- assert_eq!(&stdout_to_string(&mut cmd), "");
- assert_eq!(&stderr_to_string(&mut cmd), expected);
-}
-
-#[test]
-#[cfg(target_family = "unix")]
-fn test_missing_manifest() {
- let (_dir, mut cmd) = setup_e2e("test_missing_manifest");
- cmd.args(["missing.yml"]);
-
- let expected = "Error: No such file or directory (os error 2)\n";
- assert_eq!(&stdout_to_string(&mut cmd), "");
- assert_eq!(&stderr_to_string(&mut cmd), expected);
-}
-
-#[test]
-#[cfg(target_family = "windows")]
-fn test_missing_manifest() {
- let (_dir, mut cmd) = setup_e2e("test_missing_manifest");
- cmd.args(["missing.yml"]);
-
- let expected = "Error: The system cannot find the file specified. \
- (os error 2)\n";
- assert_eq!(&stdout_to_string(&mut cmd), "");
- assert_eq!(&stderr_to_string(&mut cmd), expected);
-}
-
-#[test]
-fn test_absolute_manifest() {
- let (dir, mut cmd) = setup_e2e("test_absolute_manifest");
- let manifest_path = dir.dir.join("manifest.yml");
+fn test_basic_absolute_manifest() {
+ let (dirs, mut cmd) = setup_e2e_local("test_basic_absolute_manifest");
+ let manifest_path = dirs.local.join("manifest.yml");
cmd.args([&manifest_path.to_str().unwrap(), "--dry-run", "-t", "linux"]);
- manifest_1(&dir.dir);
let expected = "\
-[1/3] Copy gitconfig to ~/.gitconfig.coliru (DRY RUN)
-[2/3] Link bashrc to ~/.bashrc.coliru (DRY RUN)
-[2/3] Link vimrc to ~/.vimrc.coliru (DRY RUN)
+[1/3] Copy gitconfig to .gitconfig (DRY RUN)
+[2/3] Link bashrc to .bashrc (DRY RUN)
+[2/3] Link vimrc to .vimrc (DRY RUN)
[2/3] Run sh script.sh arg1 linux (DRY RUN)
";
- assert_eq!(&stdout_to_string(&mut cmd), expected);
assert_eq!(&stderr_to_string(&mut cmd), "");
+ assert_eq!(&stdout_to_string(&mut cmd), expected);
// Assert files are correctly copied/linked/run
- let bash_exists = dir.dir.join(".bashrc.coliru").exists();
- let git_exists = dir.dir.join(".gitconfig.coliru").exists();
- let vim1_exists = dir.dir.join(".vimrc.coliru").exists();
- let vim2_exists = dir.dir.join("_vimrc.coliru").exists();
- let log_exists = dir.dir.join("log.txt").exists();
+ let bash_exists = dirs.local.join(".bashrc").exists();
+ let git_exists = dirs.local.join(".gitconfig").exists();
+ let vim1_exists = dirs.local.join(".vimrc").exists();
+ let vim2_exists = dirs.local.join("_vimrc").exists();
+ let log_exists = dirs.local.join("log.txt").exists();
assert_eq!(bash_exists, false);
assert_eq!(git_exists, false);
assert_eq!(vim1_exists, false);
diff --git a/tests/common/mod.rs b/tests/common/mod.rs
@@ -6,59 +6,142 @@ use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
-/// Stores the path to a temporary directory that is automatically deleted
-/// when the value is dropped.
-///
-/// Adapted from ripgrep's tests (crates/ignore/src/lib.rs)
-pub struct TempDir {
- pub dir: PathBuf
+/// The SSH test server
+pub const SSH_HOST: &str = "test@localhost"; // TODO: add explicit port
+
+/// A set of temporary directories that are automatically deleted when the value
+/// is dropped
+pub struct TempDirs {
+ /// A temporary directory that is located at or in $HOME on Unix
+ pub home: PathBuf,
+
+ /// A temporary directory that is located at or under the CWD
+ pub local: PathBuf,
+
+ /// A temporary directory that is mounted to the SSH server under $HOME
+ pub ssh: PathBuf,
+
+ /// A temporary directory that is mounted to the SSH server under ~/.coliru
+ pub ssh_cwd: PathBuf,
}
-impl Drop for TempDir {
+impl Drop for TempDirs {
fn drop(&mut self) {
- fs::remove_dir_all(&self.dir).unwrap();
+ fs::remove_dir_all(&self.home).unwrap();
+ fs::remove_dir_all(&self.local).unwrap();
+ fs::remove_dir_all(&self.ssh).unwrap();
+ fs::remove_dir_all(&self.ssh_cwd).unwrap();
}
}
-impl TempDir {
- fn new(name: &str) -> TempDir {
- let dir = env::temp_dir().join("coliru-tests").join(name);
- assert_eq!(dir.exists(), false);
- fs::create_dir_all(&dir).unwrap();
- TempDir { dir }
+impl TempDirs {
+ fn new(name: &str) -> TempDirs {
+ // The CWD of the current process is always the repository root
+ let dir = env::current_dir().unwrap().join("tests").join(".temp");
+
+ let home = dir.join("home").join(name);
+ let local = dir.join("local").join(name);
+ let ssh = dir.join("ssh").join(name);
+ let ssh_cwd = dir.join("ssh").join(".coliru").join(name);
+
+ assert_eq!(home.exists(), false);
+ assert_eq!(local.exists(), false);
+ assert_eq!(ssh.exists(), false);
+ assert_eq!(ssh_cwd.exists(), false);
+
+ fs::create_dir_all(&home).unwrap();
+ fs::create_dir_all(&local).unwrap();
+ fs::create_dir_all(&ssh).unwrap();
+ fs::create_dir_all(&ssh_cwd).unwrap();
+
+ TempDirs { home, local, ssh, ssh_cwd }
}
}
-/// Creates a temporary directory with a certain name and sets $HOME and the
-/// CWD to the parent directory.
+/// Initializes temporary directories for integration tests
///
-/// All tests in this module use the same values for $HOME and the CWD,
-/// which prevents issues when tests are run in multiple threads.
-pub fn setup_integration(name: &str) -> TempDir {
- let dir = TempDir::new(name);
- let root = dir.dir.parent().unwrap();
- env::set_current_dir(root).unwrap();
+/// On Unix, $HOME is set to the parent directory of the home temporary
+/// directory, which is the same for all integration tests. This prevents issues
+/// when tests are run in multiple threads.
+pub fn setup_integration(name: &str) -> TempDirs {
+ let dirs = TempDirs::new(name);
if cfg!(target_family = "unix") {
- env::set_var("HOME", root);
+ env::set_var("HOME", dirs.home.parent().unwrap());
}
- dir
+ dirs
}
-/// Creates a temporary directory with a certain name and create a new coliru
-/// Command with $HOME and the CWD set the the temporary directory.
+/// Initializes temporary directories and a coliru Command for e2e tests
///
-/// Adapted from ripgrep's tests (tests/utils.rs)
-pub fn setup_e2e(name: &str) -> (TempDir, Command) {
- let dir = TempDir::new(name);
+/// The Command's CWD is set to the local temporary directory, and on Unix, the
+/// Command's $HOME variable is set to the home temporary directory.
+fn setup_e2e(name: &str) -> (TempDirs, Command) {
+ let dirs = TempDirs::new(name);
let exe = env::current_exe().unwrap().parent().unwrap().to_path_buf()
.join(format!("../coliru{}", env::consts::EXE_SUFFIX));
let mut cmd = Command::new(exe);
- cmd.current_dir(&dir.dir);
-
+ cmd.current_dir(&dirs.local);
if cfg!(target_family = "unix") {
- cmd.env("HOME", &dir.dir);
+ cmd.env("HOME", &dirs.home);
}
- (dir, cmd)
+ (dirs, cmd)
+}
+
+/// Initializes temporary directories and a coliru Command for local e2e tests
+///
+/// A test dotfile repo is copied to the local folder, to be installed to $HOME
+/// on Unix and CWD on Windows.
+pub fn setup_e2e_local(name: &str) -> (TempDirs, Command) {
+ let (dirs, cmd) = setup_e2e(name);
+
+ // It's difficult to mock $HOME on Windows, so install dotfiles in CWD
+ let home_dir = if cfg!(target_family = "unix") { "~/" } else { "" };
+ copy_manifest(&dirs.local, home_dir, "");
+
+ (dirs, cmd)
+}
+
+/// Initializes temporary directories and a coliru Command for ssh e2e tests
+///
+/// A test dotfile repo is copied to the local folder, to be installed to
+/// ~/test_name/ with scripts copied to ~/.coliru/test_name/, and the --host
+/// argument is set to the test SSH server.
+pub fn setup_e2e_ssh(name: &str) -> (TempDirs, Command) {
+ let (dirs, mut cmd) = setup_e2e(name);
+ cmd.args(["--host", SSH_HOST]);
+
+ // Replace ~/ and scripts/ with custom directory to isolate SSH tests
+ copy_manifest(&dirs.local, &format!("~/{name}/"), &format!("{name}/"));
+
+ (dirs, cmd)
+}
+
+/// Create a basic manifest file and its associated dotfiles in a directory
+///
+/// All occurrences of the string "~/" and "scripts/" (e.g. in manifest.yml and
+/// scripts/*) will be replaced with the value of home_dir and script_dir
+/// respectively to ensures that dotfiles are isolated across tests when
+/// necessary.
+fn copy_manifest(dir: &Path, home_dir: &str, script_dir: &str) {
+ let examples = env::current_exe().unwrap().parent().unwrap().to_path_buf()
+ .join("../../../examples/test");
+
+ let copy_file = |path: &str| {
+ let mut contents = read_file(&examples.join(path));
+ contents = contents.replace("~/", home_dir);
+ contents = contents.replace("scripts/", script_dir);
+ let dst = path.replace("scripts/", script_dir);
+ write_file(&dir.join(dst), &contents);
+ };
+
+ copy_file("manifest.yml");
+ fs::create_dir_all(&dir.join(script_dir)).unwrap();
+ copy_file("scripts/script.bat");
+ copy_file("scripts/script.sh");
+ copy_file("bashrc");
+ copy_file("gitconfig");
+ copy_file("vimrc");
+
}
/// Writes a string to a file, overwriting it if it already exists.
diff --git a/tests/local.rs b/tests/local.rs
@@ -0,0 +1,365 @@
+/// End to end tests that test specific installation behavior on the local file
+/// system
+
+mod common;
+
+use common::*;
+use std::fs::remove_file;
+
+#[test]
+#[cfg(target_family = "unix")]
+fn test_local_standard() {
+ let (dirs, mut cmd) = setup_e2e_local("test_local_standard");
+ cmd.args(["manifest.yml", "-t", "linux"]);
+
+ let expected = "\
+[1/3] Copy gitconfig to ~/.gitconfig
+[2/3] Link bashrc to ~/.bashrc
+[2/3] Link vimrc to ~/.vimrc
+[2/3] Run sh script.sh arg1 linux
+script.sh called with arg1 linux
+";
+ assert_eq!(&stderr_to_string(&mut cmd), "");
+ assert_eq!(&stdout_to_string(&mut cmd), expected);
+
+ // Assert files are correctly copied/linked/run
+ write_file(&dirs.local.join("bashrc"), "bash #2\n");
+ write_file(&dirs.local.join("gitconfig"), "git #2\n");
+ write_file(&dirs.local.join("vimrc"), "vim #2\n");
+ let bash_contents = read_file(&dirs.home.join(".bashrc"));
+ let git_contents = read_file(&dirs.home.join(".gitconfig"));
+ let vim1_contents = read_file(&dirs.home.join(".vimrc"));
+ let vim2_exists = dirs.home.join("_vimrc").exists();
+ let log_contents = read_file(&dirs.local.join("log.txt"));
+ assert_eq!(bash_contents, "bash #2\n");
+ assert_eq!(git_contents, "git #1\n");
+ assert_eq!(vim1_contents, "vim #2\n");
+ assert_eq!(vim2_exists, false);
+ assert_eq!(log_contents, "script.sh called with arg1 linux\n");
+}
+
+#[test]
+#[cfg(target_family = "windows")]
+fn test_local_standard() {
+ let (dirs, mut cmd) = setup_e2e_local("test_local_standard");
+ cmd.args(["manifest.yml", "-t", "windows"]);
+
+ let expected = "\
+[1/3] Copy gitconfig to .gitconfig
+[3/3] Link vimrc to _vimrc
+[3/3] Run script.bat arg1 windows
+script.bat called with arg1 windows\r
+";
+ assert_eq!(&stderr_to_string(&mut cmd), "");
+ assert_eq!(&stdout_to_string(&mut cmd), expected);
+
+ // Assert files are correctly copied/linked/run
+ write_file(&dirs.local.join("gitconfig"), "git #2\r\n");
+ write_file(&dirs.local.join("vimrc"), "vim #2\r\n");
+ let bash_exists = dirs.local.join(".bashrc").exists();
+ let git_contents = read_file(&dirs.local.join(".gitconfig"));
+ let vim1_exists = dirs.local.join(".vimrc").exists();
+ let vim2_contents = read_file(&dirs.local.join("_vimrc"));
+ let log_contents = read_file(&dirs.local.join("log.txt"));
+ assert_eq!(bash_exists, false);
+ assert_eq!(git_contents, "git #1\r\n");
+ assert_eq!(vim1_exists, false);
+ assert_eq!(vim2_contents, "vim #2\r\n");
+ assert_eq!(log_contents, "script.bat called with arg1 windows \r\n");
+}
+
+#[test]
+#[cfg(target_family = "unix")]
+fn test_local_run_alternate_tag_rules_1() {
+ let (dirs, mut cmd) = setup_e2e_local("test_local_run_alternate_tag_rules_1");
+ cmd.args(["manifest.yml", "-t", "linux", "^windows"]);
+
+ let expected = "\
+[2/3] Link bashrc to ~/.bashrc
+[2/3] Link vimrc to ~/.vimrc
+[2/3] Run sh script.sh arg1 linux ^windows
+script.sh called with arg1 linux ^windows
+";
+ assert_eq!(&stderr_to_string(&mut cmd), "");
+ assert_eq!(&stdout_to_string(&mut cmd), expected);
+
+ // Assert files are correctly copied/linked/run
+ write_file(&dirs.local.join("bashrc"), "bash #2\n");
+ write_file(&dirs.local.join("vimrc"), "vim #2\n");
+ let bash_contents = read_file(&dirs.home.join(".bashrc"));
+ let git_exists = dirs.home.join(".gitconfig").exists();
+ let vim1_contents = read_file(&dirs.home.join(".vimrc"));
+ let vim2_exists = dirs.home.join("_vimrc").exists();
+ let log_contents = read_file(&dirs.local.join("log.txt"));
+ assert_eq!(bash_contents, "bash #2\n");
+ assert_eq!(git_exists, false);
+ assert_eq!(vim1_contents, "vim #2\n");
+ assert_eq!(vim2_exists, false);
+ assert_eq!(log_contents, "script.sh called with arg1 linux ^windows\n");
+}
+
+#[test]
+#[cfg(target_family = "unix")]
+fn test_local_run_alternate_tag_rules_2() {
+ let (dirs, mut cmd) = setup_e2e_local("test_local_run_alternate_tag_rules_2");
+ cmd.args(["manifest.yml", "-t", "macos"]);
+
+ let expected = "\
+[1/3] Copy gitconfig to ~/.gitconfig
+[2/3] Link bashrc to ~/.bashrc
+[2/3] Link vimrc to ~/.vimrc
+[2/3] Run sh script.sh arg1 macos
+script.sh called with arg1 macos
+";
+ assert_eq!(&stderr_to_string(&mut cmd), "");
+ assert_eq!(&stdout_to_string(&mut cmd), expected);
+
+ // Assert files are correctly copied/linked/run
+ write_file(&dirs.local.join("bashrc"), "bash #2\n");
+ write_file(&dirs.local.join("gitconfig"), "git #2\n");
+ write_file(&dirs.local.join("vimrc"), "vim #2\n");
+ let bash_contents = read_file(&dirs.home.join(".bashrc"));
+ let git_contents = read_file(&dirs.home.join(".gitconfig"));
+ let vim1_contents = read_file(&dirs.home.join(".vimrc"));
+ let vim2_exists = dirs.home.join("_vimrc").exists();
+ let log_contents = read_file(&dirs.local.join("log.txt"));
+ assert_eq!(bash_contents, "bash #2\n");
+ assert_eq!(git_contents, "git #1\n");
+ assert_eq!(vim1_contents, "vim #2\n");
+ assert_eq!(vim2_exists, false);
+ assert_eq!(log_contents, "script.sh called with arg1 macos\n");
+}
+
+#[test]
+#[cfg(target_family = "unix")]
+fn test_local_dry_run() {
+ let (dirs, mut cmd) = setup_e2e_local("test_local_dry_run");
+ cmd.args(["manifest.yml", "--dry-run", "-t", "linux"]);
+
+ let expected = "\
+[1/3] Copy gitconfig to ~/.gitconfig (DRY RUN)
+[2/3] Link bashrc to ~/.bashrc (DRY RUN)
+[2/3] Link vimrc to ~/.vimrc (DRY RUN)
+[2/3] Run sh script.sh arg1 linux (DRY RUN)
+";
+ assert_eq!(&stderr_to_string(&mut cmd), "");
+ assert_eq!(&stdout_to_string(&mut cmd), expected);
+
+ // Assert files are correctly copied/linked/run
+ let bash_exists = dirs.home.join(".bashrc").exists();
+ let git_exists = dirs.home.join(".gitconfig").exists();
+ let vim1_exists = dirs.home.join(".vimrc").exists();
+ let vim2_exists = dirs.home.join("_vimrc").exists();
+ let log_exists = dirs.local.join("log.txt").exists();
+ assert_eq!(bash_exists, false);
+ assert_eq!(git_exists, false);
+ assert_eq!(vim1_exists, false);
+ assert_eq!(vim2_exists, false);
+ assert_eq!(log_exists, false);
+}
+
+#[test]
+#[cfg(target_family = "windows")]
+fn test_local_dry_run() {
+ let (dirs, mut cmd) = setup_e2e_local("test_local_dry_run");
+ cmd.args(["manifest.yml", "--dry-run", "-t", "windows"]);
+
+ let expected = "\
+[1/3] Copy gitconfig to .gitconfig (DRY RUN)
+[3/3] Link vimrc to _vimrc (DRY RUN)
+[3/3] Run script.bat arg1 windows (DRY RUN)
+";
+ assert_eq!(&stderr_to_string(&mut cmd), "");
+ assert_eq!(&stdout_to_string(&mut cmd), expected);
+
+ // Assert files are correctly copied/linked/run
+ let bash_exists = dirs.local.join(".bashrc").exists();
+ let git_exists = dirs.local.join(".gitconfig").exists();
+ let vim1_exists = dirs.local.join(".vimrc").exists();
+ let vim2_exists = dirs.local.join("_vimrc").exists();
+ let log_exists = dirs.local.join("log.txt").exists();
+ assert_eq!(bash_exists, false);
+ assert_eq!(git_exists, false);
+ assert_eq!(vim1_exists, false);
+ assert_eq!(vim2_exists, false);
+ assert_eq!(log_exists, false);
+}
+
+#[test]
+#[cfg(target_family = "unix")]
+fn test_local_copy() {
+ let (dirs, mut cmd) = setup_e2e_local("test_local_copy");
+ cmd.args(["manifest.yml", "--copy", "-t", "linux"]);
+
+ let expected = "\
+[1/3] Copy gitconfig to ~/.gitconfig
+[2/3] Copy bashrc to ~/.bashrc
+[2/3] Copy vimrc to ~/.vimrc
+[2/3] Run sh script.sh arg1 linux
+script.sh called with arg1 linux
+";
+ assert_eq!(&stderr_to_string(&mut cmd), "");
+ assert_eq!(&stdout_to_string(&mut cmd), expected);
+
+ // Assert files are correctly copied/linked/run
+ write_file(&dirs.local.join("bashrc"), "bash #2\n");
+ write_file(&dirs.local.join("gitconfig"), "git #2\n");
+ write_file(&dirs.local.join("vimrc"), "vim #2\n");
+ let bash_contents = read_file(&dirs.home.join(".bashrc"));
+ let git_contents = read_file(&dirs.home.join(".gitconfig"));
+ let vim1_contents = read_file(&dirs.home.join(".vimrc"));
+ let vim2_exists = dirs.home.join("_vimrc").exists();
+ let log_contents = read_file(&dirs.local.join("log.txt"));
+ assert_eq!(bash_contents, "bash #1\n");
+ assert_eq!(git_contents, "git #1\n");
+ assert_eq!(vim1_contents, "vim #1\n");
+ assert_eq!(vim2_exists, false);
+ assert_eq!(log_contents, "script.sh called with arg1 linux\n");
+}
+
+#[test]
+#[cfg(target_family = "windows")]
+fn test_local_copy() {
+ let (dirs, mut cmd) = setup_e2e_local("test_local_copy");
+ cmd.args(["manifest.yml", "--copy", "-t", "windows"]);
+
+ let expected = "\
+[1/3] Copy gitconfig to .gitconfig
+[3/3] Copy vimrc to _vimrc
+[3/3] Run script.bat arg1 windows
+script.bat called with arg1 windows\r
+";
+ assert_eq!(&stderr_to_string(&mut cmd), "");
+ assert_eq!(&stdout_to_string(&mut cmd), expected);
+
+ // Assert files are correctly copied/linked/run
+ write_file(&dirs.local.join("gitconfig"), "git #2\r\n");
+ write_file(&dirs.local.join("vimrc"), "vim #2\r\n");
+ let bash_exists = dirs.local.join(".bashrc").exists();
+ let git_contents = read_file(&dirs.local.join(".gitconfig"));
+ let vim1_exists = dirs.local.join(".vimrc").exists();
+ let vim2_contents = read_file(&dirs.local.join("_vimrc"));
+ let log_contents = read_file(&dirs.local.join("log.txt"));
+ assert_eq!(bash_exists, false);
+ assert_eq!(git_contents, "git #1\r\n");
+ assert_eq!(vim1_exists, false);
+ assert_eq!(vim2_contents, "vim #1\r\n");
+ assert_eq!(log_contents, "script.bat called with arg1 windows \r\n");
+}
+
+#[test]
+#[cfg(target_family = "unix")]
+fn test_local_run_failure() {
+ let (dirs, mut cmd) = setup_e2e_local("test_local_run_failure");
+ cmd.args(["manifest.yml", "-t", "linux"]);
+ write_file(&dirs.local.join("script.sh"), "exit 1");
+
+ let expected_stdout = "\
+[1/3] Copy gitconfig to ~/.gitconfig
+[2/3] Link bashrc to ~/.bashrc
+[2/3] Link vimrc to ~/.vimrc
+[2/3] Run sh script.sh arg1 linux
+";
+ let expected_stderr = " Error: Process exited with exit status: 1\n";
+ assert_eq!(&stderr_to_string(&mut cmd), expected_stderr);
+ assert_eq!(&stdout_to_string(&mut cmd), expected_stdout);
+
+ // Assert files are correctly copied/linked/run
+ write_file(&dirs.local.join("bashrc"), "bash #2\n");
+ write_file(&dirs.local.join("gitconfig"), "git #2\n");
+ write_file(&dirs.local.join("vimrc"), "vim #2\n");
+ let bash_contents = read_file(&dirs.home.join(".bashrc"));
+ let git_contents = read_file(&dirs.home.join(".gitconfig"));
+ let vim1_contents = read_file(&dirs.home.join(".vimrc"));
+ let vim2_exists = dirs.home.join("_vimrc").exists();
+ assert_eq!(bash_contents, "bash #2\n");
+ assert_eq!(git_contents, "git #1\n");
+ assert_eq!(vim1_contents, "vim #2\n");
+ assert_eq!(vim2_exists, false);
+}
+
+#[test]
+#[cfg(target_family = "windows")]
+fn test_local_run_failure() {
+ let (dirs, mut cmd) = setup_e2e_local("test_local_run_failure");
+ cmd.args(["manifest.yml", "-t", "windows"]);
+ write_file(&dirs.local.join("script.bat"), "@echo off\r\nexit 1");
+
+ let expected_stdout = "\
+[1/3] Copy gitconfig to .gitconfig
+[3/3] Link vimrc to _vimrc
+[3/3] Run script.bat arg1 windows
+";
+ let expected_stderr = " Error: Process exited with exit code: 1\n";
+ assert_eq!(&stderr_to_string(&mut cmd), expected_stderr);
+ assert_eq!(&stdout_to_string(&mut cmd), expected_stdout);
+
+ // Assert files are correctly copied/linked/run
+ write_file(&dirs.local.join("gitconfig"), "git #2\r\n");
+ write_file(&dirs.local.join("vimrc"), "vim #2\r\n");
+ let bash_exists = dirs.local.join(".bashrc").exists();
+ let git_contents = read_file(&dirs.local.join(".gitconfig"));
+ let vim1_exists = dirs.local.join(".vimrc").exists();
+ let vim2_contents = read_file(&dirs.local.join("_vimrc"));
+ assert_eq!(bash_exists, false);
+ assert_eq!(git_contents, "git #1\r\n");
+ assert_eq!(vim1_exists, false);
+ assert_eq!(vim2_contents, "vim #2\r\n");
+}
+
+#[test]
+#[cfg(target_family = "unix")]
+fn test_local_missing_file() {
+ let (dirs, mut cmd) = setup_e2e_local("test_local_missing_file");
+ cmd.args(["manifest.yml", "-t", "linux"]);
+ remove_file(&dirs.local.join("vimrc")).unwrap();
+
+ let expected_stdout = "\
+[1/3] Copy gitconfig to ~/.gitconfig
+[2/3] Link bashrc to ~/.bashrc
+[2/3] Link vimrc to ~/.vimrc
+[2/3] Run sh script.sh arg1 linux
+script.sh called with arg1 linux
+";
+ let expected_stderr = " Error: No such file or directory (os error 2)\n";
+ assert_eq!(&stderr_to_string(&mut cmd), expected_stderr);
+ assert_eq!(&stdout_to_string(&mut cmd), expected_stdout);
+
+ // Assert files are correctly copied/linked/run
+ write_file(&dirs.local.join("bashrc"), "bash #2\n");
+ write_file(&dirs.local.join("gitconfig"), "git #2\n");
+ let bash_contents = read_file(&dirs.home.join(".bashrc"));
+ let git_contents = read_file(&dirs.home.join(".gitconfig"));
+ let log_contents = read_file(&dirs.local.join("log.txt"));
+ assert_eq!(bash_contents, "bash #2\n");
+ assert_eq!(git_contents, "git #1\n");
+ assert_eq!(log_contents, "script.sh called with arg1 linux\n");
+}
+
+#[test]
+#[cfg(target_family = "windows")]
+fn test_local_missing_file() {
+ let (dirs, mut cmd) = setup_e2e_local("test_local_missing_file");
+ cmd.args(["manifest.yml", "-t", "windows"]);
+ remove_file(&dirs.local.join("vimrc")).unwrap();
+
+ let expected_stdout = "\
+[1/3] Copy gitconfig to .gitconfig
+[3/3] Link vimrc to _vimrc
+[3/3] Run script.bat arg1 windows
+script.bat called with arg1 windows\r
+";
+ let expected_stderr = " Error: The system cannot find the file specified. \
+ (os error 2)\n";
+ assert_eq!(&stderr_to_string(&mut cmd), expected_stderr);
+ assert_eq!(&stdout_to_string(&mut cmd), expected_stdout);
+
+ // Assert files are correctly copied/linked/run
+ write_file(&dirs.local.join("gitconfig"), "git #2\r\n");
+ let bash_exists = dirs.local.join(".bashrc").exists();
+ let git_contents = read_file(&dirs.local.join(".gitconfig"));
+ let log_contents = read_file(&dirs.local.join("log.txt"));
+ assert_eq!(bash_exists, false);
+ assert_eq!(git_contents, "git #1\r\n");
+ assert_eq!(log_contents, "script.bat called with arg1 windows \r\n");
+}
diff --git a/tests/server/Dockerfile b/tests/server/Dockerfile
@@ -0,0 +1,17 @@
+# An SSH server with authentication disabled for the user "test"
+
+FROM alpine
+
+RUN apk update && apk add openssh
+
+RUN ssh-keygen -A
+
+RUN echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config
+RUN echo 'PermitEmptyPasswords yes' >> /etc/ssh/sshd_config
+RUN echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config
+
+COPY entry.sh /entry.sh
+
+EXPOSE 22
+
+CMD ["/entry.sh"]
diff --git a/tests/server/compose.yml b/tests/server/compose.yml
@@ -0,0 +1,13 @@
+services:
+ coliru-ssh:
+ environment:
+ - PUID # loaded from .env, =1000 by default
+ build:
+ context: .
+ ports:
+ - 2222:22
+ volumes:
+ - type: bind
+ source: ../.temp/ssh
+ target: /home/test
+ restart: unless-stopped
diff --git a/tests/server/entry.sh b/tests/server/entry.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env sh
+
+PUID=${PUID:-1000}
+echo "Creating user test with PUID=$PUID ..."
+adduser -h /home/test -s /bin/sh -D -u "$PUID" test
+passwd -d test
+
+echo "Starting sshd ..."
+/usr/sbin/sshd -D
diff --git a/tests/ssh.rs b/tests/ssh.rs
@@ -0,0 +1,204 @@
+/// End to end tests that test specific installation behavior on a remote
+/// machine via SSH
+
+mod common;
+
+use common::*;
+use std::fs::remove_file;
+
+#[test]
+#[cfg(target_family = "unix")]
+fn test_ssh_standard() {
+ let (dirs, mut cmd) = setup_e2e_ssh("test_ssh_standard");
+ cmd.args(["manifest.yml", "-t", "linux"]);
+
+ let expected = format!("\
+[1/3] Copy gitconfig to {SSH_HOST}:~/test_ssh_standard/.gitconfig
+[2/3] Copy bashrc to {SSH_HOST}:~/test_ssh_standard/.bashrc
+[2/3] Copy vimrc to {SSH_HOST}:~/test_ssh_standard/.vimrc
+[2/3] Run sh test_ssh_standard/script.sh arg1 linux on {SSH_HOST}
+script.sh called with arg1 linux
+");
+ assert_eq!(&stderr_to_string(&mut cmd), "");
+ assert_eq!(stdout_to_string(&mut cmd), expected);
+
+ // Assert files are correctly copied/run
+ let bash_contents = read_file(&dirs.ssh.join(".bashrc"));
+ let git_contents = read_file(&dirs.ssh.join(".gitconfig"));
+ let vim1_contents = read_file(&dirs.ssh.join(".vimrc"));
+ let vim2_exists = dirs.ssh.join("_vimrc").exists();
+ let log_contents = read_file(&dirs.ssh_cwd.join("log.txt"));
+ assert_eq!(bash_contents, "bash #1\n");
+ assert_eq!(git_contents, "git #1\n");
+ assert_eq!(vim1_contents, "vim #1\n");
+ assert_eq!(vim2_exists, false);
+ assert_eq!(log_contents, "script.sh called with arg1 linux\n");
+}
+
+#[test]
+#[cfg(target_family = "unix")]
+fn test_ssh_run_alternate_tag_rules_1() {
+ let (dirs, mut cmd) = setup_e2e_ssh("test_ssh_run_alternate_tag_rules_1");
+ cmd.args(["manifest.yml", "-t", "linux", "^windows"]);
+
+ let expected = format!("\
+[2/3] Copy bashrc to {SSH_HOST}:~/test_ssh_run_alternate_tag_rules_1/.bashrc
+[2/3] Copy vimrc to {SSH_HOST}:~/test_ssh_run_alternate_tag_rules_1/.vimrc
+[2/3] Run sh test_ssh_run_alternate_tag_rules_1/script.sh arg1 linux ^windows on {SSH_HOST}
+script.sh called with arg1 linux ^windows
+");
+ assert_eq!(&stderr_to_string(&mut cmd), "");
+ assert_eq!(stdout_to_string(&mut cmd), expected);
+
+ // Assert files are correctly copied/run
+ let bash_contents = read_file(&dirs.ssh.join(".bashrc"));
+ let git_exists = dirs.ssh.join(".gitconfig").exists();
+ let vim1_contents = read_file(&dirs.ssh.join(".vimrc"));
+ let vim2_exists = dirs.ssh.join("_vimrc").exists();
+ let log_contents = read_file(&dirs.ssh_cwd.join("log.txt"));
+ assert_eq!(bash_contents, "bash #1\n");
+ assert_eq!(git_exists, false);
+ assert_eq!(vim1_contents, "vim #1\n");
+ assert_eq!(vim2_exists, false);
+ assert_eq!(log_contents, "script.sh called with arg1 linux ^windows\n");
+}
+
+#[test]
+#[cfg(target_family = "unix")]
+fn test_ssh_run_alternate_tag_rules_2() {
+ let (dirs, mut cmd) = setup_e2e_ssh("test_ssh_run_alternate_tag_rules_2");
+ cmd.args(["manifest.yml", "-t", "macos"]);
+
+ let expected = format!("\
+[1/3] Copy gitconfig to {SSH_HOST}:~/test_ssh_run_alternate_tag_rules_2/.gitconfig
+[2/3] Copy bashrc to {SSH_HOST}:~/test_ssh_run_alternate_tag_rules_2/.bashrc
+[2/3] Copy vimrc to {SSH_HOST}:~/test_ssh_run_alternate_tag_rules_2/.vimrc
+[2/3] Run sh test_ssh_run_alternate_tag_rules_2/script.sh arg1 macos on {SSH_HOST}
+script.sh called with arg1 macos
+");
+ assert_eq!(&stderr_to_string(&mut cmd), "");
+ assert_eq!(stdout_to_string(&mut cmd), expected);
+
+ // Assert files are correctly copied/run
+ let bash_contents = read_file(&dirs.ssh.join(".bashrc"));
+ let git_contents = read_file(&dirs.ssh.join(".gitconfig"));
+ let vim1_contents = read_file(&dirs.ssh.join(".vimrc"));
+ let vim2_exists = dirs.ssh.join("_vimrc").exists();
+ let log_contents = read_file(&dirs.ssh_cwd.join("log.txt"));
+ assert_eq!(bash_contents, "bash #1\n");
+ assert_eq!(git_contents, "git #1\n");
+ assert_eq!(vim1_contents, "vim #1\n");
+ assert_eq!(vim2_exists, false);
+ assert_eq!(log_contents, "script.sh called with arg1 macos\n");
+}
+
+#[test]
+fn test_ssh_dry_run() {
+ let (dirs, mut cmd) = setup_e2e_ssh("test_ssh_dry_run");
+ cmd.args(["manifest.yml", "--dry-run", "-t", "linux"]);
+
+ let expected = format!("\
+[1/3] Copy gitconfig to {SSH_HOST}:~/test_ssh_dry_run/.gitconfig (DRY RUN)
+[2/3] Copy bashrc to {SSH_HOST}:~/test_ssh_dry_run/.bashrc (DRY RUN)
+[2/3] Copy vimrc to {SSH_HOST}:~/test_ssh_dry_run/.vimrc (DRY RUN)
+[2/3] Run sh test_ssh_dry_run/script.sh arg1 linux on {SSH_HOST} (DRY RUN)
+");
+ assert_eq!(&stderr_to_string(&mut cmd), "");
+ assert_eq!(stdout_to_string(&mut cmd), expected);
+
+ // Assert files are correctly copied/run
+ let bash_exists = dirs.ssh.join(".bashrc").exists();
+ let git_exists = dirs.ssh.join(".gitconfig").exists();
+ let vim1_exists = dirs.ssh.join(".vimrc").exists();
+ let vim2_exists = dirs.ssh.join("_vimrc").exists();
+ let log_exists = dirs.ssh_cwd.join("log.txt").exists();
+ assert_eq!(bash_exists, false);
+ assert_eq!(git_exists, false);
+ assert_eq!(vim1_exists, false);
+ assert_eq!(vim2_exists, false);
+ assert_eq!(log_exists, false);
+}
+
+#[test]
+#[cfg(target_family = "unix")]
+fn test_ssh_copy() {
+ let (dirs, mut cmd) = setup_e2e_ssh("test_ssh_copy");
+ cmd.args(["manifest.yml", "--copy", "-t", "linux"]);
+
+ let expected = format!("\
+[1/3] Copy gitconfig to {SSH_HOST}:~/test_ssh_copy/.gitconfig
+[2/3] Copy bashrc to {SSH_HOST}:~/test_ssh_copy/.bashrc
+[2/3] Copy vimrc to {SSH_HOST}:~/test_ssh_copy/.vimrc
+[2/3] Run sh test_ssh_copy/script.sh arg1 linux on {SSH_HOST}
+script.sh called with arg1 linux
+");
+ assert_eq!(&stderr_to_string(&mut cmd), "");
+ assert_eq!(stdout_to_string(&mut cmd), expected);
+
+ // Assert files are correctly copied/run
+ let bash_contents = read_file(&dirs.ssh.join(".bashrc"));
+ let git_contents = read_file(&dirs.ssh.join(".gitconfig"));
+ let vim1_contents = read_file(&dirs.ssh.join(".vimrc"));
+ let vim2_exists = dirs.ssh.join("_vimrc").exists();
+ let log_contents = read_file(&dirs.ssh_cwd.join("log.txt"));
+ assert_eq!(bash_contents, "bash #1\n");
+ assert_eq!(git_contents, "git #1\n");
+ assert_eq!(vim1_contents, "vim #1\n");
+ assert_eq!(vim2_exists, false);
+ assert_eq!(log_contents, "script.sh called with arg1 linux\n");
+}
+
+#[test]
+#[cfg(target_family = "unix")]
+fn test_ssh_run_failure() {
+ let (dirs, mut cmd) = setup_e2e_ssh("test_ssh_run_failure");
+ cmd.args(["manifest.yml", "-t", "linux"]);
+ write_file(&dirs.local.join("test_ssh_run_failure/script.sh"), "exit 1");
+
+ let expected_stdout = format!("\
+[1/3] Copy gitconfig to {SSH_HOST}:~/test_ssh_run_failure/.gitconfig
+[2/3] Copy bashrc to {SSH_HOST}:~/test_ssh_run_failure/.bashrc
+[2/3] Copy vimrc to {SSH_HOST}:~/test_ssh_run_failure/.vimrc
+[2/3] Run sh test_ssh_run_failure/script.sh arg1 linux on {SSH_HOST}
+");
+ let expected_stderr = " Error: SSH exited with exit status: 1\n";
+ assert_eq!(&stderr_to_string(&mut cmd), expected_stderr);
+ assert_eq!(stdout_to_string(&mut cmd), expected_stdout);
+
+ // Assert files are correctly copied/run
+ let bash_contents = read_file(&dirs.ssh.join(".bashrc"));
+ let git_contents = read_file(&dirs.ssh.join(".gitconfig"));
+ let vim1_contents = read_file(&dirs.ssh.join(".vimrc"));
+ let vim2_exists = dirs.ssh.join("_vimrc").exists();
+ assert_eq!(bash_contents, "bash #1\n");
+ assert_eq!(git_contents, "git #1\n");
+ assert_eq!(vim1_contents, "vim #1\n");
+ assert_eq!(vim2_exists, false);
+}
+
+#[test]
+#[cfg(target_family = "unix")]
+fn test_ssh_missing_file() {
+ let (dirs, mut cmd) = setup_e2e_ssh("test_ssh_missing_file");
+ cmd.args(["manifest.yml", "-t", "linux"]);
+ remove_file(&dirs.local.join("vimrc")).unwrap();
+
+ let expected_stdout = format!("\
+[1/3] Copy gitconfig to {SSH_HOST}:~/test_ssh_missing_file/.gitconfig
+[2/3] Copy bashrc to {SSH_HOST}:~/test_ssh_missing_file/.bashrc
+[2/3] Copy vimrc to {SSH_HOST}:~/test_ssh_missing_file/.vimrc
+[2/3] Run sh test_ssh_missing_file/script.sh arg1 linux on {SSH_HOST}
+script.sh called with arg1 linux
+");
+ let expected_stderr = " Error: No such file or directory (os error 2)\n";
+ assert_eq!(&stderr_to_string(&mut cmd), expected_stderr);
+ assert_eq!(stdout_to_string(&mut cmd), expected_stdout);
+
+ // Assert files are correctly copied/run
+ let bash_contents = read_file(&dirs.ssh.join(".bashrc"));
+ let git_contents = read_file(&dirs.ssh.join(".gitconfig"));
+ let log_contents = read_file(&dirs.ssh_cwd.join("log.txt"));
+ assert_eq!(bash_contents, "bash #1\n");
+ assert_eq!(git_contents, "git #1\n");
+ assert_eq!(log_contents, "script.sh called with arg1 linux\n");
+}