use anyhow::Result; use assert_cmd::{cargo::cargo_bin, Command}; use get_port::{tcp::TcpPort, Ops}; use rexpect::session::{spawn_command, PtySession}; use std::{io, path, sync::Once}; use tempfile::TempDir; use thiserror::Error; static BUILD_DOCKER: Once = Once::new(); static mut BUILD_DOCKER_RESULT: Result<(), DockerBuildError> = Ok(()); struct TestContext { workdir: TempDir, ssh_port: u16, docker_process: PtySession, } impl Drop for TestContext { fn drop(&mut self) { self.docker_process.send_line("exit").unwrap(); } } #[derive(Error, Debug, Clone)] pub enum DockerBuildError { #[error(transparent)] StripPrefixError(#[from] path::StripPrefixError), #[error("IO Error: `{0}`")] IoError(String), #[error("Failed to build dockerfile")] CliError, } impl From for DockerBuildError { fn from(e: io::Error) -> Self { DockerBuildError::IoError(e.to_string()) } } fn build_docker_image() -> Result<(), DockerBuildError> { let build_docker = || { let mut command = std::process::Command::new("docker"); let absolute_shell_path = cargo_bin(env!("CARGO_PKG_NAME")); let relative_shell_path = absolute_shell_path.strip_prefix(std::fs::canonicalize(".")?)?; command.args([ "build", "--quiet", "-t", "shackle-server", "--build-arg", &format!("SHELL={}", relative_shell_path.display()), "./", ]); let status = command.status()?; if status.success() { Ok(()) } else { Err(DockerBuildError::CliError) } }; // Based on this example: https://doc.rust-lang.org/std/sync/struct.Once.html#examples-1 // Could be replaced by https://doc.rust-lang.org/std/cell/struct.LazyCell.html once that is in stable rust unsafe { BUILD_DOCKER.call_once(|| { BUILD_DOCKER_RESULT = build_docker(); }); BUILD_DOCKER_RESULT.clone() } } fn spawn_ssh_server() -> Result { build_docker_image()?; let workdir = tempfile::tempdir()?; let ssh_port = TcpPort::any("127.0.0.1").unwrap(); let mut command = std::process::Command::new("docker"); command.args([ "run", "-it", "-p", &format!("{}:22", ssh_port), "shackle-server", ]); command.current_dir(&workdir); let mut docker_process = spawn_command(command, Some(3000))?; docker_process.exp_string("Ready")?; Ok(TestContext { workdir, ssh_port, docker_process, }) } fn connect_to_ssh_server_interactively(c: &TestContext) -> Result { let mut command = std::process::Command::new("ssh"); command.args([ "-p", &c.ssh_port.to_string(), "shukkie@localhost", "-o", "StrictHostKeyChecking=false", ]); command.current_dir(&c.workdir); let mut p = spawn_command(command, Some(3000))?; expect_prompt(&mut p)?; Ok(p) } fn expect_prompt(p: &mut PtySession) -> Result<()> { p.exp_string("> ")?; Ok(()) } fn make_new_repo(c: &TestContext, repo_name: &str) -> Result<()> { let mut p = connect_to_ssh_server_interactively(&c)?; p.send_line(&format!("git-init {}", repo_name))?; expect_prompt(&mut p)?; p.send_line("exit")?; p.exp_eof()?; Ok(()) } #[test] fn shows_a_prompt() -> Result<()> { let c = spawn_ssh_server()?; connect_to_ssh_server_interactively(&c)?; Ok(()) } #[test] fn git_clone_works_with_an_empty_repo() -> Result<()> { let c = spawn_ssh_server()?; let repo_name = "my-new-repo"; make_new_repo(&c, repo_name)?; Command::new("git") .arg("clone") .arg(&format!("shukkie@localhost:2022:{}.git", repo_name)) .current_dir(&c.workdir) .assert() .success(); Ok(()) }