#![allow(
dead_code,
unreachable_pub,
reason = "Each test binary compiles this module as its own crate root and uses a subset of its helpers"
)]
use moonmchn::config::EgressEndpoint;
use moonmchn::daemon::api::Status;
use moonmchn::daemon::client::Client;
use moonmchn::daemon::{Mode, serve_in};
use moonmchn::image::{Digest, Image};
use moonmchn::machine::{self, Decoded, Engine, Flow, Machine, Spec, State};
use moonmchn::{Error, Result};
use std::fs::Permissions;
use std::net::{IpAddr, SocketAddr};
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use tempfile::TempDir;
static EXECS: Mutex<Vec<(String, Vec<String>)>> = Mutex::new(Vec::new());
static BINDINGS: Mutex<Vec<(String, EgressEndpoint)>> = Mutex::new(Vec::new());
pub struct FakeEngine;
impl Engine for FakeEngine {
type Machine = FakeMachine;
type Gateway = FakeGateway;
fn image_digest(image: &Image) -> Result<Digest> {
match image.as_str() {
"ghcr.io/org/unresolvable:latest" => Err(Error::ImageResolve {
image: image.to_string(),
message: "the fake registry has no such image".to_string(),
}),
_ => Digest::parse(&format!("sha256:{}", "f".repeat(64))),
}
}
}
pub struct FakeGateway;
impl machine::Gateway for FakeGateway {
type FlowError = &'static str;
const ALLOW: u8 = b'y';
const DENY: u8 = b'n';
fn decode_flow(bytes: &[u8]) -> Result<Decoded, &'static str> {
let Some(head) = bytes.get(..39) else {
return Ok(Decoded::Partial(39));
};
let length = usize::from(head[38]);
if length > 128 {
return Err("more than 128 opening bytes");
}
let Some(opening) = bytes.get(39..39 + length) else {
return Ok(Decoded::Partial(39 + length));
};
Ok(Decoded::Complete(Flow {
token: head[..32].try_into().unwrap(),
destination: SocketAddr::from((
[head[32], head[33], head[34], head[35]],
u16::from_be_bytes([head[36], head[37]]),
)),
opening: opening.to_vec(),
}))
}
fn is_guest_link(ip: IpAddr) -> bool {
ip == IpAddr::from([198, 51, 100, 1])
}
fn is_host_address(_: IpAddr) -> bool {
false
}
}
pub fn assert_announced(name: &str, reason: &str) {
let commands: Vec<Vec<String>> = EXECS
.lock()
.unwrap()
.iter()
.filter(|(machine, _)| machine == name)
.map(|(_, command)| command.clone())
.collect();
assert_eq!(commands.len(), 1, "{commands:?}");
let message = commands[0].last().unwrap();
assert!(message.contains(reason), "{message:?}");
}
pub fn bindings(name: &str) -> Vec<EgressEndpoint> {
BINDINGS
.lock()
.unwrap()
.iter()
.filter(|(machine, _)| machine == name)
.map(|(_, binding)| binding.clone())
.collect()
}
pub struct FakeMachine {
name: String,
}
impl Machine for FakeMachine {
fn start(name: &str, _spec: Spec, egress: &EgressEndpoint) -> Result<Self> {
BINDINGS
.lock()
.unwrap()
.push((name.to_string(), egress.clone()));
match name {
"hung" => hang(),
"boom" => Err(engine("boom")),
"wreck" => Err(Error::MachineLeftover {
name: name.to_string(),
boot: "boom".to_string(),
cleanup: "stuck".to_string(),
}),
"slow" => {
thread::sleep(Duration::from_millis(200));
Self::attach(name)
}
_ => Self::attach(name),
}
}
fn attach(name: &str) -> Result<Self> {
Ok(Self {
name: name.to_string(),
})
}
fn status(&self) -> Result<machine::Status> {
let state = match self.name.as_str() {
"hung" => hang(),
"flaky" => return Err(engine("flaky")),
"ghost" => State::Unknown,
"wreck" | "dead" => State::Failed,
"frozen" => State::Paused,
_ => State::Running,
};
let pid = (state == State::Running).then_some(1);
Ok(machine::Status {
name: self.name.clone(),
state,
pid,
})
}
fn resume(&self) -> Result<()> {
Ok(())
}
fn exec(&self, command: &[String], _timeout: Duration) -> Result<i32> {
if self.name.as_str() == "mute" {
Err(engine("mute"))
} else {
EXECS
.lock()
.unwrap()
.push((self.name.clone(), command.to_vec()));
Ok(0)
}
}
fn stop(self) -> Result<()> {
match self.name.as_str() {
"hung" => hang(),
"stubborn" => Err(engine("stubborn")),
"ghost" => Err(Error::MachineNotFound { name: self.name }),
_ => Ok(()),
}
}
}
fn hang() -> ! {
loop {
thread::sleep(Duration::from_secs(3600));
}
}
fn engine(message: &str) -> Error {
Error::Engine {
message: message.to_string(),
}
}
pub const TEARDOWN_TIMEOUT: Duration = Duration::from_secs(60);
pub fn state_dir() -> TempDir {
tempfile::Builder::new()
.permissions(Permissions::from_mode(0o700))
.tempdir()
.unwrap()
}
pub fn start_daemon(dir: PathBuf) -> JoinHandle<(Result<()>, Vec<u8>)> {
start_daemon_with(dir, TEARDOWN_TIMEOUT)
}
pub fn start_daemon_with(
dir: PathBuf,
teardown_timeout: Duration,
) -> JoinHandle<(Result<()>, Vec<u8>)> {
thread::spawn(move || {
let mut out = Vec::new();
(
serve_in::<FakeEngine>(&dir, Mode::Foreground, &mut out, teardown_timeout),
out,
)
})
}
pub type DaemonThread = JoinHandle<(Result<()>, Vec<u8>)>;
pub struct Daemon {
dir: TempDir,
thread: Option<DaemonThread>,
}
impl Daemon {
pub fn path(&self) -> &Path {
self.dir.path()
}
}
impl Drop for Daemon {
fn drop(&mut self) {
drop(Client::new(self.path().to_path_buf()).shutdown(true));
if let Some(thread) = self.thread.take() {
drop(thread.join());
}
}
}
pub fn daemon() -> (Daemon, Client) {
daemon_in(state_dir())
}
pub fn daemon_in(dir: TempDir) -> (Daemon, Client) {
let (dir, client, thread) = launch(dir, TEARDOWN_TIMEOUT);
(
Daemon {
dir,
thread: Some(thread),
},
client,
)
}
pub fn daemon_with_thread() -> (TempDir, Client, DaemonThread) {
daemon_with_teardown(TEARDOWN_TIMEOUT)
}
pub fn daemon_with_teardown(timeout: Duration) -> (TempDir, Client, DaemonThread) {
launch(state_dir(), timeout)
}
fn launch(dir: TempDir, timeout: Duration) -> (TempDir, Client, DaemonThread) {
let thread = start_daemon_with(dir.path().to_path_buf(), timeout);
let client = Client::new(dir.path().to_path_buf());
wait_for_status(&client).unwrap();
(dir, client, thread)
}
pub fn wait_for_status(client: &Client) -> moonmchn::Result<Status> {
let deadline = Instant::now() + Duration::from_secs(5);
loop {
match client.status() {
Ok(status) => return Ok(status),
Err(Error::DaemonNotRunning) => {
if Instant::now() > deadline {
return Err(Error::DaemonNotRunning);
}
thread::sleep(Duration::from_millis(10));
}
Err(e) => return Err(e),
}
}
}
pub fn marker(dir: &Path, name: &str) -> PathBuf {
dir.join("machines").join(name)
}
pub fn token_in(dir: &Path, name: &str) -> String {
let marker: toml::Table = std::fs::read_to_string(marker(dir, name))
.unwrap()
.parse()
.unwrap();
marker["egress"]["token"].as_str().unwrap().to_string()
}
pub fn egress_port(dir: &Path) -> u16 {
std::fs::read_to_string(dir.join("egress").join("port"))
.unwrap()
.trim()
.parse()
.unwrap()
}