fabro/lib/components/fabro-petri/tests/support/mod.rs

155 lines
4.9 KiB
Rust

//! What the adapter tests share: a bundle admitted
//! through `check`, a run request over the engine assembly, and the run's
//! records read back from its store.
#![allow(
dead_code,
reason = "each test file uses the part of the support it needs"
)]
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant};
use fabro_petri::admission::AdmittedGraphs;
use fabro_petri::check::{self, Bundle, CheckRequest, Launch};
use fabro_petri::controls::RunControls;
use fabro_petri::engine::{Execution, RunRequest};
use fabro_petri::interview::{Approval, FabroInterviewer, QuestionNotice, QuestionSink};
use fabro_petri::runtime::RuntimeSpec;
use fabro_types::SandboxProviderKind;
use fabro_types::settings::run::{EnvironmentNetworkSettings, EnvironmentResourcesSettings};
use petri_execution::inspect;
use petri_store::{Access, LogId, RunKey, RunStore};
use tokio::time::sleep;
use tokio_util::sync::CancellationToken;
pub(crate) const POLL: Duration = Duration::from_millis(10);
pub(crate) const PATIENCE: Duration = Duration::from_secs(30);
/// The `.fabro/workflows/hello` bundle checked into this repository.
pub(crate) fn hello_bundle() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../.fabro/workflows/hello")
}
pub(crate) const SETTINGS: &str = "_version = 1\n\n[workflow]\ngraph = \"workflow.fabro\"\n";
pub(crate) fn bundle(files: &[(&str, &str)]) -> Bundle {
Bundle {
files: files
.iter()
.map(|(path, text)| ((*path).to_string(), (*text).to_string()))
.collect(),
entrypoint: "workflow.fabro".to_string(),
project_toml: None,
}
}
/// Admit a bundle as the create handler does, with the given launch.
pub(crate) fn admit(
files: &[(&str, &str)],
launch: Launch,
runtime: &RuntimeSpec,
) -> AdmittedGraphs {
let request = CheckRequest {
bundle: bundle(files),
inputs: BTreeMap::new(),
vars: BTreeMap::new(),
launch,
runtime: runtime.clone(),
unbound_is_warning: false,
};
let admitted = check::check(&request)
.unwrap_or_else(|error| panic!("the workflow is admitted: {error:?}"));
AdmittedGraphs {
graph: admitted.graph,
children: admitted.children,
}
}
/// A run request over the engine assembly, on the host sandbox, with a
/// fresh cancel token and nothing installed beyond the interviewer.
pub(crate) fn run_request(
run_id: &str,
run_dir: &Path,
graphs: AdmittedGraphs,
store: Arc<dyn RunStore>,
runtime: RuntimeSpec,
interviewer: FabroInterviewer,
) -> RunRequest {
RunRequest {
run_id: run_id.to_string(),
run_dir: run_dir.to_path_buf(),
execution: Execution::Start(graphs),
store,
runtime,
provider: SandboxProviderKind::LOCAL,
resources: EnvironmentResourcesSettings::default(),
network: EnvironmentNetworkSettings::default(),
cancel: CancellationToken::new(),
controls: RunControls::new(),
observers: vec![interviewer.observer()],
interviewer: Arc::new(interviewer),
secrets: None,
blobs: None,
hooks: None,
}
}
/// An interviewer whose answers nobody delivers, for runs that ask nothing.
pub(crate) fn no_questions(sink: Arc<dyn QuestionSink>) -> FabroInterviewer {
FabroInterviewer::new(
Arc::new(fabro_interview::ControlInterviewer::new()),
Approval::Prompt,
)
.with_sink(sink)
}
/// A sink that drops every notice.
pub(crate) struct Silent;
#[async_trait::async_trait]
impl QuestionSink for Silent {
async fn post(&self, _notice: QuestionNotice) -> anyhow::Result<()> {
Ok(())
}
}
/// Every record of every log of a stored run, as JSON, in log order.
pub(crate) async fn all_records(store: &dyn RunStore, run_id: &str) -> Vec<serde_json::Value> {
let logs = store
.open(&RunKey::new(run_id), Access::Read)
.await
.expect("the run opens for reading");
let inspection = inspect::inspect_run(&*logs)
.await
.expect("the stored run inspects");
let mut ids = vec![LogId::Coordinator, LogId::Resources];
ids.extend(
inspection
.executions
.iter()
.map(|execution| LogId::Execution(execution.execution)),
);
let mut records = Vec::new();
for id in ids {
records.extend(
logs.read(&id)
.await
.expect("the log reads")
.into_iter()
.map(|record| record.record),
);
}
records
}
/// Wait until `condition` holds, polling, or fail after [`PATIENCE`].
pub(crate) async fn wait_until(what: &str, mut condition: impl FnMut() -> bool) {
let deadline = Instant::now() + PATIENCE;
while !condition() {
assert!(Instant::now() < deadline, "timed out waiting for {what}");
sleep(POLL).await;
}
}