summaryrefslogtreecommitdiffhomepage
path: root/test
diff options
context:
space:
mode:
authorMarkus Pettersson <markus.pettersson@mullvad.net>2024-08-07 11:00:15 +0200
committerMarkus Pettersson <markus.pettersson@mullvad.net>2024-08-12 15:25:00 +0200
commite593ca40447eda3045089eb92fb4264ff6773120 (patch)
treed826bfa9fc3568f6e5a8c529b28a8ec2c7499dcc /test
parent042f2f04d5b0a6dc172610cf81276b9bb28e9456 (diff)
downloadmullvadvpn-e593ca40447eda3045089eb92fb4264ff6773120.tar.xz
mullvadvpn-e593ca40447eda3045089eb92fb4264ff6773120.zip
Replace OpenVPN CA certificate using CLI flag
Diffstat (limited to 'test')
-rw-r--r--test/assets/openvpn.ca.crt (renamed from test/openvpn.ca.crt)0
-rwxr-xr-xtest/scripts/build-runner-image.sh1
-rw-r--r--test/scripts/ssh-setup.sh2
-rw-r--r--test/test-manager/src/config.rs12
-rw-r--r--test/test-manager/src/main.rs45
-rw-r--r--test/test-manager/src/tests/config.rs88
-rw-r--r--test/test-manager/src/tests/install.rs37
-rw-r--r--test/test-manager/src/tests/mod.rs12
-rw-r--r--test/test-manager/src/vm/mod.rs8
-rw-r--r--test/test-manager/src/vm/network/linux.rs2
-rw-r--r--test/test-manager/src/vm/network/macos.rs2
-rw-r--r--test/test-manager/src/vm/network/mod.rs10
-rw-r--r--test/test-manager/src/vm/provision.rs103
13 files changed, 214 insertions, 108 deletions
diff --git a/test/openvpn.ca.crt b/test/assets/openvpn.ca.crt
index 4e04d2cb1a..4e04d2cb1a 100644
--- a/test/openvpn.ca.crt
+++ b/test/assets/openvpn.ca.crt
diff --git a/test/scripts/build-runner-image.sh b/test/scripts/build-runner-image.sh
index 4aec7b0439..512dc93a4c 100755
--- a/test/scripts/build-runner-image.sh
+++ b/test/scripts/build-runner-image.sh
@@ -35,7 +35,6 @@ case $TARGET in
"${SCRIPT_DIR}/../target/$TARGET/release/test-runner.exe" \
"${SCRIPT_DIR}/../target/$TARGET/release/connection-checker.exe" \
"${PACKAGE_DIR}/"*.exe \
- "${SCRIPT_DIR}/../openvpn.ca.crt" \
"::"
mdir -i "${TEST_RUNNER_IMAGE_PATH}"
;;
diff --git a/test/scripts/ssh-setup.sh b/test/scripts/ssh-setup.sh
index 7eb8ab56d3..41131169e0 100644
--- a/test/scripts/ssh-setup.sh
+++ b/test/scripts/ssh-setup.sh
@@ -16,7 +16,7 @@ echo "Copying test-runner to $RUNNER_DIR"
mkdir -p "$RUNNER_DIR"
-for file in test-runner connection-checker $APP_PACKAGE $PREVIOUS_APP $UI_RUNNER openvpn.ca.crt; do
+for file in test-runner connection-checker $APP_PACKAGE $PREVIOUS_APP $UI_RUNNER; do
echo "Moving $file to $RUNNER_DIR"
cp -f "$SCRIPT_DIR/$file" "$RUNNER_DIR"
done
diff --git a/test/test-manager/src/config.rs b/test/test-manager/src/config.rs
index 585e6c75df..7b74c48dcf 100644
--- a/test/test-manager/src/config.rs
+++ b/test/test-manager/src/config.rs
@@ -8,6 +8,8 @@ use std::{
path::{Path, PathBuf},
};
+use crate::tests::config::DEFAULT_MULLVAD_HOST;
+
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Could not find config dir")]
@@ -67,6 +69,16 @@ impl Config {
pub fn get_vm(&self, name: &str) -> Option<&VmConfig> {
self.vms.get(name)
}
+
+ /// Get the Mullvad host to use.
+ ///
+ /// Defaults to [`DEFAULT_MULLVAD_HOST`] if the host was not provided in the [`ConfigFile`].
+ pub fn get_host(&self) -> String {
+ self.mullvad_host.clone().unwrap_or_else(|| {
+ log::debug!("No Mullvad host has been set explicitly. Falling back to default host");
+ DEFAULT_MULLVAD_HOST.to_owned()
+ })
+ }
}
pub struct ConfigFile {
diff --git a/test/test-manager/src/main.rs b/test/test-manager/src/main.rs
index daad04983b..b26fa02fe9 100644
--- a/test/test-manager/src/main.rs
+++ b/test/test-manager/src/main.rs
@@ -9,12 +9,12 @@ mod summary;
mod tests;
mod vm;
-use std::path::PathBuf;
+use std::{net::SocketAddr, path::PathBuf};
use anyhow::{Context, Result};
use clap::Parser;
-use std::net::SocketAddr;
-use tests::config::DEFAULT_MULLVAD_HOST;
+
+use crate::tests::config::OpenVPNCertificate;
/// Test manager for Mullvad VPN app
#[derive(Parser, Debug)]
@@ -110,6 +110,12 @@ enum Commands {
#[arg(long, value_name = "DIR")]
package_dir: Option<PathBuf>,
+ /// OpenVPN CA certificate to use with the app under test. The expected argument is a path
+ /// (absolut or relative) to the desired CA certificate. The default certificate is
+ /// `assets/openvpn.ca.crt`.
+ #[arg(long)]
+ openvpn_certificate: Option<PathBuf>,
+
/// Only run tests matching substrings
test_filters: Vec<String>,
@@ -228,6 +234,7 @@ async fn main() -> Result<()> {
app_package_to_upgrade_from,
gui_package,
package_dir,
+ openvpn_certificate,
test_filters,
verbose,
test_report,
@@ -240,10 +247,7 @@ async fn main() -> Result<()> {
(true, true) => unreachable!("invalid combination"),
};
- let mullvad_host = config
- .mullvad_host
- .clone()
- .unwrap_or(DEFAULT_MULLVAD_HOST.to_owned());
+ let mullvad_host = config.get_host();
log::debug!("Mullvad host: {mullvad_host}");
let vm_config = vm::get_vm_config(&config, &vm).context("Cannot get VM config")?;
@@ -270,6 +274,13 @@ async fn main() -> Result<()> {
)
.context("Could not find the specified app packages")?;
+ // Load a new OpenVPN CA certificate if the user provided a path.
+ let openvpn_certificate = openvpn_certificate
+ .map(OpenVPNCertificate::from_file)
+ .transpose()
+ .context("Could not find OpenVPN CA certificate")?
+ .unwrap_or_default();
+
let mut instance = vm::run(&config, &vm).await.context("Failed to start VM")?;
let artifacts_dir = vm::provision(&config, &vm, &*instance, &manifest)
.await
@@ -285,28 +296,26 @@ async fn main() -> Result<()> {
let skip_wait = vm_config.provisioner != config::Provisioner::Noop;
let result = run_tests::run(
- tests::config::TestConfig {
- account_number: account,
+ tests::config::TestConfig::new(
+ account,
artifacts_dir,
- app_package_filename: manifest
+ manifest
.app_package_path
.file_name()
.unwrap()
.to_string_lossy()
.into_owned(),
- app_package_to_upgrade_from_filename: manifest
+ manifest
.app_package_to_upgrade_from_path
.map(|path| path.file_name().unwrap().to_string_lossy().into_owned()),
- ui_e2e_tests_filename: manifest
+ manifest
.gui_package_path
.map(|path| path.file_name().unwrap().to_string_lossy().into_owned()),
mullvad_host,
- #[cfg(target_os = "macos")]
- host_bridge_name: crate::vm::network::macos::find_vm_bridge()?,
- #[cfg(not(target_os = "macos"))]
- host_bridge_name: crate::vm::network::linux::BRIDGE_NAME.to_owned(),
- os: test_rpc::meta::Os::from(vm_config.os_type),
- },
+ vm::network::bridge()?,
+ test_rpc::meta::Os::from(vm_config.os_type),
+ openvpn_certificate,
+ ),
&*instance,
&test_filters,
skip_wait,
diff --git a/test/test-manager/src/tests/config.rs b/test/test-manager/src/tests/config.rs
index 58ebc4fa01..ae2f434698 100644
--- a/test/test-manager/src/tests/config.rs
+++ b/test/test-manager/src/tests/config.rs
@@ -1,9 +1,21 @@
use once_cell::sync::OnceCell;
-use std::ops::Deref;
+use std::{ops::Deref, path::Path};
use test_rpc::meta::Os;
-// Default `mullvad_host`. This should match the production env.
+/// Default `mullvad_host`. This should match the production env.
pub const DEFAULT_MULLVAD_HOST: &str = "mullvad.net";
+/// Bundled OpenVPN CA certificate use with the installed Mullvad app.
+const OPENVPN_CA_CERTIFICATE: &[u8] = include_bytes!(concat!(
+ env!("CARGO_MANIFEST_DIR"),
+ "/../assets/",
+ "openvpn.ca.crt"
+));
+/// Script for bootstrapping the test-runner after the test-manager has successfully logged in.
+pub const BOOTSTRAP_SCRIPT: &[u8] = include_bytes!(concat!(
+ env!("CARGO_MANIFEST_DIR"),
+ "/../scripts/",
+ "ssh-setup.sh"
+));
/// Constants that are accessible from each test via `TEST_CONFIG`.
/// The constants must be initialized before running any tests using `TEST_CONFIG.init()`.
@@ -23,6 +35,78 @@ pub struct TestConfig {
pub host_bridge_name: String,
pub os: Os,
+ /// The OpenVPN CA certificate to use with the the installed Mullvad App.
+ pub openvpn_certificate: OpenVPNCertificate,
+}
+
+impl TestConfig {
+ #[allow(clippy::too_many_arguments)]
+ // TODO: This argument list is very long, we should strive to shorten it if possible.
+ pub const fn new(
+ account_number: String,
+ artifacts_dir: String,
+ app_package_filename: String,
+ app_package_to_upgrade_from_filename: Option<String>,
+ ui_e2e_tests_filename: Option<String>,
+ mullvad_host: String,
+ host_bridge_name: String,
+ os: Os,
+ openvpn_certificate: OpenVPNCertificate,
+ ) -> Self {
+ Self {
+ account_number,
+ artifacts_dir,
+ app_package_filename,
+ app_package_to_upgrade_from_filename,
+ ui_e2e_tests_filename,
+ mullvad_host,
+ host_bridge_name,
+ os,
+ openvpn_certificate,
+ }
+ }
+}
+
+/// The OpenVPN CA certificate to use with the installed Mullvad App.
+#[derive(Clone, Debug)]
+pub struct OpenVPNCertificate(Vec<u8>);
+
+impl OpenVPNCertificate {
+ pub fn from_file(path: impl AsRef<Path>) -> std::io::Result<Self> {
+ Ok(Self(std::fs::read(path)?))
+ }
+}
+
+impl Deref for OpenVPNCertificate {
+ type Target = [u8];
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl Default for OpenVPNCertificate {
+ fn default() -> Self {
+ Self(Vec::from(OPENVPN_CA_CERTIFICATE))
+ }
+}
+
+/// A script which should be run *in* the test runner before the test run begins.
+#[derive(Clone, Debug)]
+pub struct BootstrapScript(Vec<u8>);
+
+impl Deref for BootstrapScript {
+ type Target = [u8];
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl Default for BootstrapScript {
+ fn default() -> Self {
+ Self(Vec::from(BOOTSTRAP_SCRIPT))
+ }
}
#[derive(Debug, Clone)]
diff --git a/test/test-manager/src/tests/install.rs b/test/test-manager/src/tests/install.rs
index 0c9e2b82fd..b92f68b413 100644
--- a/test/test-manager/src/tests/install.rs
+++ b/test/test-manager/src/tests/install.rs
@@ -1,5 +1,5 @@
use anyhow::{bail, Context};
-use std::time::Duration;
+use std::{path::Path, time::Duration};
use mullvad_management_interface::MullvadProxyClient;
use mullvad_types::{constraints::Constraint, relay_constraints};
@@ -29,14 +29,14 @@ pub async fn test_install_previous_app(_: TestContext, rpc: ServiceClient) -> an
.context("Missing previous app version")?,
)?)
.await?;
+ log::debug!("Replacing the OpenVPN CA certificate");
+ replace_openvpn_certificate(&rpc).await?;
// verify that daemon is running
if rpc.mullvad_daemon_get_status().await? != ServiceStatus::Running {
bail!(Error::DaemonNotRunning);
}
- replace_openvpn_cert(&rpc).await?;
-
// Override env vars
rpc.set_daemon_environment(get_app_env().await?).await?;
@@ -270,7 +270,7 @@ pub async fn test_install_new_app(_: TestContext, rpc: ServiceClient) -> anyhow:
rpc.set_daemon_log_level(test_rpc::mullvad_daemon::Verbosity::Trace)
.await?;
- replace_openvpn_cert(&rpc).await?;
+ replace_openvpn_certificate(&rpc).await?;
// Override env vars
rpc.set_daemon_environment(get_app_env().await?).await?;
@@ -348,10 +348,9 @@ pub async fn test_installation_idempotency(
Ok(())
}
-async fn replace_openvpn_cert(rpc: &ServiceClient) -> Result<(), Error> {
- use std::path::Path;
-
- const SOURCE_CERT_FILENAME: &str = "openvpn.ca.crt";
+/// Replace the OpenVPN CA certificate which is currently used by the installed Mullvad App.
+/// This needs to be invoked after reach (re)installation to use the custom OpenVPN certificate.
+async fn replace_openvpn_certificate(rpc: &ServiceClient) -> Result<(), Error> {
const DEST_CERT_FILENAME: &str = "ca.crt";
let dest_dir = match TEST_CONFIG.os {
@@ -360,18 +359,12 @@ async fn replace_openvpn_cert(rpc: &ServiceClient) -> Result<(), Error> {
Os::Macos => "/Applications/Mullvad VPN.app/Contents/Resources",
};
- rpc.copy_file(
- Path::new(&TEST_CONFIG.artifacts_dir)
- .join(SOURCE_CERT_FILENAME)
- .as_os_str()
- .to_string_lossy()
- .into_owned(),
- Path::new(dest_dir)
- .join(DEST_CERT_FILENAME)
- .as_os_str()
- .to_string_lossy()
- .into_owned(),
- )
- .await
- .map_err(Error::Rpc)
+ let dest = Path::new(dest_dir)
+ .join(DEST_CERT_FILENAME)
+ .as_os_str()
+ .to_string_lossy()
+ .into_owned();
+ rpc.write_file(dest, TEST_CONFIG.openvpn_certificate.to_vec())
+ .await
+ .map_err(Error::Rpc)
}
diff --git a/test/test-manager/src/tests/mod.rs b/test/test-manager/src/tests/mod.rs
index 0a9a6913df..312e8ae2b3 100644
--- a/test/test-manager/src/tests/mod.rs
+++ b/test/test-manager/src/tests/mod.rs
@@ -14,18 +14,18 @@ mod tunnel;
mod tunnel_state;
mod ui;
+pub use test_metadata::TestMetadata;
+
+use anyhow::Context;
+use futures::future::BoxFuture;
+use std::time::Duration;
+
use crate::{
mullvad_daemon::{MullvadClientArgument, RpcClientProvider},
tests::helpers::get_app_env,
};
-use anyhow::Context;
-pub use test_metadata::TestMetadata;
use test_rpc::ServiceClient;
-use futures::future::BoxFuture;
-
-use std::time::Duration;
-
const WAIT_FOR_TUNNEL_STATE_TIMEOUT: Duration = Duration::from_secs(40);
#[derive(Clone)]
diff --git a/test/test-manager/src/vm/mod.rs b/test/test-manager/src/vm/mod.rs
index a5c794a58a..6cb18c9c5f 100644
--- a/test/test-manager/src/vm/mod.rs
+++ b/test/test-manager/src/vm/mod.rs
@@ -1,13 +1,14 @@
+use anyhow::{Context, Result};
+use std::net::IpAddr;
+
use crate::{
config::{Config, ConfigFile, VmConfig, VmType},
package,
};
-use anyhow::{Context, Result};
-use std::net::IpAddr;
mod logging;
pub mod network;
-mod provision;
+pub mod provision;
mod qemu;
mod ssh;
#[cfg(target_os = "macos")]
@@ -62,6 +63,7 @@ pub async fn run(config: &Config, name: &str) -> Result<Box<dyn VmInstance>> {
Ok(instance)
}
+/// Returns the directory in the test runner where the test-runner binary is installed.
pub async fn provision(
config: &Config,
name: &str,
diff --git a/test/test-manager/src/vm/network/linux.rs b/test/test-manager/src/vm/network/linux.rs
index 12095138f6..34e8438b92 100644
--- a/test/test-manager/src/vm/network/linux.rs
+++ b/test/test-manager/src/vm/network/linux.rs
@@ -21,7 +21,7 @@ pub const TEST_SUBNET_DHCP_FIRST: Ipv4Addr = Ipv4Addr::new(172, 29, 1, 2);
pub const TEST_SUBNET_DHCP_LAST: Ipv4Addr = Ipv4Addr::new(172, 29, 1, 128);
/// Bridge interface on the host
-pub const BRIDGE_NAME: &str = "br-mullvadtest";
+pub(crate) const BRIDGE_NAME: &str = "br-mullvadtest";
/// TAP interface used by the guest
pub const TAP_NAME: &str = "tap-mullvadtest";
diff --git a/test/test-manager/src/vm/network/macos.rs b/test/test-manager/src/vm/network/macos.rs
index 78653df41c..35467144ec 100644
--- a/test/test-manager/src/vm/network/macos.rs
+++ b/test/test-manager/src/vm/network/macos.rs
@@ -53,7 +53,7 @@ pub async fn setup_test_network() -> Result<()> {
/// A hack to find the Tart bridge interface using `NON_TUN_GATEWAY`.
/// It should be possible to retrieve this using the virtualization framework instead,
/// but that requires an entitlement.
-pub fn find_vm_bridge() -> Result<String> {
+pub(crate) fn find_vm_bridge() -> Result<String> {
for addr in nix::ifaddrs::getifaddrs().unwrap() {
if !addr.interface_name.starts_with("bridge") {
continue;
diff --git a/test/test-manager/src/vm/network/mod.rs b/test/test-manager/src/vm/network/mod.rs
index de055376b0..a06227027b 100644
--- a/test/test-manager/src/vm/network/mod.rs
+++ b/test/test-manager/src/vm/network/mod.rs
@@ -17,3 +17,13 @@ pub use platform::{
/// Port on NON_TUN_GATEWAY that hosts a SOCKS5 server
pub const SOCKS5_PORT: u16 = 54321;
+
+/// Get the name of the bridge interface between the test-manager and the test-runner.
+pub fn bridge() -> anyhow::Result<String> {
+ #[cfg(target_os = "macos")]
+ {
+ crate::vm::network::macos::find_vm_bridge()
+ }
+ #[cfg(not(target_os = "macos"))]
+ Ok(platform::BRIDGE_NAME.to_owned())
+}
diff --git a/test/test-manager/src/vm/provision.rs b/test/test-manager/src/vm/provision.rs
index 0b7b88e766..067a4dd900 100644
--- a/test/test-manager/src/vm/provision.rs
+++ b/test/test-manager/src/vm/provision.rs
@@ -1,16 +1,17 @@
use crate::{
config::{OsType, Provisioner, VmConfig},
package,
+ tests::config::BOOTSTRAP_SCRIPT,
};
use anyhow::{bail, Context, Result};
use ssh2::Session;
use std::{
- fs::File,
io::{self, Read},
net::{IpAddr, SocketAddr, TcpStream},
- path::Path,
+ path::{Path, PathBuf},
};
+/// Returns the directory in the test runner where the test-runner binary is installed.
pub async fn provision(
config: &VmConfig,
instance: &dyn super::VmInstance,
@@ -21,7 +22,7 @@ pub async fn provision(
log::debug!("SSH provisioning");
let (user, password) = config.get_ssh_options().context("missing SSH config")?;
- ssh(
+ provision_ssh(
instance,
config.os_type,
&config.get_runner_dir(),
@@ -42,7 +43,8 @@ pub async fn provision(
}
}
-async fn ssh(
+/// Returns the directory in the test runner where the test-runner binary is installed.
+async fn provision_ssh(
instance: &dyn super::VmInstance,
os_type: OsType,
local_runner_dir: &Path,
@@ -89,8 +91,6 @@ fn blocking_ssh(
) -> Result<()> {
// Directory that receives the payload. Any directory that the SSH user has access to.
const REMOTE_TEMP_DIR: &str = "/tmp/";
- const SCRIPT_PAYLOAD: &[u8] = include_bytes!("../../../scripts/ssh-setup.sh");
- const OPENVPN_CERT: &[u8] = include_bytes!("../../../openvpn.ca.crt");
let temp_dir = Path::new(REMOTE_TEMP_DIR);
@@ -106,55 +106,36 @@ fn blocking_ssh(
// Transfer a test runner
let source = local_runner_dir.join("test-runner");
- ssh_send_file_path(&session, &source, temp_dir)
- .context("Failed to send test runner to remote")?;
+ ssh_send_file(&session, &source, temp_dir).context("Failed to send test runner to remote")?;
// Transfer connection-checker
let source = local_runner_dir.join("connection-checker");
- ssh_send_file_path(&session, &source, temp_dir)
+ ssh_send_file(&session, &source, temp_dir)
.context("Failed to send connection-checker to remote")?;
// Transfer app packages
- ssh_send_file_path(&session, &local_app_manifest.app_package_path, temp_dir)
+ ssh_send_file(&session, &local_app_manifest.app_package_path, temp_dir)
.context("Failed to send current app package to remote")?;
if let Some(app_package_to_upgrade_from_path) =
&local_app_manifest.app_package_to_upgrade_from_path
{
- ssh_send_file_path(&session, app_package_to_upgrade_from_path, temp_dir)
+ ssh_send_file(&session, app_package_to_upgrade_from_path, temp_dir)
.context("Failed to send previous app package to remote")?;
} else {
log::warn!("No previous app to send to remote")
}
if let Some(gui_package_path) = &local_app_manifest.gui_package_path {
- ssh_send_file_path(&session, gui_package_path, temp_dir)
+ ssh_send_file(&session, gui_package_path, temp_dir)
.context("Failed to send gui_package_path to remote")?;
} else {
log::warn!("No UI e2e test to send to remote")
}
- // Transfer openvpn cert
- let dest: std::path::PathBuf = temp_dir.join("openvpn.ca.crt");
- log::debug!("Copying remote openvpn.ca.crt -> {}", dest.display());
- #[allow(const_item_mutation)]
- ssh_send_file(
- &session,
- &mut OPENVPN_CERT,
- u64::try_from(OPENVPN_CERT.len()).expect("cert too long"),
- &dest,
- )
- .context("failed to send openvpn crt to remote")?;
-
// Transfer setup script
- let dest = temp_dir.join("ssh-setup.sh");
- log::debug!("Copying remote setup script -> {}", dest.display());
- #[allow(const_item_mutation)]
- ssh_send_file(
- &session,
- &mut SCRIPT_PAYLOAD,
- u64::try_from(SCRIPT_PAYLOAD.len()).expect("script too long"),
- &dest,
- )
- .context("failed to send bootstrap script to remote")?;
+ // TODO: Move this name to a constant somewhere?
+ let bootstrap_script_dest = temp_dir.join("ssh-setup.sh");
+ ssh_write(&session, &bootstrap_script_dest, BOOTSTRAP_SCRIPT)
+ .context("failed to send bootstrap script to remote")?;
// Run setup script
let app_package_path = local_app_manifest
@@ -171,9 +152,10 @@ fn blocking_ssh(
.map(|path| path.file_name().unwrap().to_string_lossy().into_owned())
.unwrap_or_default();
+ // Run the setup script in the test runner
let cmd = format!(
- "sudo {} {remote_dir} \"{app_package_path}\" \"{app_package_to_upgrade_from_path}\" \"{gui_package_path}\"",
- dest.display()
+ r#"sudo {} {remote_dir} "{app_package_path}" "{app_package_to_upgrade_from_path}" "{gui_package_path}""#,
+ bootstrap_script_dest.display(),
);
log::debug!("Running setup script on remote, cmd: {cmd}");
ssh_exec(&session, &cmd)
@@ -181,36 +163,51 @@ fn blocking_ssh(
.context("Failed to run setup script")
}
-fn ssh_send_file_path(session: &Session, source: &Path, dest_dir: &Path) -> Result<()> {
- let dest = dest_dir.join(source.file_name().context("Missing source file name")?);
+/// Copy a `source` file to `dest_dir` in the test runner.
+///
+/// Returns the aboslute path in the test runner where the file is stored.
+fn ssh_send_file<P: AsRef<Path> + Copy>(
+ session: &Session,
+ source: P,
+ dest_dir: &Path,
+) -> Result<PathBuf> {
+ let dest = dest_dir.join(
+ source
+ .as_ref()
+ .file_name()
+ .context("Missing source file name")?,
+ );
log::debug!(
"Copying file to remote: {} -> {}",
- source.display(),
+ source.as_ref().display(),
dest.display(),
);
- let mut file =
- File::open(source).with_context(|| format!("Failed to open file at {source:?}"))?;
- let file_len = file
- .metadata()
- .with_context(|| format!("Failed to get file size of {source:?}"))?
- .len();
- ssh_send_file(session, &mut file, file_len, &dest)
+ let source = std::fs::read(source)
+ .with_context(|| format!("Failed to open file at {}", source.as_ref().display()))?;
+
+ ssh_write(session, &dest, &source[..])?;
+
+ Ok(dest)
}
-fn ssh_send_file<R: Read>(
- session: &Session,
- source: &mut R,
- source_len: u64,
- dest: &Path,
-) -> Result<()> {
- let mut remote_file = session.scp_send(dest, 0o744, source_len, None)?;
+/// Analogues to [`std::fs::write`], but over ssh!
+fn ssh_write<P: AsRef<Path>, C: AsRef<[u8]>>(session: &Session, dest: P, source: C) -> Result<()> {
+ let bytes = source.as_ref();
+
+ let source = &mut &bytes[..];
+ let source_len = u64::try_from(bytes.len()).context("File too large, did not fit in a u64")?;
+
+ let mut remote_file = session.scp_send(dest.as_ref(), 0o744, source_len, None)?;
+
io::copy(source, &mut remote_file).context("failed to write file")?;
+
remote_file.send_eof()?;
remote_file.wait_eof()?;
remote_file.close()?;
remote_file.wait_close()?;
+
Ok(())
}