blob: ceff26e9d91a0d7a59d1f9929d50b575ae8cff93 [file] [log] [blame]
// Copyright 2023 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::config::PduRef;
use crate::config::{self, Config};
use crate::driver::Driver;
use crate::env::Environment;
use crate::net::IpAddr;
use crate::yaml;
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Context, Result};
use itertools::Itertools;
use serde::Deserialize;
use serde_yaml::Value;
use thiserror::Error;
const TESTBED_NAME: &'static str = "antlion-runner";
const ENV_OUT_DIR: &'static str = "FUCHSIA_TEST_OUTDIR";
const ENV_TESTBED_CONFIG: &'static str = "FUCHSIA_TESTBED_CONFIG";
const TEST_SUMMARY_FILE: &'static str = "test_summary.yaml";
#[derive(Debug)]
/// Driver for running antlion on emulated and hardware testbeds hosted by
/// Fuchsia infrastructure.
pub(crate) struct InfraDriver {
output_dir: PathBuf,
config: Config,
}
#[derive(Error, Debug)]
pub(crate) enum InfraDriverError {
#[error("infra environment not detected, \"{0}\" environment variable not present")]
NotDetected(String),
#[error(transparent)]
Config(#[from] ConfigError),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
#[derive(Error, Debug)]
pub(crate) enum ConfigError {
#[error("ip {ip} in use by several devices")]
DuplicateIp { ip: IpAddr },
#[error("ip {ip} port {port} in use by several devices")]
DuplicatePort { ip: IpAddr, port: u8 },
}
impl InfraDriver {
/// Detect an InfraDriver. Returns None if the required environmental
/// variables are not found.
pub fn new<E: Environment>(
env: E,
ssh_binary: PathBuf,
ffx_binary: PathBuf,
) -> Result<Self, InfraDriverError> {
let config_path = match env.var(ENV_TESTBED_CONFIG) {
Ok(p) => PathBuf::from(p),
Err(std::env::VarError::NotPresent) => {
return Err(InfraDriverError::NotDetected(ENV_TESTBED_CONFIG.to_string()))
}
Err(e) => {
return Err(InfraDriverError::Other(anyhow!(
"Failed to read \"{ENV_TESTBED_CONFIG}\" {e}"
)))
}
};
let config = fs::read_to_string(&config_path)
.with_context(|| format!("Failed to read \"{}\"", config_path.display()))?;
let targets: Vec<InfraTarget> = serde_json::from_str(&config)
.with_context(|| format!("Failed to parse into InfraTarget: \"{config}\""))?;
if targets.len() == 0 {
return Err(InfraDriverError::Other(anyhow!(
"Expected at least one target declared in \"{}\"",
config_path.display()
)));
}
let output_path = match env.var(ENV_OUT_DIR) {
Ok(p) => p,
Err(std::env::VarError::NotPresent) => {
return Err(InfraDriverError::NotDetected(ENV_OUT_DIR.to_string()))
}
Err(e) => {
return Err(InfraDriverError::Other(anyhow!(
"Failed to read \"{ENV_OUT_DIR}\" {e}"
)))
}
};
let output_dir = PathBuf::from(output_path);
if !fs::metadata(&output_dir).context("Failed to stat the output directory")?.is_dir() {
return Err(InfraDriverError::Other(anyhow!(
"Expected a directory but found a file at \"{}\"",
output_dir.display()
)));
}
Ok(InfraDriver {
output_dir: output_dir.clone(),
config: InfraDriver::parse_targets(targets, ssh_binary, ffx_binary, output_dir)?,
})
}
fn parse_targets(
targets: Vec<InfraTarget>,
ssh_binary: PathBuf,
ffx_binary: PathBuf,
output_dir: PathBuf,
) -> Result<Config, InfraDriverError> {
let mut fuchsia_devices: Vec<config::Fuchsia> = vec![];
let mut access_points: Vec<config::AccessPoint> = vec![];
let mut attenuators: HashMap<IpAddr, config::Attenuator> = HashMap::new();
let mut pdus: HashMap<IpAddr, config::Pdu> = HashMap::new();
let mut iperf_servers: Vec<config::IPerfServer> = vec![];
let mut test_params: Option<Value> = None;
let mut used_ips: HashSet<IpAddr> = HashSet::new();
let mut used_ports: HashMap<IpAddr, HashSet<u8>> = HashMap::new();
let mut register_ip = |ip: IpAddr| -> Result<(), InfraDriverError> {
if !used_ips.insert(ip.clone()) {
return Err(ConfigError::DuplicateIp { ip }.into());
}
Ok(())
};
let mut register_port = |ip: IpAddr, port: u8| -> Result<(), InfraDriverError> {
match used_ports.get_mut(&ip) {
Some(ports) => {
if !ports.insert(port) {
return Err(ConfigError::DuplicatePort { ip, port }.into());
}
}
None => {
if used_ports.insert(ip, HashSet::from([port])).is_some() {
return Err(InfraDriverError::Other(anyhow!(
"Used ports set was unexpectedly modified by concurrent use",
)));
}
}
};
Ok(())
};
let mut register_pdu = |p: Option<PduRef>| -> Result<(), InfraDriverError> {
if let Some(PduRef { device, ip, port }) = p {
register_port(ip.clone(), port)?;
let new = config::Pdu { device: device.clone(), host: ip.clone() };
if let Some(old) = pdus.insert(ip.clone(), new.clone()) {
if old != new {
return Err(ConfigError::DuplicateIp { ip }.into());
}
}
}
Ok(())
};
let mut register_attenuator = |a: Option<AttenuatorRef>| -> Result<(), InfraDriverError> {
if let Some(a) = a {
let new = config::Attenuator {
model: "minicircuits".to_string(),
instrument_count: 4,
address: a.ip.clone(),
protocol: "http".to_string(),
port: 80,
};
if let Some(old) = attenuators.insert(a.ip.clone(), new.clone()) {
if old != new {
return Err(ConfigError::DuplicateIp { ip: a.ip }.into());
}
}
}
Ok(())
};
let mut merge_test_params = |p: Option<Value>| {
match (test_params.as_mut(), p) {
(None, Some(new)) => test_params = Some(new),
(Some(existing), Some(new)) => yaml::merge(existing, new),
(_, None) => {}
};
};
for target in targets {
match target {
InfraTarget::FuchsiaDevice { nodename, ipv4, ipv6, ssh_key, pdu, test_params } => {
let ip: IpAddr = if !ipv4.is_empty() {
ipv4.parse().context("Invalid IPv4 address")
} else if !ipv6.is_empty() {
ipv6.parse().context("Invalid IPv6 address")
} else {
Err(anyhow!("IP address not specified"))
}?;
fuchsia_devices.push(config::Fuchsia {
mdns_name: nodename.clone(),
ip: ip.clone(),
take_bug_report_on_fail: true,
ssh_binary_path: ssh_binary.clone(),
// TODO(http://b/244747218): Remove when ssh_config is refactored away
ssh_config: None,
ffx_binary_path: ffx_binary.clone(),
ssh_priv_key: ssh_key.clone(),
pdu_device: pdu.clone(),
hard_reboot_on_fail: true,
});
register_ip(ip)?;
register_pdu(pdu)?;
merge_test_params(test_params);
}
InfraTarget::AccessPoint { ip, attenuator, pdu, ssh_key } => {
access_points.push(config::AccessPoint {
wan_interface: "eth0".to_string(),
ssh_config: config::SshConfig {
ssh_binary_path: ssh_binary.clone(),
host: ip.clone(),
user: "root".to_string(),
identity_file: ssh_key.clone(),
},
pdu_device: pdu.clone(),
attenuators: attenuator.as_ref().map(|a| {
vec![config::AttenuatorRef {
address: a.ip.clone(),
ports_2g: vec![1, 2, 3],
ports_5g: vec![1, 2, 3],
}]
}),
});
register_ip(ip)?;
register_pdu(pdu)?;
register_attenuator(attenuator)?;
}
InfraTarget::IPerfServer { ip, user, test_interface, pdu, ssh_key } => {
iperf_servers.push(config::IPerfServer {
ssh_config: config::SshConfig {
ssh_binary_path: ssh_binary.clone(),
host: ip.clone(),
user: user.to_string(),
identity_file: ssh_key.clone(),
},
port: 5201,
test_interface: test_interface.clone(),
use_killall: true,
});
register_ip(ip.clone())?;
register_pdu(pdu)?;
}
};
}
Ok(Config {
testbeds: vec![config::Testbed {
name: TESTBED_NAME.to_string(),
controllers: config::Controllers {
fuchsia_devices: fuchsia_devices,
access_points: access_points,
attenuators: attenuators
.into_values()
.sorted_by_key(|a| a.address.clone())
.collect(),
pdus: pdus.into_values().sorted_by_key(|p| p.host.clone()).collect(),
iperf_servers: iperf_servers,
},
test_params,
}],
mobly_params: config::MoblyParams { log_path: output_dir },
})
}
}
impl Driver for InfraDriver {
fn output_path(&self) -> &Path {
self.output_dir.as_path()
}
fn config(&self) -> Config {
self.config.clone()
}
fn teardown(&self) -> Result<()> {
let results_path =
self.output_dir.join(TESTBED_NAME).join("latest").join(TEST_SUMMARY_FILE);
match fs::File::open(&results_path) {
Ok(mut results) => {
println!("\nTest results from {}\n", results_path.display());
println!("[=====MOBLY RESULTS=====]");
std::io::copy(&mut results, &mut std::io::stdout())
.context("Failed to copy results to stdout")?;
}
Err(e) => eprintln!("Failed to open \"{}\": {}", results_path.display(), e),
};
// Remove any symlinks from the output directory; this causes errors
// while uploading to CAS.
//
// TODO: Remove when the fix is released and supported on Swarming bots
// https://github.com/bazelbuild/remote-apis-sdks/pull/229.
remove_symlinks(self.output_dir.clone())?;
Ok(())
}
}
fn remove_symlinks<P: AsRef<Path>>(path: P) -> Result<()> {
let meta = fs::symlink_metadata(path.as_ref())?;
if meta.is_symlink() {
fs::remove_file(path)?;
} else if meta.is_dir() {
for entry in fs::read_dir(path)? {
remove_symlinks(entry?.path())?;
}
}
Ok(())
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type")]
/// Schema used to communicate target information from the test environment set
/// up by botanist.
///
/// See https://cs.opensource.google/fuchsia/fuchsia/+/main:tools/botanist/README.md
enum InfraTarget {
FuchsiaDevice {
nodename: String,
ipv4: String,
ipv6: String,
ssh_key: PathBuf,
pdu: Option<PduRef>,
test_params: Option<Value>,
},
AccessPoint {
ip: IpAddr,
ssh_key: PathBuf,
attenuator: Option<AttenuatorRef>,
pdu: Option<PduRef>,
},
IPerfServer {
ip: IpAddr,
ssh_key: PathBuf,
#[serde(default = "default_iperf_user")]
user: String,
test_interface: String,
pdu: Option<PduRef>,
},
}
fn default_iperf_user() -> String {
"pi".to_string()
}
#[derive(Clone, Debug, Deserialize)]
struct AttenuatorRef {
ip: IpAddr,
}
#[cfg(test)]
mod test {
use super::*;
use crate::run;
use crate::runner::Runner;
use crate::{env::Environment, runner::ExitStatus};
use std::ffi::OsStr;
use assert_matches::assert_matches;
use indoc::formatdoc;
use pretty_assertions::assert_eq;
use serde_json::json;
use tempfile::{NamedTempFile, TempDir};
const FUCHSIA_NAME: &'static str = "fuchsia-1234-5678-9abc";
const FUCHSIA_ADDR: &'static str = "fe80::1%2";
#[derive(Default)]
struct MockRunner {
out_dir: PathBuf,
config: std::cell::Cell<PathBuf>,
}
impl MockRunner {
fn new(out_dir: PathBuf) -> Self {
Self { out_dir, ..Default::default() }
}
}
impl Runner for MockRunner {
fn run(&self, config: PathBuf) -> Result<ExitStatus> {
self.config.set(config);
let antlion_out = self.out_dir.join(TESTBED_NAME).join("latest");
fs::create_dir_all(&antlion_out)
.context("Failed to create antlion output directory")?;
fs::write(antlion_out.join(TEST_SUMMARY_FILE), "")
.context("Failed to write test_summary.yaml")?;
Ok(ExitStatus::Ok)
}
}
struct MockEnvironment {
config: Option<PathBuf>,
out_dir: Option<PathBuf>,
}
impl Environment for MockEnvironment {
fn var<K: AsRef<OsStr>>(&self, key: K) -> Result<String, std::env::VarError> {
if key.as_ref() == ENV_TESTBED_CONFIG {
self.config
.clone()
.ok_or(std::env::VarError::NotPresent)
.map(|p| p.into_os_string().into_string().unwrap())
} else if key.as_ref() == ENV_OUT_DIR {
self.out_dir
.clone()
.ok_or(std::env::VarError::NotPresent)
.map(|p| p.into_os_string().into_string().unwrap())
} else {
Err(std::env::VarError::NotPresent)
}
}
}
#[test]
fn infra_not_detected() {
let ssh = NamedTempFile::new().unwrap();
let ffx = NamedTempFile::new().unwrap();
let env = MockEnvironment { config: None, out_dir: None };
let got = InfraDriver::new(env, ssh.path().to_path_buf(), ffx.path().to_path_buf());
assert_matches!(got, Err(InfraDriverError::NotDetected(_)));
}
#[test]
fn infra_not_detected_config() {
let ssh = NamedTempFile::new().unwrap();
let ffx = NamedTempFile::new().unwrap();
let out_dir = TempDir::new().unwrap();
let env = MockEnvironment { config: None, out_dir: Some(out_dir.path().to_path_buf()) };
let got = InfraDriver::new(env, ssh.path().to_path_buf(), ffx.path().to_path_buf());
assert_matches!(got, Err(InfraDriverError::NotDetected(v)) if v == ENV_TESTBED_CONFIG);
}
#[test]
fn infra_not_detected_out_dir() {
let ssh = NamedTempFile::new().unwrap();
let ssh_key = NamedTempFile::new().unwrap();
let ffx = NamedTempFile::new().unwrap();
let testbed_config = NamedTempFile::new().unwrap();
serde_json::to_writer_pretty(
testbed_config.as_file(),
&json!([{
"type": "FuchsiaDevice",
"nodename": FUCHSIA_NAME,
"ipv4": "",
"ipv6": FUCHSIA_ADDR,
"ssh_key": ssh_key.path(),
}]),
)
.unwrap();
let env =
MockEnvironment { config: Some(testbed_config.path().to_path_buf()), out_dir: None };
let got = InfraDriver::new(env, ssh.path().to_path_buf(), ffx.path().to_path_buf());
assert_matches!(got, Err(InfraDriverError::NotDetected(v)) if v == ENV_OUT_DIR);
}
#[test]
fn infra_invalid_config() {
let ssh = NamedTempFile::new().unwrap();
let ffx = NamedTempFile::new().unwrap();
let out_dir = TempDir::new().unwrap();
let testbed_config = NamedTempFile::new().unwrap();
serde_json::to_writer_pretty(testbed_config.as_file(), &json!({ "foo": "bar" })).unwrap();
let env = MockEnvironment {
config: Some(testbed_config.path().to_path_buf()),
out_dir: Some(out_dir.path().to_path_buf()),
};
let got = InfraDriver::new(env, ssh.path().to_path_buf(), ffx.path().to_path_buf());
assert_matches!(got, Err(_));
}
#[test]
fn infra() {
let ssh = NamedTempFile::new().unwrap();
let ssh_key = NamedTempFile::new().unwrap();
let ffx = NamedTempFile::new().unwrap();
let out_dir = TempDir::new().unwrap();
let testbed_config = NamedTempFile::new().unwrap();
serde_json::to_writer_pretty(
testbed_config.as_file(),
&json!([{
"type": "FuchsiaDevice",
"nodename": FUCHSIA_NAME,
"ipv4": "",
"ipv6": FUCHSIA_ADDR,
"ssh_key": ssh_key.path(),
}]),
)
.unwrap();
let runner = MockRunner::new(out_dir.path().to_path_buf());
let env = MockEnvironment {
config: Some(testbed_config.path().to_path_buf()),
out_dir: Some(out_dir.path().to_path_buf()),
};
let driver =
InfraDriver::new(env, ssh.path().to_path_buf(), ffx.path().to_path_buf()).unwrap();
run(runner, driver, None).unwrap();
let got = fs::read_to_string(out_dir.path().join("config.yaml")).unwrap();
let ssh_path = ssh.path().display().to_string();
let ssh_key_path = ssh_key.path().display().to_string();
let ffx_path = ffx.path().display().to_string();
let out_path = out_dir.path().display();
let want = formatdoc! {r#"
TestBeds:
- Name: {TESTBED_NAME}
Controllers:
FuchsiaDevice:
- mdns_name: {FUCHSIA_NAME}
ip: {FUCHSIA_ADDR}
take_bug_report_on_fail: true
ssh_binary_path: {ssh_path}
ffx_binary_path: {ffx_path}
ssh_priv_key: {ssh_key_path}
hard_reboot_on_fail: true
MoblyParams:
LogPath: {out_path}
"#};
assert_eq!(got, want);
}
#[test]
fn infra_with_test_params() {
let ssh = NamedTempFile::new().unwrap();
let ssh_key = NamedTempFile::new().unwrap();
let ffx = NamedTempFile::new().unwrap();
let out_dir = TempDir::new().unwrap();
let testbed_config = NamedTempFile::new().unwrap();
serde_json::to_writer_pretty(
testbed_config.as_file(),
&json!([{
"type": "FuchsiaDevice",
"nodename": FUCHSIA_NAME,
"ipv4": "",
"ipv6": FUCHSIA_ADDR,
"ssh_key": ssh_key.path(),
"test_params": {
"sl4f_sanity_test_params": {
"can_overwrite": false,
"from_original": true,
}
}
}]),
)
.unwrap();
let runner = MockRunner::new(out_dir.path().to_path_buf());
let env = MockEnvironment {
config: Some(testbed_config.path().to_path_buf()),
out_dir: Some(out_dir.path().to_path_buf()),
};
let driver =
InfraDriver::new(env, ssh.path().to_path_buf(), ffx.path().to_path_buf()).unwrap();
let params = "
sl4f_sanity_test_params:
merged_with: true
can_overwrite: true
";
let params = serde_yaml::from_str(params).unwrap();
run(runner, driver, Some(params)).unwrap();
let got = fs::read_to_string(out_dir.path().join("config.yaml")).unwrap();
let ssh_path = ssh.path().display().to_string();
let ssh_key_path = ssh_key.path().display().to_string();
let ffx_path = ffx.path().display().to_string();
let out_path = out_dir.path().display();
let want = formatdoc! {r#"
TestBeds:
- Name: {TESTBED_NAME}
Controllers:
FuchsiaDevice:
- mdns_name: {FUCHSIA_NAME}
ip: {FUCHSIA_ADDR}
take_bug_report_on_fail: true
ssh_binary_path: {ssh_path}
ffx_binary_path: {ffx_path}
ssh_priv_key: {ssh_key_path}
hard_reboot_on_fail: true
TestParams:
sl4f_sanity_test_params:
can_overwrite: true
from_original: true
merged_with: true
MoblyParams:
LogPath: {out_path}
"#};
assert_eq!(got, want);
}
#[test]
fn infra_with_auxiliary_devices() {
const FUCHSIA_PDU_IP: &'static str = "192.168.42.14";
const FUCHSIA_PDU_PORT: u8 = 1;
const AP_IP: &'static str = "192.168.42.11";
const AP_AND_IPERF_PDU_IP: &'static str = "192.168.42.13";
const AP_PDU_PORT: u8 = 1;
const ATTENUATOR_IP: &'static str = "192.168.42.15";
const IPERF_IP: &'static str = "192.168.42.12";
const IPERF_USER: &'static str = "alice";
const IPERF_PDU_PORT: u8 = 2;
let ssh = NamedTempFile::new().unwrap();
let ssh_key = NamedTempFile::new().unwrap();
let ffx = NamedTempFile::new().unwrap();
let out_dir = TempDir::new().unwrap();
let testbed_config = NamedTempFile::new().unwrap();
serde_json::to_writer_pretty(
testbed_config.as_file(),
&json!([{
"type": "FuchsiaDevice",
"nodename": FUCHSIA_NAME,
"ipv4": "",
"ipv6": FUCHSIA_ADDR,
"ssh_key": ssh_key.path(),
"pdu": {
"ip": FUCHSIA_PDU_IP,
"port": FUCHSIA_PDU_PORT,
},
}, {
"type": "AccessPoint",
"ip": AP_IP,
"ssh_key": ssh_key.path(),
"attenuator": {
"ip": ATTENUATOR_IP,
},
"pdu": {
"ip": AP_AND_IPERF_PDU_IP,
"port": AP_PDU_PORT,
"device": "fancy-pdu",
},
}, {
"type": "IPerfServer",
"ip": IPERF_IP,
"ssh_key": ssh_key.path(),
"user": IPERF_USER,
"test_interface": "eth0",
"pdu": {
"ip": AP_AND_IPERF_PDU_IP,
"port": IPERF_PDU_PORT,
"device": "fancy-pdu",
},
}]),
)
.unwrap();
let runner = MockRunner::new(out_dir.path().to_path_buf());
let env = MockEnvironment {
config: Some(testbed_config.path().to_path_buf()),
out_dir: Some(out_dir.path().to_path_buf()),
};
let driver =
InfraDriver::new(env, ssh.path().to_path_buf(), ffx.path().to_path_buf()).unwrap();
run(runner, driver, None).unwrap();
let got = std::fs::read_to_string(out_dir.path().join("config.yaml")).unwrap();
let ssh_path = ssh.path().display().to_string();
let ssh_key_path = ssh_key.path().display().to_string();
let ffx_path = ffx.path().display().to_string();
let out_path = out_dir.path().display();
let want = formatdoc! {r#"
TestBeds:
- Name: {TESTBED_NAME}
Controllers:
FuchsiaDevice:
- mdns_name: {FUCHSIA_NAME}
ip: {FUCHSIA_ADDR}
take_bug_report_on_fail: true
ssh_binary_path: {ssh_path}
ffx_binary_path: {ffx_path}
ssh_priv_key: {ssh_key_path}
PduDevice:
device: synaccess.np02b
host: {FUCHSIA_PDU_IP}
port: {FUCHSIA_PDU_PORT}
hard_reboot_on_fail: true
AccessPoint:
- wan_interface: eth0
ssh_config:
ssh_binary_path: {ssh_path}
host: {AP_IP}
user: root
identity_file: {ssh_key_path}
PduDevice:
device: fancy-pdu
host: {AP_AND_IPERF_PDU_IP}
port: {AP_PDU_PORT}
Attenuator:
- Address: {ATTENUATOR_IP}
attenuator_ports_wifi_2g:
- 1
- 2
- 3
attenuator_ports_wifi_5g:
- 1
- 2
- 3
Attenuator:
- Model: minicircuits
InstrumentCount: 4
Address: {ATTENUATOR_IP}
Protocol: http
Port: 80
PduDevice:
- device: fancy-pdu
host: {AP_AND_IPERF_PDU_IP}
- device: synaccess.np02b
host: {FUCHSIA_PDU_IP}
IPerfServer:
- ssh_config:
ssh_binary_path: {ssh_path}
host: {IPERF_IP}
user: {IPERF_USER}
identity_file: {ssh_key_path}
port: 5201
test_interface: eth0
use_killall: true
MoblyParams:
LogPath: {out_path}
"#};
assert_eq!(got, want);
}
#[test]
fn infra_duplicate_port_pdu() {
let pdu_ip: IpAddr = "192.168.42.13".parse().unwrap();
let pdu_port = 1;
let ssh = NamedTempFile::new().unwrap();
let ssh_key = NamedTempFile::new().unwrap();
let ffx = NamedTempFile::new().unwrap();
let out_dir = TempDir::new().unwrap();
let testbed_config = NamedTempFile::new().unwrap();
serde_json::to_writer_pretty(
testbed_config.as_file(),
&json!([{
"type": "FuchsiaDevice",
"nodename": "foo",
"ipv4": "",
"ipv6": "fe80::1%2",
"ssh_key": ssh_key.path(),
"pdu": {
"ip": pdu_ip,
"port": pdu_port,
},
}, {
"type": "AccessPoint",
"ip": "192.168.42.11",
"ssh_key": ssh_key.path(),
"pdu": {
"ip": pdu_ip,
"port": pdu_port,
},
}]),
)
.unwrap();
let env = MockEnvironment {
config: Some(testbed_config.path().to_path_buf()),
out_dir: Some(out_dir.path().to_path_buf()),
};
let got = InfraDriver::new(env, ssh.path().to_path_buf(), ffx.path().to_path_buf());
assert_matches!(got,
Err(InfraDriverError::Config(ConfigError::DuplicatePort { ip, port }))
if ip == pdu_ip && port == pdu_port
);
}
#[test]
fn infra_duplicate_ip_pdu() {
let duplicate_ip: IpAddr = "192.168.42.13".parse().unwrap();
let ssh = NamedTempFile::new().unwrap();
let ssh_key = NamedTempFile::new().unwrap();
let ffx = NamedTempFile::new().unwrap();
let out_dir = TempDir::new().unwrap();
let testbed_config = NamedTempFile::new().unwrap();
serde_json::to_writer_pretty(
testbed_config.as_file(),
&json!([{
"type": "FuchsiaDevice",
"nodename": "foo",
"ipv4": "",
"ipv6": "fe80::1%2",
"ssh_key": ssh_key.path(),
"pdu": {
"ip": duplicate_ip,
"port": 1,
"device": "A",
},
}, {
"type": "AccessPoint",
"ip": "192.168.42.11",
"ssh_key": ssh_key.path(),
"pdu": {
"ip": duplicate_ip,
"port": 2,
"device": "B",
},
}]),
)
.unwrap();
let env = MockEnvironment {
config: Some(testbed_config.path().to_path_buf()),
out_dir: Some(out_dir.path().to_path_buf()),
};
assert_matches!(
InfraDriver::new(env, ssh.path().to_path_buf(), ffx.path().to_path_buf()),
Err(InfraDriverError::Config(ConfigError::DuplicateIp { ip }))
if ip == duplicate_ip
);
}
#[test]
fn infra_duplicate_ip_devices() {
let duplicate_ip: IpAddr = "192.168.42.11".parse().unwrap();
let ssh = NamedTempFile::new().unwrap();
let ssh_key = NamedTempFile::new().unwrap();
let ffx = NamedTempFile::new().unwrap();
let out_dir = TempDir::new().unwrap();
let testbed_config = NamedTempFile::new().unwrap();
serde_json::to_writer_pretty(
testbed_config.as_file(),
&json!([{
"type": "FuchsiaDevice",
"nodename": "foo",
"ipv4": duplicate_ip,
"ipv6": "",
"ssh_key": ssh_key.path(),
}, {
"type": "AccessPoint",
"ip": duplicate_ip,
"ssh_key": ssh_key.path(),
}]),
)
.unwrap();
let env = MockEnvironment {
config: Some(testbed_config.path().to_path_buf()),
out_dir: Some(out_dir.path().to_path_buf()),
};
let got = InfraDriver::new(env, ssh.path().to_path_buf(), ffx.path().to_path_buf());
assert_matches!(got,
Err(InfraDriverError::Config(ConfigError::DuplicateIp { ip }))
if ip == duplicate_ip
);
}
#[test]
fn remove_symlinks_works() {
const SYMLINK_FILE: &'static str = "latest";
let out_dir = TempDir::new().unwrap();
let test_file = NamedTempFile::new_in(&out_dir).unwrap();
let symlink_path = out_dir.path().join(SYMLINK_FILE);
#[cfg(unix)]
std::os::unix::fs::symlink(&test_file, &symlink_path).unwrap();
#[cfg(windows)]
std::os::windows::fs::symlink_file(&test_file, &symlink_path).unwrap();
assert_matches!(remove_symlinks(out_dir.path()), Ok(()));
assert_matches!(fs::symlink_metadata(symlink_path), Err(e) if e.kind() == std::io::ErrorKind::NotFound);
assert_matches!(fs::symlink_metadata(test_file), Ok(meta) if meta.is_file());
}
}