style: apply cargo fmt workspace-wide

Pure formatting pass from `cargo fmt --all`. No logic changes. Separating
this out so the 1.9 release feature commits that follow show only their
intentional edits.
This commit is contained in:
2026-04-07 18:44:21 +01:00
parent 3915bcc1ec
commit 02a574b24e
102 changed files with 2467 additions and 1307 deletions

View File

@@ -2,4 +2,4 @@ pub mod config;
pub mod step; pub mod step;
pub use config::{BuildkitConfig, RegistryAuth, TlsConfig}; pub use config::{BuildkitConfig, RegistryAuth, TlsConfig};
pub use step::{build_output_data, parse_digest, BuildkitStep}; pub use step::{BuildkitStep, build_output_data, parse_digest};

View File

@@ -9,9 +9,9 @@ use wfe_buildkit_protos::moby::buildkit::v1::control_client::ControlClient;
use wfe_buildkit_protos::moby::buildkit::v1::{ use wfe_buildkit_protos::moby::buildkit::v1::{
CacheOptions, CacheOptionsEntry, Exporter, SolveRequest, StatusRequest, CacheOptions, CacheOptionsEntry, Exporter, SolveRequest, StatusRequest,
}; };
use wfe_core::WfeError;
use wfe_core::models::ExecutionResult; use wfe_core::models::ExecutionResult;
use wfe_core::traits::step::{StepBody, StepExecutionContext}; use wfe_core::traits::step::{StepBody, StepExecutionContext};
use wfe_core::WfeError;
use crate::config::BuildkitConfig; use crate::config::BuildkitConfig;
@@ -45,10 +45,7 @@ impl BuildkitStep {
tracing::info!(addr = %addr, "connecting to BuildKit daemon"); tracing::info!(addr = %addr, "connecting to BuildKit daemon");
let channel = if addr.starts_with("unix://") { let channel = if addr.starts_with("unix://") {
let socket_path = addr let socket_path = addr.strip_prefix("unix://").unwrap().to_string();
.strip_prefix("unix://")
.unwrap()
.to_string();
// Verify the socket exists before attempting connection. // Verify the socket exists before attempting connection.
if !Path::new(&socket_path).exists() { if !Path::new(&socket_path).exists() {
@@ -60,9 +57,7 @@ impl BuildkitStep {
// tonic requires a dummy URI for Unix sockets; the actual path // tonic requires a dummy URI for Unix sockets; the actual path
// is provided via the connector. // is provided via the connector.
Endpoint::try_from("http://[::]:50051") Endpoint::try_from("http://[::]:50051")
.map_err(|e| { .map_err(|e| WfeError::StepExecution(format!("failed to create endpoint: {e}")))?
WfeError::StepExecution(format!("failed to create endpoint: {e}"))
})?
.connect_with_connector(tower::service_fn(move |_: Uri| { .connect_with_connector(tower::service_fn(move |_: Uri| {
let path = socket_path.clone(); let path = socket_path.clone();
async move { async move {
@@ -231,10 +226,7 @@ impl BuildkitStep {
let context_name = "context"; let context_name = "context";
let dockerfile_name = "dockerfile"; let dockerfile_name = "dockerfile";
frontend_attrs.insert( frontend_attrs.insert("context".to_string(), format!("local://{context_name}"));
"context".to_string(),
format!("local://{context_name}"),
);
frontend_attrs.insert( frontend_attrs.insert(
format!("local-sessionid:{context_name}"), format!("local-sessionid:{context_name}"),
session_id.clone(), session_id.clone(),
@@ -276,20 +268,18 @@ impl BuildkitStep {
// The x-docker-expose-session-uuid header tells buildkitd which // The x-docker-expose-session-uuid header tells buildkitd which
// session owns the local sources. The x-docker-expose-session-grpc-method // session owns the local sources. The x-docker-expose-session-grpc-method
// header lists the gRPC methods the session implements. // header lists the gRPC methods the session implements.
if let Ok(key) = if let Ok(key) = "x-docker-expose-session-uuid"
"x-docker-expose-session-uuid" .parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>()
.parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>() && let Ok(val) =
&& let Ok(val) = session_id session_id.parse::<tonic::metadata::MetadataValue<tonic::metadata::Ascii>>()
.parse::<tonic::metadata::MetadataValue<tonic::metadata::Ascii>>()
{ {
metadata.insert(key, val); metadata.insert(key, val);
} }
// Advertise the filesync method so the daemon knows it can request // Advertise the filesync method so the daemon knows it can request
// local file content from our session. // local file content from our session.
if let Ok(key) = if let Ok(key) = "x-docker-expose-session-grpc-method"
"x-docker-expose-session-grpc-method" .parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>()
.parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>()
{ {
if let Ok(val) = "/moby.filesync.v1.FileSync/DiffCopy" if let Ok(val) = "/moby.filesync.v1.FileSync/DiffCopy"
.parse::<tonic::metadata::MetadataValue<tonic::metadata::Ascii>>() .parse::<tonic::metadata::MetadataValue<tonic::metadata::Ascii>>()
@@ -598,7 +588,8 @@ mod tests {
#[test] #[test]
fn parse_digest_with_digest_prefix() { fn parse_digest_with_digest_prefix() {
let output = "digest: sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef\n"; let output =
"digest: sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef\n";
let digest = parse_digest(output); let digest = parse_digest(output);
assert_eq!( assert_eq!(
digest, digest,
@@ -630,8 +621,7 @@ mod tests {
#[test] #[test]
fn parse_digest_wrong_prefix() { fn parse_digest_wrong_prefix() {
let output = let output = "sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
"sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
assert_eq!(parse_digest(output), None); assert_eq!(parse_digest(output), None);
} }
@@ -651,7 +641,10 @@ mod tests {
"#; "#;
assert_eq!( assert_eq!(
parse_digest(output), parse_digest(output),
Some("sha256:aabbccdd0011223344556677aabbccdd0011223344556677aabbccdd00112233".to_string()) Some(
"sha256:aabbccdd0011223344556677aabbccdd0011223344556677aabbccdd00112233"
.to_string()
)
); );
} }
@@ -659,9 +652,7 @@ mod tests {
fn parse_digest_first_match_wins() { fn parse_digest_first_match_wins() {
let hash1 = "a".repeat(64); let hash1 = "a".repeat(64);
let hash2 = "b".repeat(64); let hash2 = "b".repeat(64);
let output = format!( let output = format!("exporting manifest sha256:{hash1}\ndigest: sha256:{hash2}");
"exporting manifest sha256:{hash1}\ndigest: sha256:{hash2}"
);
let digest = parse_digest(&output).unwrap(); let digest = parse_digest(&output).unwrap();
assert_eq!(digest, format!("sha256:{hash1}")); assert_eq!(digest, format!("sha256:{hash1}"));
} }
@@ -806,10 +797,7 @@ mod tests {
exporters[0].attrs.get("name"), exporters[0].attrs.get("name"),
Some(&"myapp:latest,myapp:v1.0".to_string()) Some(&"myapp:latest,myapp:v1.0".to_string())
); );
assert_eq!( assert_eq!(exporters[0].attrs.get("push"), Some(&"true".to_string()));
exporters[0].attrs.get("push"),
Some(&"true".to_string())
);
} }
#[test] #[test]

View File

@@ -9,17 +9,16 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
use wfe_buildkit::config::{BuildkitConfig, TlsConfig};
use wfe_buildkit::BuildkitStep; use wfe_buildkit::BuildkitStep;
use wfe_buildkit::config::{BuildkitConfig, TlsConfig};
use wfe_core::models::{ExecutionPointer, WorkflowInstance, WorkflowStep}; use wfe_core::models::{ExecutionPointer, WorkflowInstance, WorkflowStep};
use wfe_core::traits::step::{StepBody, StepExecutionContext}; use wfe_core::traits::step::{StepBody, StepExecutionContext};
/// Get the BuildKit daemon address from the environment or use the default. /// Get the BuildKit daemon address from the environment or use the default.
fn buildkit_addr() -> String { fn buildkit_addr() -> String {
std::env::var("WFE_BUILDKIT_ADDR").unwrap_or_else(|_| { std::env::var("WFE_BUILDKIT_ADDR")
"unix:///Users/sienna/.lima/wfe-test/sock/buildkitd.sock".to_string() .unwrap_or_else(|_| "unix:///Users/sienna/.lima/wfe-test/sock/buildkitd.sock".to_string())
})
} }
/// Check whether the BuildKit daemon socket is reachable. /// Check whether the BuildKit daemon socket is reachable.
@@ -33,13 +32,7 @@ fn buildkitd_available() -> bool {
} }
} }
fn make_test_context( fn make_test_context(step_name: &str) -> (WorkflowStep, ExecutionPointer, WorkflowInstance) {
step_name: &str,
) -> (
WorkflowStep,
ExecutionPointer,
WorkflowInstance,
) {
let mut step = WorkflowStep::new(0, "buildkit"); let mut step = WorkflowStep::new(0, "buildkit");
step.name = Some(step_name.to_string()); step.name = Some(step_name.to_string());
let pointer = ExecutionPointer::new(0); let pointer = ExecutionPointer::new(0);
@@ -50,21 +43,14 @@ fn make_test_context(
#[tokio::test] #[tokio::test]
async fn build_simple_dockerfile_via_grpc() { async fn build_simple_dockerfile_via_grpc() {
if !buildkitd_available() { if !buildkitd_available() {
eprintln!( eprintln!("SKIP: BuildKit daemon not available at {}", buildkit_addr());
"SKIP: BuildKit daemon not available at {}",
buildkit_addr()
);
return; return;
} }
// Create a temp directory with a trivial Dockerfile. // Create a temp directory with a trivial Dockerfile.
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let dockerfile = tmp.path().join("Dockerfile"); let dockerfile = tmp.path().join("Dockerfile");
std::fs::write( std::fs::write(&dockerfile, "FROM alpine:latest\nRUN echo built\n").unwrap();
&dockerfile,
"FROM alpine:latest\nRUN echo built\n",
)
.unwrap();
let config = BuildkitConfig { let config = BuildkitConfig {
dockerfile: "Dockerfile".to_string(), dockerfile: "Dockerfile".to_string(),
@@ -94,7 +80,7 @@ async fn build_simple_dockerfile_via_grpc() {
workflow: &instance, workflow: &instance,
cancellation_token: cancel, cancellation_token: cancel,
host_context: None, host_context: None,
log_sink: None, log_sink: None,
}; };
let result = step.run(&ctx).await.expect("build should succeed"); let result = step.run(&ctx).await.expect("build should succeed");
@@ -135,10 +121,7 @@ async fn build_simple_dockerfile_via_grpc() {
#[tokio::test] #[tokio::test]
async fn build_with_build_args() { async fn build_with_build_args() {
if !buildkitd_available() { if !buildkitd_available() {
eprintln!( eprintln!("SKIP: BuildKit daemon not available at {}", buildkit_addr());
"SKIP: BuildKit daemon not available at {}",
buildkit_addr()
);
return; return;
} }
@@ -181,10 +164,13 @@ async fn build_with_build_args() {
workflow: &instance, workflow: &instance,
cancellation_token: cancel, cancellation_token: cancel,
host_context: None, host_context: None,
log_sink: None, log_sink: None,
}; };
let result = step.run(&ctx).await.expect("build with args should succeed"); let result = step
.run(&ctx)
.await
.expect("build with args should succeed");
assert!(result.proceed); assert!(result.proceed);
let data = result.output_data.expect("should have output_data"); let data = result.output_data.expect("should have output_data");
@@ -229,7 +215,7 @@ async fn connect_to_unavailable_daemon_returns_error() {
workflow: &instance, workflow: &instance,
cancellation_token: cancel, cancellation_token: cancel,
host_context: None, host_context: None,
log_sink: None, log_sink: None,
}; };
let err = step.run(&ctx).await; let err = step.run(&ctx).await;

View File

@@ -133,7 +133,11 @@ mod tests {
assert_eq!(deserialized.tls.ca, Some("/ca.pem".to_string())); assert_eq!(deserialized.tls.ca, Some("/ca.pem".to_string()));
assert_eq!(deserialized.tls.cert, Some("/cert.pem".to_string())); assert_eq!(deserialized.tls.cert, Some("/cert.pem".to_string()));
assert_eq!(deserialized.tls.key, Some("/key.pem".to_string())); assert_eq!(deserialized.tls.key, Some("/key.pem".to_string()));
assert!(deserialized.registry_auth.contains_key("registry.example.com")); assert!(
deserialized
.registry_auth
.contains_key("registry.example.com")
);
assert_eq!(deserialized.timeout_ms, Some(30000)); assert_eq!(deserialized.timeout_ms, Some(30000));
} }

View File

@@ -8,21 +8,20 @@ use wfe_core::models::ExecutionResult;
use wfe_core::traits::step::{StepBody, StepExecutionContext}; use wfe_core::traits::step::{StepBody, StepExecutionContext};
use wfe_containerd_protos::containerd::services::containers::v1::{ use wfe_containerd_protos::containerd::services::containers::v1::{
containers_client::ContainersClient, Container, CreateContainerRequest, Container, CreateContainerRequest, DeleteContainerRequest, container::Runtime,
DeleteContainerRequest, container::Runtime, containers_client::ContainersClient,
}; };
use wfe_containerd_protos::containerd::services::content::v1::{ use wfe_containerd_protos::containerd::services::content::v1::{
content_client::ContentClient, ReadContentRequest, ReadContentRequest, content_client::ContentClient,
}; };
use wfe_containerd_protos::containerd::services::images::v1::{ use wfe_containerd_protos::containerd::services::images::v1::{
images_client::ImagesClient, GetImageRequest, GetImageRequest, images_client::ImagesClient,
}; };
use wfe_containerd_protos::containerd::services::snapshots::v1::{ use wfe_containerd_protos::containerd::services::snapshots::v1::{
snapshots_client::SnapshotsClient, MountsRequest, PrepareSnapshotRequest, MountsRequest, PrepareSnapshotRequest, snapshots_client::SnapshotsClient,
}; };
use wfe_containerd_protos::containerd::services::tasks::v1::{ use wfe_containerd_protos::containerd::services::tasks::v1::{
tasks_client::TasksClient, CreateTaskRequest, DeleteTaskRequest, StartRequest, CreateTaskRequest, DeleteTaskRequest, StartRequest, WaitRequest, tasks_client::TasksClient,
WaitRequest,
}; };
use wfe_containerd_protos::containerd::services::version::v1::version_client::VersionClient; use wfe_containerd_protos::containerd::services::version::v1::version_client::VersionClient;
@@ -49,10 +48,7 @@ impl ContainerdStep {
/// TCP/HTTP endpoints. /// TCP/HTTP endpoints.
pub(crate) async fn connect(addr: &str) -> Result<Channel, WfeError> { pub(crate) async fn connect(addr: &str) -> Result<Channel, WfeError> {
let channel = if addr.starts_with('/') || addr.starts_with("unix://") { let channel = if addr.starts_with('/') || addr.starts_with("unix://") {
let socket_path = addr let socket_path = addr.strip_prefix("unix://").unwrap_or(addr).to_string();
.strip_prefix("unix://")
.unwrap_or(addr)
.to_string();
if !Path::new(&socket_path).exists() { if !Path::new(&socket_path).exists() {
return Err(WfeError::StepExecution(format!( return Err(WfeError::StepExecution(format!(
@@ -61,9 +57,7 @@ impl ContainerdStep {
} }
Endpoint::try_from("http://[::]:50051") Endpoint::try_from("http://[::]:50051")
.map_err(|e| { .map_err(|e| WfeError::StepExecution(format!("failed to create endpoint: {e}")))?
WfeError::StepExecution(format!("failed to create endpoint: {e}"))
})?
.connect_with_connector(tower::service_fn(move |_: Uri| { .connect_with_connector(tower::service_fn(move |_: Uri| {
let path = socket_path.clone(); let path = socket_path.clone();
async move { async move {
@@ -112,20 +106,14 @@ impl ContainerdStep {
/// `ctr image pull` or `nerdctl pull`. /// `ctr image pull` or `nerdctl pull`.
/// ///
/// TODO: implement full image pull via TransferService or content ingest. /// TODO: implement full image pull via TransferService or content ingest.
async fn ensure_image( async fn ensure_image(channel: &Channel, image: &str, namespace: &str) -> Result<(), WfeError> {
channel: &Channel,
image: &str,
namespace: &str,
) -> Result<(), WfeError> {
let mut client = ImagesClient::new(channel.clone()); let mut client = ImagesClient::new(channel.clone());
let mut req = tonic::Request::new(GetImageRequest { let mut req = tonic::Request::new(GetImageRequest {
name: image.to_string(), name: image.to_string(),
}); });
req.metadata_mut().insert( req.metadata_mut()
"containerd-namespace", .insert("containerd-namespace", namespace.parse().unwrap());
namespace.parse().unwrap(),
);
match client.get(req).await { match client.get(req).await {
Ok(_) => Ok(()), Ok(_) => Ok(()),
@@ -151,20 +139,24 @@ impl ContainerdStep {
image: &str, image: &str,
namespace: &str, namespace: &str,
) -> Result<String, WfeError> { ) -> Result<String, WfeError> {
use sha2::{Sha256, Digest}; use sha2::{Digest, Sha256};
// 1. Get the image record to find the manifest digest. // 1. Get the image record to find the manifest digest.
let mut images_client = ImagesClient::new(channel.clone()); let mut images_client = ImagesClient::new(channel.clone());
let req = Self::with_namespace( let req = Self::with_namespace(
GetImageRequest { name: image.to_string() }, GetImageRequest {
name: image.to_string(),
},
namespace, namespace,
); );
let image_resp = images_client.get(req).await.map_err(|e| { let image_resp = images_client
WfeError::StepExecution(format!("failed to get image '{image}': {e}")) .get(req)
})?; .await
let img = image_resp.into_inner().image.ok_or_else(|| { .map_err(|e| WfeError::StepExecution(format!("failed to get image '{image}': {e}")))?;
WfeError::StepExecution(format!("image '{image}' has no record")) let img = image_resp
})?; .into_inner()
.image
.ok_or_else(|| WfeError::StepExecution(format!("image '{image}' has no record")))?;
let target = img.target.ok_or_else(|| { let target = img.target.ok_or_else(|| {
WfeError::StepExecution(format!("image '{image}' has no target descriptor")) WfeError::StepExecution(format!("image '{image}' has no target descriptor"))
})?; })?;
@@ -188,22 +180,26 @@ impl ContainerdStep {
let manifests = manifest_json["manifests"].as_array().ok_or_else(|| { let manifests = manifest_json["manifests"].as_array().ok_or_else(|| {
WfeError::StepExecution("image index has no manifests array".to_string()) WfeError::StepExecution("image index has no manifests array".to_string())
})?; })?;
let platform_manifest = manifests.iter().find(|m| { let platform_manifest = manifests
m.get("platform") .iter()
.and_then(|p| p.get("architecture")) .find(|m| {
.and_then(|a| a.as_str()) m.get("platform")
== Some(oci_arch) .and_then(|p| p.get("architecture"))
}).ok_or_else(|| { .and_then(|a| a.as_str())
WfeError::StepExecution(format!( == Some(oci_arch)
"no manifest for architecture '{oci_arch}' in image index" })
)) .ok_or_else(|| {
})?; WfeError::StepExecution(format!(
"no manifest for architecture '{oci_arch}' in image index"
))
})?;
let digest = platform_manifest["digest"].as_str().ok_or_else(|| { let digest = platform_manifest["digest"].as_str().ok_or_else(|| {
WfeError::StepExecution("platform manifest has no digest".to_string()) WfeError::StepExecution("platform manifest has no digest".to_string())
})?; })?;
let bytes = Self::read_content(channel, digest, namespace).await?; let bytes = Self::read_content(channel, digest, namespace).await?;
serde_json::from_slice(&bytes) serde_json::from_slice(&bytes).map_err(|e| {
.map_err(|e| WfeError::StepExecution(format!("failed to parse platform manifest: {e}")))? WfeError::StepExecution(format!("failed to parse platform manifest: {e}"))
})?
} else { } else {
manifest_json manifest_json
}; };
@@ -211,9 +207,7 @@ impl ContainerdStep {
// 3. Get the config digest from the manifest. // 3. Get the config digest from the manifest.
let config_digest = manifest_json["config"]["digest"] let config_digest = manifest_json["config"]["digest"]
.as_str() .as_str()
.ok_or_else(|| { .ok_or_else(|| WfeError::StepExecution("manifest has no config.digest".to_string()))?;
WfeError::StepExecution("manifest has no config.digest".to_string())
})?;
// 4. Read the image config. // 4. Read the image config.
let config_bytes = Self::read_content(channel, config_digest, namespace).await?; let config_bytes = Self::read_content(channel, config_digest, namespace).await?;
@@ -239,9 +233,9 @@ impl ContainerdStep {
.to_string(); .to_string();
for diff_id in &diff_ids[1..] { for diff_id in &diff_ids[1..] {
let diff = diff_id.as_str().ok_or_else(|| { let diff = diff_id
WfeError::StepExecution("diff_id is not a string".to_string()) .as_str()
})?; .ok_or_else(|| WfeError::StepExecution("diff_id is not a string".to_string()))?;
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(format!("{chain_id} {diff}")); hasher.update(format!("{chain_id} {diff}"));
chain_id = format!("sha256:{:x}", hasher.finalize()); chain_id = format!("sha256:{:x}", hasher.finalize());
@@ -269,9 +263,11 @@ impl ContainerdStep {
namespace, namespace,
); );
let mut stream = client.read(req).await.map_err(|e| { let mut stream = client
WfeError::StepExecution(format!("failed to read content {digest}: {e}")) .read(req)
})?.into_inner(); .await
.map_err(|e| WfeError::StepExecution(format!("failed to read content {digest}: {e}")))?
.into_inner();
let mut data = Vec::new(); let mut data = Vec::new();
while let Some(chunk) = stream.next().await { while let Some(chunk) = stream.next().await {
@@ -288,10 +284,7 @@ impl ContainerdStep {
/// ///
/// The spec is serialized as JSON and wrapped in a protobuf Any with /// The spec is serialized as JSON and wrapped in a protobuf Any with
/// the containerd OCI spec type URL. /// the containerd OCI spec type URL.
pub(crate) fn build_oci_spec( pub(crate) fn build_oci_spec(&self, merged_env: &HashMap<String, String>) -> prost_types::Any {
&self,
merged_env: &HashMap<String, String>,
) -> prost_types::Any {
// Build the args array for the process. // Build the args array for the process.
let args: Vec<String> = if let Some(ref run) = self.config.run { let args: Vec<String> = if let Some(ref run) = self.config.run {
vec!["/bin/sh".to_string(), "-c".to_string(), run.clone()] vec!["/bin/sh".to_string(), "-c".to_string(), run.clone()]
@@ -302,10 +295,7 @@ impl ContainerdStep {
}; };
// Build env in KEY=VALUE form. // Build env in KEY=VALUE form.
let env: Vec<String> = merged_env let env: Vec<String> = merged_env.iter().map(|(k, v)| format!("{k}={v}")).collect();
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect();
// Build mounts. // Build mounts.
let mut mounts = vec![ let mut mounts = vec![
@@ -360,10 +350,20 @@ impl ContainerdStep {
// capability set so tools like apt-get work. Non-root gets nothing. // capability set so tools like apt-get work. Non-root gets nothing.
let caps = if uid == 0 { let caps = if uid == 0 {
serde_json::json!([ serde_json::json!([
"CAP_AUDIT_WRITE", "CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_AUDIT_WRITE",
"CAP_FOWNER", "CAP_FSETID", "CAP_KILL", "CAP_MKNOD", "CAP_CHOWN",
"CAP_NET_BIND_SERVICE", "CAP_NET_RAW", "CAP_SETFCAP", "CAP_DAC_OVERRIDE",
"CAP_SETGID", "CAP_SETPCAP", "CAP_SETUID", "CAP_SYS_CHROOT", "CAP_FOWNER",
"CAP_FSETID",
"CAP_KILL",
"CAP_MKNOD",
"CAP_NET_BIND_SERVICE",
"CAP_NET_RAW",
"CAP_SETFCAP",
"CAP_SETGID",
"CAP_SETPCAP",
"CAP_SETUID",
"CAP_SYS_CHROOT",
]) ])
} else { } else {
serde_json::json!([]) serde_json::json!([])
@@ -405,10 +405,9 @@ impl ContainerdStep {
/// Inject a `containerd-namespace` header into a tonic request. /// Inject a `containerd-namespace` header into a tonic request.
pub(crate) fn with_namespace<T>(req: T, namespace: &str) -> tonic::Request<T> { pub(crate) fn with_namespace<T>(req: T, namespace: &str) -> tonic::Request<T> {
let mut request = tonic::Request::new(req); let mut request = tonic::Request::new(req);
request.metadata_mut().insert( request
"containerd-namespace", .metadata_mut()
namespace.parse().unwrap(), .insert("containerd-namespace", namespace.parse().unwrap());
);
request request
} }
@@ -492,8 +491,7 @@ impl ContainerdStep {
match snapshots_client.mounts(mounts_req).await { match snapshots_client.mounts(mounts_req).await {
Ok(resp) => resp.into_inner().mounts, Ok(resp) => resp.into_inner().mounts,
Err(_) => { Err(_) => {
let parent = let parent = Self::resolve_image_chain_id(&channel, image, namespace).await?;
Self::resolve_image_chain_id(&channel, image, namespace).await?;
let prepare_req = Self::with_namespace( let prepare_req = Self::with_namespace(
PrepareSnapshotRequest { PrepareSnapshotRequest {
snapshotter: DEFAULT_SNAPSHOTTER.to_string(), snapshotter: DEFAULT_SNAPSHOTTER.to_string(),
@@ -531,9 +529,10 @@ impl ContainerdStep {
}, },
namespace, namespace,
); );
tasks_client.create(create_task_req).await.map_err(|e| { tasks_client
WfeError::StepExecution(format!("failed to create service task: {e}")) .create(create_task_req)
})?; .await
.map_err(|e| WfeError::StepExecution(format!("failed to create service task: {e}")))?;
let start_req = Self::with_namespace( let start_req = Self::with_namespace(
StartRequest { StartRequest {
@@ -542,9 +541,10 @@ impl ContainerdStep {
}, },
namespace, namespace,
); );
tasks_client.start(start_req).await.map_err(|e| { tasks_client
WfeError::StepExecution(format!("failed to start service task: {e}")) .start(start_req)
})?; .await
.map_err(|e| WfeError::StepExecution(format!("failed to start service task: {e}")))?;
tracing::info!(container_id = %container_id, image = %image, "service container started"); tracing::info!(container_id = %container_id, image = %image, "service container started");
Ok(()) Ok(())
@@ -701,9 +701,10 @@ impl StepBody for ContainerdStep {
namespace, namespace,
); );
containers_client.create(create_req).await.map_err(|e| { containers_client
WfeError::StepExecution(format!("failed to create container: {e}")) .create(create_req)
})?; .await
.map_err(|e| WfeError::StepExecution(format!("failed to create container: {e}")))?;
// 6. Prepare snapshot with the image's layers as parent. // 6. Prepare snapshot with the image's layers as parent.
let mut snapshots_client = SnapshotsClient::new(channel.clone()); let mut snapshots_client = SnapshotsClient::new(channel.clone());
@@ -723,7 +724,8 @@ impl StepBody for ContainerdStep {
Err(_) => { Err(_) => {
// Resolve the image's chain ID to use as snapshot parent. // Resolve the image's chain ID to use as snapshot parent.
let parent = if should_check { let parent = if should_check {
Self::resolve_image_chain_id(&channel, &self.config.image, namespace).await? Self::resolve_image_chain_id(&channel, &self.config.image, namespace)
.await?
} else { } else {
String::new() String::new()
}; };
@@ -741,9 +743,7 @@ impl StepBody for ContainerdStep {
.prepare(prepare_req) .prepare(prepare_req)
.await .await
.map_err(|e| { .map_err(|e| {
WfeError::StepExecution(format!( WfeError::StepExecution(format!("failed to prepare snapshot: {e}"))
"failed to prepare snapshot: {e}"
))
})? })?
.into_inner() .into_inner()
.mounts .mounts
@@ -758,9 +758,8 @@ impl StepBody for ContainerdStep {
.map(std::path::PathBuf::from) .map(std::path::PathBuf::from)
.unwrap_or_else(|_| std::env::temp_dir()); .unwrap_or_else(|_| std::env::temp_dir());
let tmp_dir = io_base.join(format!("wfe-io-{container_id}")); let tmp_dir = io_base.join(format!("wfe-io-{container_id}"));
std::fs::create_dir_all(&tmp_dir).map_err(|e| { std::fs::create_dir_all(&tmp_dir)
WfeError::StepExecution(format!("failed to create IO temp dir: {e}")) .map_err(|e| WfeError::StepExecution(format!("failed to create IO temp dir: {e}")))?;
})?;
let stdout_path = tmp_dir.join("stdout"); let stdout_path = tmp_dir.join("stdout");
let stderr_path = tmp_dir.join("stderr"); let stderr_path = tmp_dir.join("stderr");
@@ -802,9 +801,10 @@ impl StepBody for ContainerdStep {
namespace, namespace,
); );
tasks_client.create(create_task_req).await.map_err(|e| { tasks_client
WfeError::StepExecution(format!("failed to create task: {e}")) .create(create_task_req)
})?; .await
.map_err(|e| WfeError::StepExecution(format!("failed to create task: {e}")))?;
// Start the task. // Start the task.
let start_req = Self::with_namespace( let start_req = Self::with_namespace(
@@ -815,9 +815,10 @@ impl StepBody for ContainerdStep {
namespace, namespace,
); );
tasks_client.start(start_req).await.map_err(|e| { tasks_client
WfeError::StepExecution(format!("failed to start task: {e}")) .start(start_req)
})?; .await
.map_err(|e| WfeError::StepExecution(format!("failed to start task: {e}")))?;
tracing::info!(container_id = %container_id, "task started"); tracing::info!(container_id = %container_id, "task started");
@@ -836,12 +837,7 @@ impl StepBody for ContainerdStep {
Ok(result) => result, Ok(result) => result,
Err(_) => { Err(_) => {
// Attempt cleanup before returning timeout error. // Attempt cleanup before returning timeout error.
let _ = Self::cleanup( let _ = Self::cleanup(&channel, &container_id, namespace).await;
&channel,
&container_id,
namespace,
)
.await;
let _ = std::fs::remove_dir_all(&tmp_dir); let _ = std::fs::remove_dir_all(&tmp_dir);
return Err(WfeError::StepExecution(format!( return Err(WfeError::StepExecution(format!(
"container execution timed out after {timeout_ms}ms" "container execution timed out after {timeout_ms}ms"
@@ -887,8 +883,13 @@ impl StepBody for ContainerdStep {
// 13. Parse outputs and build result. // 13. Parse outputs and build result.
let parsed = Self::parse_outputs(&stdout_content); let parsed = Self::parse_outputs(&stdout_content);
let output_data = let output_data = Self::build_output_data(
Self::build_output_data(step_name, &stdout_content, &stderr_content, exit_code, &parsed); step_name,
&stdout_content,
&stderr_content,
exit_code,
&parsed,
);
Ok(ExecutionResult { Ok(ExecutionResult {
proceed: true, proceed: true,
@@ -927,9 +928,7 @@ impl ContainerdStep {
containers_client containers_client
.delete(del_container_req) .delete(del_container_req)
.await .await
.map_err(|e| { .map_err(|e| WfeError::StepExecution(format!("failed to delete container: {e}")))?;
WfeError::StepExecution(format!("failed to delete container: {e}"))
})?;
Ok(()) Ok(())
} }
@@ -1013,10 +1012,7 @@ mod tests {
let stdout = "##wfe[output url=https://example.com?a=1&b=2]\n"; let stdout = "##wfe[output url=https://example.com?a=1&b=2]\n";
let outputs = ContainerdStep::parse_outputs(stdout); let outputs = ContainerdStep::parse_outputs(stdout);
assert_eq!(outputs.len(), 1); assert_eq!(outputs.len(), 1);
assert_eq!( assert_eq!(outputs.get("url").unwrap(), "https://example.com?a=1&b=2");
outputs.get("url").unwrap(),
"https://example.com?a=1&b=2"
);
} }
#[test] #[test]
@@ -1043,13 +1039,7 @@ mod tests {
#[test] #[test]
fn build_output_data_basic() { fn build_output_data_basic() {
let parsed = HashMap::from([("result".to_string(), "success".to_string())]); let parsed = HashMap::from([("result".to_string(), "success".to_string())]);
let data = ContainerdStep::build_output_data( let data = ContainerdStep::build_output_data("my_step", "hello world\n", "", 0, &parsed);
"my_step",
"hello world\n",
"",
0,
&parsed,
);
let obj = data.as_object().unwrap(); let obj = data.as_object().unwrap();
assert_eq!(obj.get("result").unwrap(), "success"); assert_eq!(obj.get("result").unwrap(), "success");
@@ -1060,13 +1050,7 @@ mod tests {
#[test] #[test]
fn build_output_data_no_parsed_outputs() { fn build_output_data_no_parsed_outputs() {
let data = ContainerdStep::build_output_data( let data = ContainerdStep::build_output_data("step1", "out", "err", 1, &HashMap::new());
"step1",
"out",
"err",
1,
&HashMap::new(),
);
let obj = data.as_object().unwrap(); let obj = data.as_object().unwrap();
assert_eq!(obj.len(), 3); // stdout, stderr, exit_code assert_eq!(obj.len(), 3); // stdout, stderr, exit_code
@@ -1150,7 +1134,11 @@ mod tests {
fn build_oci_spec_with_command() { fn build_oci_spec_with_command() {
let mut config = minimal_config(); let mut config = minimal_config();
config.run = None; config.run = None;
config.command = Some(vec!["echo".to_string(), "hello".to_string(), "world".to_string()]); config.command = Some(vec![
"echo".to_string(),
"hello".to_string(),
"world".to_string(),
]);
let step = ContainerdStep::new(config); let step = ContainerdStep::new(config);
let spec = step.build_oci_spec(&HashMap::new()); let spec = step.build_oci_spec(&HashMap::new());
@@ -1227,10 +1215,8 @@ mod tests {
// 3 default + 2 user = 5 // 3 default + 2 user = 5
assert_eq!(mounts.len(), 5); assert_eq!(mounts.len(), 5);
let bind_mounts: Vec<&serde_json::Value> = mounts let bind_mounts: Vec<&serde_json::Value> =
.iter() mounts.iter().filter(|m| m["type"] == "bind").collect();
.filter(|m| m["type"] == "bind")
.collect();
assert_eq!(bind_mounts.len(), 2); assert_eq!(bind_mounts.len(), 2);
let ro_mount = bind_mounts let ro_mount = bind_mounts
@@ -1274,10 +1260,9 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn connect_to_missing_unix_socket_with_scheme_returns_error() { async fn connect_to_missing_unix_socket_with_scheme_returns_error() {
let err = let err = ContainerdStep::connect("unix:///tmp/nonexistent-wfe-containerd-test.sock")
ContainerdStep::connect("unix:///tmp/nonexistent-wfe-containerd-test.sock") .await
.await .unwrap_err();
.unwrap_err();
let msg = format!("{err}"); let msg = format!("{err}");
assert!( assert!(
msg.contains("socket not found"), msg.contains("socket not found"),
@@ -1304,9 +1289,11 @@ mod tests {
let config = minimal_config(); let config = minimal_config();
let step = ContainerdStep::new(config); let step = ContainerdStep::new(config);
assert_eq!(step.config.image, "alpine:3.18"); assert_eq!(step.config.image, "alpine:3.18");
assert_eq!(step.config.containerd_addr, "/run/containerd/containerd.sock"); assert_eq!(
step.config.containerd_addr,
"/run/containerd/containerd.sock"
);
} }
} }
/// Integration tests that require a live containerd daemon. /// Integration tests that require a live containerd daemon.
@@ -1323,9 +1310,7 @@ mod e2e_tests {
) )
}); });
let socket_path = addr let socket_path = addr.strip_prefix("unix://").unwrap_or(addr.as_str());
.strip_prefix("unix://")
.unwrap_or(addr.as_str());
if Path::new(socket_path).exists() { if Path::new(socket_path).exists() {
Some(addr) Some(addr)
@@ -1350,6 +1335,9 @@ mod e2e_tests {
assert!(!version.version.is_empty(), "version should not be empty"); assert!(!version.version.is_empty(), "version should not be empty");
assert!(!version.revision.is_empty(), "revision should not be empty"); assert!(!version.revision.is_empty(), "revision should not be empty");
eprintln!("containerd version={} revision={}", version.version, version.revision); eprintln!(
"containerd version={} revision={}",
version.version, version.revision
);
} }
} }

View File

@@ -10,8 +10,8 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
use wfe_containerd::config::{ContainerdConfig, TlsConfig};
use wfe_containerd::ContainerdStep; use wfe_containerd::ContainerdStep;
use wfe_containerd::config::{ContainerdConfig, TlsConfig};
use wfe_core::models::{ExecutionPointer, WorkflowInstance, WorkflowStep}; use wfe_core::models::{ExecutionPointer, WorkflowInstance, WorkflowStep};
use wfe_core::traits::step::{StepBody, StepExecutionContext}; use wfe_core::traits::step::{StepBody, StepExecutionContext};
@@ -75,7 +75,7 @@ fn make_context<'a>(
workflow, workflow,
cancellation_token: tokio_util::sync::CancellationToken::new(), cancellation_token: tokio_util::sync::CancellationToken::new(),
host_context: None, host_context: None,
log_sink: None, log_sink: None,
} }
} }
@@ -204,8 +204,7 @@ async fn run_container_with_volume_mount() {
return; return;
}; };
let shared_dir = std::env::var("WFE_IO_DIR") let shared_dir = std::env::var("WFE_IO_DIR").unwrap_or_else(|_| "/tmp/wfe-io".to_string());
.unwrap_or_else(|_| "/tmp/wfe-io".to_string());
let vol_dir = format!("{shared_dir}/test-vol"); let vol_dir = format!("{shared_dir}/test-vol");
std::fs::create_dir_all(&vol_dir).unwrap(); std::fs::create_dir_all(&vol_dir).unwrap();
@@ -249,8 +248,7 @@ async fn run_debian_with_volume_and_network() {
return; return;
}; };
let shared_dir = std::env::var("WFE_IO_DIR") let shared_dir = std::env::var("WFE_IO_DIR").unwrap_or_else(|_| "/tmp/wfe-io".to_string());
.unwrap_or_else(|_| "/tmp/wfe-io".to_string());
let cargo_dir = format!("{shared_dir}/test-cargo"); let cargo_dir = format!("{shared_dir}/test-cargo");
let rustup_dir = format!("{shared_dir}/test-rustup"); let rustup_dir = format!("{shared_dir}/test-rustup");
std::fs::create_dir_all(&cargo_dir).unwrap(); std::fs::create_dir_all(&cargo_dir).unwrap();
@@ -263,8 +261,12 @@ async fn run_debian_with_volume_and_network() {
config.user = "0:0".to_string(); config.user = "0:0".to_string();
config.network = "host".to_string(); config.network = "host".to_string();
config.timeout_ms = Some(30_000); config.timeout_ms = Some(30_000);
config.env.insert("CARGO_HOME".to_string(), "/cargo".to_string()); config
config.env.insert("RUSTUP_HOME".to_string(), "/rustup".to_string()); .env
.insert("CARGO_HOME".to_string(), "/cargo".to_string());
config
.env
.insert("RUSTUP_HOME".to_string(), "/rustup".to_string());
config.volumes = vec![ config.volumes = vec![
wfe_containerd::VolumeMountConfig { wfe_containerd::VolumeMountConfig {
source: cargo_dir.clone(), source: cargo_dir.clone(),

View File

@@ -67,7 +67,10 @@ impl<D: WorkflowData> StepBuilder<D> {
} }
/// Chain an inline function step. /// Chain an inline function step.
pub fn then_fn(mut self, f: impl Fn() -> ExecutionResult + Send + Sync + 'static) -> StepBuilder<D> { pub fn then_fn(
mut self,
f: impl Fn() -> ExecutionResult + Send + Sync + 'static,
) -> StepBuilder<D> {
let next_id = self.builder.add_step(std::any::type_name::<InlineStep>()); let next_id = self.builder.add_step(std::any::type_name::<InlineStep>());
self.builder.wire_outcome(self.step_id, next_id, None); self.builder.wire_outcome(self.step_id, next_id, None);
self.builder.last_step = Some(next_id); self.builder.last_step = Some(next_id);
@@ -77,7 +80,9 @@ impl<D: WorkflowData> StepBuilder<D> {
/// Insert a WaitFor step. /// Insert a WaitFor step.
pub fn wait_for(mut self, event_name: &str, event_key: &str) -> StepBuilder<D> { pub fn wait_for(mut self, event_name: &str, event_key: &str) -> StepBuilder<D> {
let next_id = self.builder.add_step(std::any::type_name::<primitives::wait_for::WaitForStep>()); let next_id = self
.builder
.add_step(std::any::type_name::<primitives::wait_for::WaitForStep>());
self.builder.wire_outcome(self.step_id, next_id, None); self.builder.wire_outcome(self.step_id, next_id, None);
self.builder.last_step = Some(next_id); self.builder.last_step = Some(next_id);
self.builder.steps[next_id].step_config = Some(serde_json::json!({ self.builder.steps[next_id].step_config = Some(serde_json::json!({
@@ -89,7 +94,9 @@ impl<D: WorkflowData> StepBuilder<D> {
/// Insert a Delay step. /// Insert a Delay step.
pub fn delay(mut self, duration: std::time::Duration) -> StepBuilder<D> { pub fn delay(mut self, duration: std::time::Duration) -> StepBuilder<D> {
let next_id = self.builder.add_step(std::any::type_name::<primitives::delay::DelayStep>()); let next_id = self
.builder
.add_step(std::any::type_name::<primitives::delay::DelayStep>());
self.builder.wire_outcome(self.step_id, next_id, None); self.builder.wire_outcome(self.step_id, next_id, None);
self.builder.last_step = Some(next_id); self.builder.last_step = Some(next_id);
self.builder.steps[next_id].step_config = Some(serde_json::json!({ self.builder.steps[next_id].step_config = Some(serde_json::json!({
@@ -104,7 +111,9 @@ impl<D: WorkflowData> StepBuilder<D> {
mut self, mut self,
build_children: impl FnOnce(&mut WorkflowBuilder<D>), build_children: impl FnOnce(&mut WorkflowBuilder<D>),
) -> StepBuilder<D> { ) -> StepBuilder<D> {
let if_id = self.builder.add_step(std::any::type_name::<primitives::if_step::IfStep>()); let if_id = self
.builder
.add_step(std::any::type_name::<primitives::if_step::IfStep>());
self.builder.wire_outcome(self.step_id, if_id, None); self.builder.wire_outcome(self.step_id, if_id, None);
// Build children // Build children
@@ -126,7 +135,9 @@ impl<D: WorkflowData> StepBuilder<D> {
mut self, mut self,
build_children: impl FnOnce(&mut WorkflowBuilder<D>), build_children: impl FnOnce(&mut WorkflowBuilder<D>),
) -> StepBuilder<D> { ) -> StepBuilder<D> {
let while_id = self.builder.add_step(std::any::type_name::<primitives::while_step::WhileStep>()); let while_id = self
.builder
.add_step(std::any::type_name::<primitives::while_step::WhileStep>());
self.builder.wire_outcome(self.step_id, while_id, None); self.builder.wire_outcome(self.step_id, while_id, None);
let before_count = self.builder.steps.len(); let before_count = self.builder.steps.len();
@@ -146,7 +157,9 @@ impl<D: WorkflowData> StepBuilder<D> {
mut self, mut self,
build_children: impl FnOnce(&mut WorkflowBuilder<D>), build_children: impl FnOnce(&mut WorkflowBuilder<D>),
) -> StepBuilder<D> { ) -> StepBuilder<D> {
let fe_id = self.builder.add_step(std::any::type_name::<primitives::foreach_step::ForEachStep>()); let fe_id = self
.builder
.add_step(std::any::type_name::<primitives::foreach_step::ForEachStep>());
self.builder.wire_outcome(self.step_id, fe_id, None); self.builder.wire_outcome(self.step_id, fe_id, None);
let before_count = self.builder.steps.len(); let before_count = self.builder.steps.len();
@@ -162,11 +175,10 @@ impl<D: WorkflowData> StepBuilder<D> {
} }
/// Insert a Saga container step with child steps. /// Insert a Saga container step with child steps.
pub fn saga( pub fn saga(mut self, build_children: impl FnOnce(&mut WorkflowBuilder<D>)) -> StepBuilder<D> {
mut self, let saga_id = self.builder.add_step(std::any::type_name::<
build_children: impl FnOnce(&mut WorkflowBuilder<D>), primitives::saga_container::SagaContainerStep,
) -> StepBuilder<D> { >());
let saga_id = self.builder.add_step(std::any::type_name::<primitives::saga_container::SagaContainerStep>());
self.builder.steps[saga_id].saga = true; self.builder.steps[saga_id].saga = true;
self.builder.wire_outcome(self.step_id, saga_id, None); self.builder.wire_outcome(self.step_id, saga_id, None);
@@ -187,7 +199,9 @@ impl<D: WorkflowData> StepBuilder<D> {
mut self, mut self,
build_branches: impl FnOnce(ParallelBuilder<D>) -> ParallelBuilder<D>, build_branches: impl FnOnce(ParallelBuilder<D>) -> ParallelBuilder<D>,
) -> StepBuilder<D> { ) -> StepBuilder<D> {
let seq_id = self.builder.add_step(std::any::type_name::<primitives::sequence::SequenceStep>()); let seq_id = self
.builder
.add_step(std::any::type_name::<primitives::sequence::SequenceStep>());
self.builder.wire_outcome(self.step_id, seq_id, None); self.builder.wire_outcome(self.step_id, seq_id, None);
let pb = ParallelBuilder { let pb = ParallelBuilder {
@@ -213,10 +227,7 @@ impl<D: WorkflowData> StepBuilder<D> {
impl<D: WorkflowData> ParallelBuilder<D> { impl<D: WorkflowData> ParallelBuilder<D> {
/// Add a parallel branch. /// Add a parallel branch.
pub fn branch( pub fn branch(mut self, build_branch: impl FnOnce(&mut WorkflowBuilder<D>)) -> Self {
mut self,
build_branch: impl FnOnce(&mut WorkflowBuilder<D>),
) -> Self {
let before_count = self.builder.steps.len(); let before_count = self.builder.steps.len();
build_branch(&mut self.builder); build_branch(&mut self.builder);
let after_count = self.builder.steps.len(); let after_count = self.builder.steps.len();

View File

@@ -1,9 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::marker::PhantomData; use std::marker::PhantomData;
use crate::models::{ use crate::models::{ExecutionResult, StepOutcome, WorkflowDefinition, WorkflowStep};
ExecutionResult, StepOutcome, WorkflowDefinition, WorkflowStep,
};
use crate::traits::step::{StepBody, WorkflowData}; use crate::traits::step::{StepBody, WorkflowData};
use super::inline_step::InlineStep; use super::inline_step::InlineStep;
@@ -77,7 +75,12 @@ impl<D: WorkflowData> WorkflowBuilder<D> {
} }
/// Wire an outcome from `from_step` to `to_step`. /// Wire an outcome from `from_step` to `to_step`.
pub fn wire_outcome(&mut self, from_step: usize, to_step: usize, value: Option<serde_json::Value>) { pub fn wire_outcome(
&mut self,
from_step: usize,
to_step: usize,
value: Option<serde_json::Value>,
) {
if let Some(step) = self.steps.get_mut(from_step) { if let Some(step) = self.steps.get_mut(from_step) {
step.outcomes.push(StepOutcome { step.outcomes.push(StepOutcome {
next_step: to_step, next_step: to_step,

View File

@@ -1,5 +1,5 @@
use crate::models::condition::{ComparisonOp, FieldComparison, StepCondition};
use crate::WfeError; use crate::WfeError;
use crate::models::condition::{ComparisonOp, FieldComparison, StepCondition};
/// Evaluate a step condition against workflow data. /// Evaluate a step condition against workflow data.
/// ///
@@ -29,10 +29,7 @@ impl From<WfeError> for EvalError {
} }
} }
fn evaluate_inner( fn evaluate_inner(condition: &StepCondition, data: &serde_json::Value) -> Result<bool, EvalError> {
condition: &StepCondition,
data: &serde_json::Value,
) -> Result<bool, EvalError> {
match condition { match condition {
StepCondition::All(conditions) => { StepCondition::All(conditions) => {
for c in conditions { for c in conditions {
@@ -582,22 +579,14 @@ mod tests {
#[test] #[test]
fn not_true_becomes_false() { fn not_true_becomes_false() {
let data = json!({"a": 1}); let data = json!({"a": 1});
let cond = StepCondition::Not(Box::new(comp( let cond = StepCondition::Not(Box::new(comp(".a", ComparisonOp::Equals, Some(json!(1)))));
".a",
ComparisonOp::Equals,
Some(json!(1)),
)));
assert!(!evaluate(&cond, &data).unwrap()); assert!(!evaluate(&cond, &data).unwrap());
} }
#[test] #[test]
fn not_false_becomes_true() { fn not_false_becomes_true() {
let data = json!({"a": 99}); let data = json!({"a": 99});
let cond = StepCondition::Not(Box::new(comp( let cond = StepCondition::Not(Box::new(comp(".a", ComparisonOp::Equals, Some(json!(1)))));
".a",
ComparisonOp::Equals,
Some(json!(1)),
)));
assert!(evaluate(&cond, &data).unwrap()); assert!(evaluate(&cond, &data).unwrap());
} }
@@ -639,11 +628,7 @@ mod tests {
comp(".a", ComparisonOp::Equals, Some(json!(1))), comp(".a", ComparisonOp::Equals, Some(json!(1))),
comp(".a", ComparisonOp::Equals, Some(json!(99))), comp(".a", ComparisonOp::Equals, Some(json!(99))),
]), ]),
StepCondition::Not(Box::new(comp( StepCondition::Not(Box::new(comp(".c", ComparisonOp::Equals, Some(json!(99))))),
".c",
ComparisonOp::Equals,
Some(json!(99)),
))),
]); ]);
assert!(evaluate(&cond, &data).unwrap()); assert!(evaluate(&cond, &data).unwrap());
} }
@@ -742,7 +727,13 @@ mod tests {
let data = json!({"score": 3.14}); let data = json!({"score": 3.14});
assert!(evaluate(&comp(".score", ComparisonOp::Gt, Some(json!(3.0))), &data).unwrap()); assert!(evaluate(&comp(".score", ComparisonOp::Gt, Some(json!(3.0))), &data).unwrap());
assert!(evaluate(&comp(".score", ComparisonOp::Lt, Some(json!(4.0))), &data).unwrap()); assert!(evaluate(&comp(".score", ComparisonOp::Lt, Some(json!(4.0))), &data).unwrap());
assert!(!evaluate(&comp(".score", ComparisonOp::Equals, Some(json!(3.0))), &data).unwrap()); assert!(
!evaluate(
&comp(".score", ComparisonOp::Equals, Some(json!(3.0))),
&data
)
.unwrap()
);
} }
#[test] #[test]

View File

@@ -29,7 +29,10 @@ pub fn handle_error(
.unwrap_or_else(|| definition.default_error_behavior.clone()); .unwrap_or_else(|| definition.default_error_behavior.clone());
match behavior { match behavior {
ErrorBehavior::Retry { interval, max_retries } => { ErrorBehavior::Retry {
interval,
max_retries,
} => {
if max_retries > 0 && pointer.retry_count >= max_retries { if max_retries > 0 && pointer.retry_count >= max_retries {
// Exceeded max retries, suspend the workflow // Exceeded max retries, suspend the workflow
pointer.status = PointerStatus::Failed; pointer.status = PointerStatus::Failed;
@@ -44,9 +47,8 @@ pub fn handle_error(
pointer.retry_count += 1; pointer.retry_count += 1;
pointer.status = PointerStatus::Sleeping; pointer.status = PointerStatus::Sleeping;
pointer.active = true; pointer.active = true;
pointer.sleep_until = Some( pointer.sleep_until =
Utc::now() + chrono::Duration::milliseconds(interval.as_millis() as i64), Some(Utc::now() + chrono::Duration::milliseconds(interval.as_millis() as i64));
);
} }
} }
ErrorBehavior::Suspend => { ErrorBehavior::Suspend => {
@@ -67,7 +69,9 @@ pub fn handle_error(
&& let Some(comp_step_id) = step.compensation_step_id && let Some(comp_step_id) = step.compensation_step_id
{ {
let mut comp_pointer = ExecutionPointer::new(comp_step_id); let mut comp_pointer = ExecutionPointer::new(comp_step_id);
comp_pointer.step_name = definition.steps.iter() comp_pointer.step_name = definition
.steps
.iter()
.find(|s| s.id == comp_step_id) .find(|s| s.id == comp_step_id)
.and_then(|s| s.name.clone()); .and_then(|s| s.name.clone());
comp_pointer.predecessor_id = Some(pointer.id.clone()); comp_pointer.predecessor_id = Some(pointer.id.clone());

View File

@@ -36,7 +36,9 @@ pub fn process_result(
let next_step_id = find_next_step(step, &result.outcome_value); let next_step_id = find_next_step(step, &result.outcome_value);
if let Some(next_id) = next_step_id { if let Some(next_id) = next_step_id {
let mut next_pointer = ExecutionPointer::new(next_id); let mut next_pointer = ExecutionPointer::new(next_id);
next_pointer.step_name = definition.steps.iter() next_pointer.step_name = definition
.steps
.iter()
.find(|s| s.id == next_id) .find(|s| s.id == next_id)
.and_then(|s| s.name.clone()); .and_then(|s| s.name.clone());
next_pointer.predecessor_id = Some(pointer.id.clone()); next_pointer.predecessor_id = Some(pointer.id.clone());
@@ -62,7 +64,9 @@ pub fn process_result(
for value in branch_values { for value in branch_values {
for &child_step_id in &child_step_ids { for &child_step_id in &child_step_ids {
let mut child_pointer = ExecutionPointer::new(child_step_id); let mut child_pointer = ExecutionPointer::new(child_step_id);
child_pointer.step_name = definition.steps.iter() child_pointer.step_name = definition
.steps
.iter()
.find(|s| s.id == child_step_id) .find(|s| s.id == child_step_id)
.and_then(|s| s.name.clone()); .and_then(|s| s.name.clone());
child_pointer.context_item = Some(value.clone()); child_pointer.context_item = Some(value.clone());
@@ -79,9 +83,7 @@ pub fn process_result(
pointer.event_name = result.event_name.clone(); pointer.event_name = result.event_name.clone();
pointer.event_key = result.event_key.clone(); pointer.event_key = result.event_key.clone();
if let (Some(event_name), Some(event_key)) = if let (Some(event_name), Some(event_key)) = (&result.event_name, &result.event_key) {
(&result.event_name, &result.event_key)
{
let as_of = result.event_as_of.unwrap_or_else(Utc::now); let as_of = result.event_as_of.unwrap_or_else(Utc::now);
let sub = EventSubscription::new( let sub = EventSubscription::new(
workflow_id, workflow_id,
@@ -107,8 +109,7 @@ pub fn process_result(
pointer.status = PointerStatus::Sleeping; pointer.status = PointerStatus::Sleeping;
pointer.active = true; pointer.active = true;
pointer.sleep_until = Some( pointer.sleep_until = Some(
Utc::now() Utc::now() + chrono::Duration::milliseconds(poll_config.interval.as_millis() as i64),
+ chrono::Duration::milliseconds(poll_config.interval.as_millis() as i64),
); );
pointer.persistence_data = result.persistence_data.clone(); pointer.persistence_data = result.persistence_data.clone();
} else if result.persistence_data.is_some() { } else if result.persistence_data.is_some() {

View File

@@ -17,7 +17,8 @@ impl StepRegistry {
/// Register a step type using its full type name as the key. /// Register a step type using its full type name as the key.
pub fn register<S: StepBody + Default + 'static>(&mut self) { pub fn register<S: StepBody + Default + 'static>(&mut self) {
let key = std::any::type_name::<S>().to_string(); let key = std::any::type_name::<S>().to_string();
self.factories.insert(key, Box::new(|| Box::new(S::default()))); self.factories
.insert(key, Box::new(|| Box::new(S::default())));
} }
/// Register a step factory with an explicit key and factory function. /// Register a step factory with an explicit key and factory function.

View File

@@ -119,12 +119,12 @@ impl WorkflowExecutor {
host_context: Option<&dyn crate::traits::HostContext>, host_context: Option<&dyn crate::traits::HostContext>,
) -> Result<()> { ) -> Result<()> {
// 2. Load workflow instance. // 2. Load workflow instance.
let mut workflow = self let mut workflow = self.persistence.get_workflow_instance(workflow_id).await?;
.persistence
.get_workflow_instance(workflow_id)
.await?;
tracing::Span::current().record("workflow.definition_id", workflow.workflow_definition_id.as_str()); tracing::Span::current().record(
"workflow.definition_id",
workflow.workflow_definition_id.as_str(),
);
if workflow.status != WorkflowStatus::Runnable { if workflow.status != WorkflowStatus::Runnable {
debug!(workflow_id, status = ?workflow.status, "Workflow not runnable, skipping"); debug!(workflow_id, status = ?workflow.status, "Workflow not runnable, skipping");
@@ -179,15 +179,15 @@ impl WorkflowExecutor {
// Activate next step via outcomes (same as Complete). // Activate next step via outcomes (same as Complete).
let next_step_id = step.outcomes.first().map(|o| o.next_step); let next_step_id = step.outcomes.first().map(|o| o.next_step);
if let Some(next_id) = next_step_id { if let Some(next_id) = next_step_id {
let mut next_pointer = let mut next_pointer = crate::models::ExecutionPointer::new(next_id);
crate::models::ExecutionPointer::new(next_id); next_pointer.step_name = definition
next_pointer.step_name = definition.steps.iter() .steps
.iter()
.find(|s| s.id == next_id) .find(|s| s.id == next_id)
.and_then(|s| s.name.clone()); .and_then(|s| s.name.clone());
next_pointer.predecessor_id = next_pointer.predecessor_id =
Some(workflow.execution_pointers[idx].id.clone()); Some(workflow.execution_pointers[idx].id.clone());
next_pointer.scope = next_pointer.scope = workflow.execution_pointers[idx].scope.clone();
workflow.execution_pointers[idx].scope.clone();
workflow.execution_pointers.push(next_pointer); workflow.execution_pointers.push(next_pointer);
} }
@@ -208,12 +208,12 @@ impl WorkflowExecutor {
); );
// b. Resolve the step body. // b. Resolve the step body.
let mut step_body = step_registry let mut step_body = step_registry.resolve(&step.step_type).ok_or_else(|| {
.resolve(&step.step_type) WfeError::StepExecution(format!(
.ok_or_else(|| WfeError::StepExecution(format!(
"Step type not found in registry: {}", "Step type not found in registry: {}",
step.step_type step.step_type
)))?; ))
})?;
// Mark pointer as running before building context. // Mark pointer as running before building context.
if workflow.execution_pointers[idx].start_time.is_none() { if workflow.execution_pointers[idx].start_time.is_none() {
@@ -229,7 +229,8 @@ impl WorkflowExecutor {
step_id, step_id,
step_name: step.name.clone(), step_name: step.name.clone(),
}, },
)).await; ))
.await;
// c. Build StepExecutionContext (borrows workflow immutably). // c. Build StepExecutionContext (borrows workflow immutably).
let cancellation_token = tokio_util::sync::CancellationToken::new(); let cancellation_token = tokio_util::sync::CancellationToken::new();
@@ -277,19 +278,15 @@ impl WorkflowExecutor {
step_id, step_id,
step_name: step.name.clone(), step_name: step.name.clone(),
}, },
)).await; ))
.await;
// e. Process the ExecutionResult. // e. Process the ExecutionResult.
// Extract workflow_id before mutable borrow. // Extract workflow_id before mutable borrow.
let wf_id = workflow.id.clone(); let wf_id = workflow.id.clone();
let process_result = { let process_result = {
let pointer = &mut workflow.execution_pointers[idx]; let pointer = &mut workflow.execution_pointers[idx];
result_processor::process_result( result_processor::process_result(&result, pointer, definition, &wf_id)
&result,
pointer,
definition,
&wf_id,
)
}; };
all_subscriptions.extend(process_result.subscriptions); all_subscriptions.extend(process_result.subscriptions);
@@ -320,7 +317,8 @@ impl WorkflowExecutor {
crate::models::LifecycleEventType::Error { crate::models::LifecycleEventType::Error {
message: error_msg.clone(), message: error_msg.clone(),
}, },
)).await; ))
.await;
let pointer_id = workflow.execution_pointers[idx].id.clone(); let pointer_id = workflow.execution_pointers[idx].id.clone();
execution_errors.push(ExecutionError::new( execution_errors.push(ExecutionError::new(
@@ -331,11 +329,7 @@ impl WorkflowExecutor {
let handler_result = { let handler_result = {
let pointer = &mut workflow.execution_pointers[idx]; let pointer = &mut workflow.execution_pointers[idx];
error_handler::handle_error( error_handler::handle_error(&error_msg, pointer, definition)
&error_msg,
pointer,
definition,
)
}; };
// Apply workflow-level status changes from error handler. // Apply workflow-level status changes from error handler.
@@ -348,7 +342,8 @@ impl WorkflowExecutor {
&workflow.workflow_definition_id, &workflow.workflow_definition_id,
workflow.version, workflow.version,
crate::models::LifecycleEventType::Terminated, crate::models::LifecycleEventType::Terminated,
)).await; ))
.await;
} }
} }
@@ -382,7 +377,8 @@ impl WorkflowExecutor {
&workflow.workflow_definition_id, &workflow.workflow_definition_id,
workflow.version, workflow.version,
crate::models::LifecycleEventType::Completed, crate::models::LifecycleEventType::Completed,
)).await; ))
.await;
// Publish completion event for SubWorkflow parents. // Publish completion event for SubWorkflow parents.
let completion_event = Event::new( let completion_event = Event::new(
@@ -427,9 +423,7 @@ impl WorkflowExecutor {
// Persist errors. // Persist errors.
if !execution_errors.is_empty() { if !execution_errors.is_empty() {
self.persistence self.persistence.persist_errors(&execution_errors).await?;
.persist_errors(&execution_errors)
.await?;
} }
// 8. Queue any follow-up work. // 8. Queue any follow-up work.
@@ -512,10 +506,7 @@ mod tests {
#[async_trait] #[async_trait]
impl StepBody for PassStep { impl StepBody for PassStep {
async fn run( async fn run(&mut self, _ctx: &StepExecutionContext<'_>) -> crate::Result<ExecutionResult> {
&mut self,
_ctx: &StepExecutionContext<'_>,
) -> crate::Result<ExecutionResult> {
Ok(ExecutionResult::next()) Ok(ExecutionResult::next())
} }
} }
@@ -525,10 +516,7 @@ mod tests {
#[async_trait] #[async_trait]
impl StepBody for OutcomeStep { impl StepBody for OutcomeStep {
async fn run( async fn run(&mut self, _ctx: &StepExecutionContext<'_>) -> crate::Result<ExecutionResult> {
&mut self,
_ctx: &StepExecutionContext<'_>,
) -> crate::Result<ExecutionResult> {
Ok(ExecutionResult::outcome(serde_json::json!("yes"))) Ok(ExecutionResult::outcome(serde_json::json!("yes")))
} }
} }
@@ -538,10 +526,7 @@ mod tests {
#[async_trait] #[async_trait]
impl StepBody for PersistStep { impl StepBody for PersistStep {
async fn run( async fn run(&mut self, _ctx: &StepExecutionContext<'_>) -> crate::Result<ExecutionResult> {
&mut self,
_ctx: &StepExecutionContext<'_>,
) -> crate::Result<ExecutionResult> {
Ok(ExecutionResult::persist(serde_json::json!({"count": 1}))) Ok(ExecutionResult::persist(serde_json::json!({"count": 1})))
} }
} }
@@ -551,10 +536,7 @@ mod tests {
#[async_trait] #[async_trait]
impl StepBody for SleepStep { impl StepBody for SleepStep {
async fn run( async fn run(&mut self, _ctx: &StepExecutionContext<'_>) -> crate::Result<ExecutionResult> {
&mut self,
_ctx: &StepExecutionContext<'_>,
) -> crate::Result<ExecutionResult> {
Ok(ExecutionResult::sleep(Duration::from_secs(30), None)) Ok(ExecutionResult::sleep(Duration::from_secs(30), None))
} }
} }
@@ -564,10 +546,7 @@ mod tests {
#[async_trait] #[async_trait]
impl StepBody for WaitEventStep { impl StepBody for WaitEventStep {
async fn run( async fn run(&mut self, _ctx: &StepExecutionContext<'_>) -> crate::Result<ExecutionResult> {
&mut self,
_ctx: &StepExecutionContext<'_>,
) -> crate::Result<ExecutionResult> {
Ok(ExecutionResult::wait_for_event( Ok(ExecutionResult::wait_for_event(
"order.completed", "order.completed",
"order-123", "order-123",
@@ -581,10 +560,7 @@ mod tests {
#[async_trait] #[async_trait]
impl StepBody for EventResumeStep { impl StepBody for EventResumeStep {
async fn run( async fn run(&mut self, ctx: &StepExecutionContext<'_>) -> crate::Result<ExecutionResult> {
&mut self,
ctx: &StepExecutionContext<'_>,
) -> crate::Result<ExecutionResult> {
if ctx.execution_pointer.event_published { if ctx.execution_pointer.event_published {
Ok(ExecutionResult::next()) Ok(ExecutionResult::next())
} else { } else {
@@ -602,10 +578,7 @@ mod tests {
#[async_trait] #[async_trait]
impl StepBody for BranchStep { impl StepBody for BranchStep {
async fn run( async fn run(&mut self, _ctx: &StepExecutionContext<'_>) -> crate::Result<ExecutionResult> {
&mut self,
_ctx: &StepExecutionContext<'_>,
) -> crate::Result<ExecutionResult> {
Ok(ExecutionResult::branch( Ok(ExecutionResult::branch(
vec![ vec![
serde_json::json!(1), serde_json::json!(1),
@@ -622,10 +595,7 @@ mod tests {
#[async_trait] #[async_trait]
impl StepBody for FailStep { impl StepBody for FailStep {
async fn run( async fn run(&mut self, _ctx: &StepExecutionContext<'_>) -> crate::Result<ExecutionResult> {
&mut self,
_ctx: &StepExecutionContext<'_>,
) -> crate::Result<ExecutionResult> {
Err(WfeError::StepExecution("step failed".into())) Err(WfeError::StepExecution("step failed".into()))
} }
} }
@@ -635,10 +605,7 @@ mod tests {
#[async_trait] #[async_trait]
impl StepBody for CompensateStep { impl StepBody for CompensateStep {
async fn run( async fn run(&mut self, _ctx: &StepExecutionContext<'_>) -> crate::Result<ExecutionResult> {
&mut self,
_ctx: &StepExecutionContext<'_>,
) -> crate::Result<ExecutionResult> {
Ok(ExecutionResult::next()) Ok(ExecutionResult::next())
} }
} }
@@ -680,7 +647,8 @@ mod tests {
registry.register::<PassStep>(); registry.register::<PassStep>();
let mut def = WorkflowDefinition::new("test", 1); let mut def = WorkflowDefinition::new("test", 1);
def.steps.push(WorkflowStep::new(0, step_type::<PassStep>())); def.steps
.push(WorkflowStep::new(0, step_type::<PassStep>()));
let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({})); let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({}));
let pointer = ExecutionPointer::new(0); let pointer = ExecutionPointer::new(0);
@@ -688,11 +656,20 @@ mod tests {
persistence.create_new_workflow(&instance).await.unwrap(); persistence.create_new_workflow(&instance).await.unwrap();
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
.execute(&instance.id, &def, &registry, None)
.await
.unwrap();
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap(); let updated = persistence
.get_workflow_instance(&instance.id)
.await
.unwrap();
assert_eq!(updated.status, WorkflowStatus::Complete); assert_eq!(updated.status, WorkflowStatus::Complete);
assert_eq!(updated.execution_pointers[0].status, PointerStatus::Complete); assert_eq!(
updated.execution_pointers[0].status,
PointerStatus::Complete
);
assert!(updated.complete_time.is_some()); assert!(updated.complete_time.is_some());
} }
@@ -712,27 +689,46 @@ mod tests {
value: None, value: None,
}); });
def.steps.push(step0); def.steps.push(step0);
def.steps.push(WorkflowStep::new(1, step_type::<PassStep>())); def.steps
.push(WorkflowStep::new(1, step_type::<PassStep>()));
let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({})); let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({}));
instance.execution_pointers.push(ExecutionPointer::new(0)); instance.execution_pointers.push(ExecutionPointer::new(0));
persistence.create_new_workflow(&instance).await.unwrap(); persistence.create_new_workflow(&instance).await.unwrap();
// First execution: step 0 completes, step 1 pointer created. // First execution: step 0 completes, step 1 pointer created.
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
.execute(&instance.id, &def, &registry, None)
.await
.unwrap();
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap(); let updated = persistence
.get_workflow_instance(&instance.id)
.await
.unwrap();
assert_eq!(updated.execution_pointers.len(), 2); assert_eq!(updated.execution_pointers.len(), 2);
assert_eq!(updated.execution_pointers[0].status, PointerStatus::Complete); assert_eq!(
updated.execution_pointers[0].status,
PointerStatus::Complete
);
// Step 1 pointer should be active and pending. // Step 1 pointer should be active and pending.
assert_eq!(updated.execution_pointers[1].step_id, 1); assert_eq!(updated.execution_pointers[1].step_id, 1);
// Second execution: step 1 completes. // Second execution: step 1 completes.
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
.execute(&instance.id, &def, &registry, None)
.await
.unwrap();
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap(); let updated = persistence
.get_workflow_instance(&instance.id)
.await
.unwrap();
assert_eq!(updated.status, WorkflowStatus::Complete); assert_eq!(updated.status, WorkflowStatus::Complete);
assert_eq!(updated.execution_pointers[1].status, PointerStatus::Complete); assert_eq!(
updated.execution_pointers[1].status,
PointerStatus::Complete
);
} }
#[tokio::test] #[tokio::test]
@@ -745,9 +741,17 @@ mod tests {
let mut def = WorkflowDefinition::new("test", 1); let mut def = WorkflowDefinition::new("test", 1);
let mut s0 = WorkflowStep::new(0, step_type::<PassStep>()); let mut s0 = WorkflowStep::new(0, step_type::<PassStep>());
s0.outcomes.push(StepOutcome { next_step: 1, label: None, value: None }); s0.outcomes.push(StepOutcome {
next_step: 1,
label: None,
value: None,
});
let mut s1 = WorkflowStep::new(1, step_type::<PassStep>()); let mut s1 = WorkflowStep::new(1, step_type::<PassStep>());
s1.outcomes.push(StepOutcome { next_step: 2, label: None, value: None }); s1.outcomes.push(StepOutcome {
next_step: 2,
label: None,
value: None,
});
let s2 = WorkflowStep::new(2, step_type::<PassStep>()); let s2 = WorkflowStep::new(2, step_type::<PassStep>());
def.steps.push(s0); def.steps.push(s0);
def.steps.push(s1); def.steps.push(s1);
@@ -759,10 +763,16 @@ mod tests {
// Execute three times for three steps. // Execute three times for three steps.
for _ in 0..3 { for _ in 0..3 {
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
.execute(&instance.id, &def, &registry, None)
.await
.unwrap();
} }
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap(); let updated = persistence
.get_workflow_instance(&instance.id)
.await
.unwrap();
assert_eq!(updated.status, WorkflowStatus::Complete); assert_eq!(updated.status, WorkflowStatus::Complete);
assert_eq!(updated.execution_pointers.len(), 3); assert_eq!(updated.execution_pointers.len(), 3);
for p in &updated.execution_pointers { for p in &updated.execution_pointers {
@@ -792,16 +802,24 @@ mod tests {
value: Some(serde_json::json!("yes")), value: Some(serde_json::json!("yes")),
}); });
def.steps.push(s0); def.steps.push(s0);
def.steps.push(WorkflowStep::new(1, step_type::<PassStep>())); def.steps
def.steps.push(WorkflowStep::new(2, step_type::<PassStep>())); .push(WorkflowStep::new(1, step_type::<PassStep>()));
def.steps
.push(WorkflowStep::new(2, step_type::<PassStep>()));
let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({})); let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({}));
instance.execution_pointers.push(ExecutionPointer::new(0)); instance.execution_pointers.push(ExecutionPointer::new(0));
persistence.create_new_workflow(&instance).await.unwrap(); persistence.create_new_workflow(&instance).await.unwrap();
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
.execute(&instance.id, &def, &registry, None)
.await
.unwrap();
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap(); let updated = persistence
.get_workflow_instance(&instance.id)
.await
.unwrap();
assert_eq!(updated.execution_pointers.len(), 2); assert_eq!(updated.execution_pointers.len(), 2);
// Should route to step 2 (the "yes" branch). // Should route to step 2 (the "yes" branch).
assert_eq!(updated.execution_pointers[1].step_id, 2); assert_eq!(updated.execution_pointers[1].step_id, 2);
@@ -816,15 +834,22 @@ mod tests {
registry.register::<PersistStep>(); registry.register::<PersistStep>();
let mut def = WorkflowDefinition::new("test", 1); let mut def = WorkflowDefinition::new("test", 1);
def.steps.push(WorkflowStep::new(0, step_type::<PersistStep>())); def.steps
.push(WorkflowStep::new(0, step_type::<PersistStep>()));
let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({})); let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({}));
instance.execution_pointers.push(ExecutionPointer::new(0)); instance.execution_pointers.push(ExecutionPointer::new(0));
persistence.create_new_workflow(&instance).await.unwrap(); persistence.create_new_workflow(&instance).await.unwrap();
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
.execute(&instance.id, &def, &registry, None)
.await
.unwrap();
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap(); let updated = persistence
.get_workflow_instance(&instance.id)
.await
.unwrap();
assert_eq!(updated.status, WorkflowStatus::Runnable); assert_eq!(updated.status, WorkflowStatus::Runnable);
assert!(updated.execution_pointers[0].active); assert!(updated.execution_pointers[0].active);
assert_eq!( assert_eq!(
@@ -842,16 +867,26 @@ mod tests {
registry.register::<SleepStep>(); registry.register::<SleepStep>();
let mut def = WorkflowDefinition::new("test", 1); let mut def = WorkflowDefinition::new("test", 1);
def.steps.push(WorkflowStep::new(0, step_type::<SleepStep>())); def.steps
.push(WorkflowStep::new(0, step_type::<SleepStep>()));
let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({})); let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({}));
instance.execution_pointers.push(ExecutionPointer::new(0)); instance.execution_pointers.push(ExecutionPointer::new(0));
persistence.create_new_workflow(&instance).await.unwrap(); persistence.create_new_workflow(&instance).await.unwrap();
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
.execute(&instance.id, &def, &registry, None)
.await
.unwrap();
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap(); let updated = persistence
assert_eq!(updated.execution_pointers[0].status, PointerStatus::Sleeping); .get_workflow_instance(&instance.id)
.await
.unwrap();
assert_eq!(
updated.execution_pointers[0].status,
PointerStatus::Sleeping
);
assert!(updated.execution_pointers[0].sleep_until.is_some()); assert!(updated.execution_pointers[0].sleep_until.is_some());
assert!(updated.execution_pointers[0].active); assert!(updated.execution_pointers[0].active);
} }
@@ -865,15 +900,22 @@ mod tests {
registry.register::<WaitEventStep>(); registry.register::<WaitEventStep>();
let mut def = WorkflowDefinition::new("test", 1); let mut def = WorkflowDefinition::new("test", 1);
def.steps.push(WorkflowStep::new(0, step_type::<WaitEventStep>())); def.steps
.push(WorkflowStep::new(0, step_type::<WaitEventStep>()));
let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({})); let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({}));
instance.execution_pointers.push(ExecutionPointer::new(0)); instance.execution_pointers.push(ExecutionPointer::new(0));
persistence.create_new_workflow(&instance).await.unwrap(); persistence.create_new_workflow(&instance).await.unwrap();
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
.execute(&instance.id, &def, &registry, None)
.await
.unwrap();
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap(); let updated = persistence
.get_workflow_instance(&instance.id)
.await
.unwrap();
assert_eq!( assert_eq!(
updated.execution_pointers[0].status, updated.execution_pointers[0].status,
PointerStatus::WaitingForEvent PointerStatus::WaitingForEvent
@@ -899,7 +941,8 @@ mod tests {
registry.register::<EventResumeStep>(); registry.register::<EventResumeStep>();
let mut def = WorkflowDefinition::new("test", 1); let mut def = WorkflowDefinition::new("test", 1);
def.steps.push(WorkflowStep::new(0, step_type::<EventResumeStep>())); def.steps
.push(WorkflowStep::new(0, step_type::<EventResumeStep>()));
let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({})); let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({}));
let mut pointer = ExecutionPointer::new(0); let mut pointer = ExecutionPointer::new(0);
@@ -911,10 +954,19 @@ mod tests {
instance.execution_pointers.push(pointer); instance.execution_pointers.push(pointer);
persistence.create_new_workflow(&instance).await.unwrap(); persistence.create_new_workflow(&instance).await.unwrap();
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
.execute(&instance.id, &def, &registry, None)
.await
.unwrap();
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap(); let updated = persistence
assert_eq!(updated.execution_pointers[0].status, PointerStatus::Complete); .get_workflow_instance(&instance.id)
.await
.unwrap();
assert_eq!(
updated.execution_pointers[0].status,
PointerStatus::Complete
);
assert_eq!(updated.status, WorkflowStatus::Complete); assert_eq!(updated.status, WorkflowStatus::Complete);
} }
@@ -931,15 +983,22 @@ mod tests {
let mut s0 = WorkflowStep::new(0, step_type::<BranchStep>()); let mut s0 = WorkflowStep::new(0, step_type::<BranchStep>());
s0.children.push(1); s0.children.push(1);
def.steps.push(s0); def.steps.push(s0);
def.steps.push(WorkflowStep::new(1, step_type::<PassStep>())); def.steps
.push(WorkflowStep::new(1, step_type::<PassStep>()));
let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({})); let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({}));
instance.execution_pointers.push(ExecutionPointer::new(0)); instance.execution_pointers.push(ExecutionPointer::new(0));
persistence.create_new_workflow(&instance).await.unwrap(); persistence.create_new_workflow(&instance).await.unwrap();
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
.execute(&instance.id, &def, &registry, None)
.await
.unwrap();
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap(); let updated = persistence
.get_workflow_instance(&instance.id)
.await
.unwrap();
// 1 original + 3 children. // 1 original + 3 children.
assert_eq!(updated.execution_pointers.len(), 4); assert_eq!(updated.execution_pointers.len(), 4);
// Children should have scope containing the parent pointer id. // Children should have scope containing the parent pointer id.
@@ -973,11 +1032,20 @@ mod tests {
instance.execution_pointers.push(ExecutionPointer::new(0)); instance.execution_pointers.push(ExecutionPointer::new(0));
persistence.create_new_workflow(&instance).await.unwrap(); persistence.create_new_workflow(&instance).await.unwrap();
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
.execute(&instance.id, &def, &registry, None)
.await
.unwrap();
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap(); let updated = persistence
.get_workflow_instance(&instance.id)
.await
.unwrap();
assert_eq!(updated.execution_pointers[0].retry_count, 1); assert_eq!(updated.execution_pointers[0].retry_count, 1);
assert_eq!(updated.execution_pointers[0].status, PointerStatus::Sleeping); assert_eq!(
updated.execution_pointers[0].status,
PointerStatus::Sleeping
);
assert!(updated.execution_pointers[0].sleep_until.is_some()); assert!(updated.execution_pointers[0].sleep_until.is_some());
assert_eq!(updated.status, WorkflowStatus::Runnable); assert_eq!(updated.status, WorkflowStatus::Runnable);
} }
@@ -999,9 +1067,15 @@ mod tests {
instance.execution_pointers.push(ExecutionPointer::new(0)); instance.execution_pointers.push(ExecutionPointer::new(0));
persistence.create_new_workflow(&instance).await.unwrap(); persistence.create_new_workflow(&instance).await.unwrap();
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
.execute(&instance.id, &def, &registry, None)
.await
.unwrap();
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap(); let updated = persistence
.get_workflow_instance(&instance.id)
.await
.unwrap();
assert_eq!(updated.status, WorkflowStatus::Suspended); assert_eq!(updated.status, WorkflowStatus::Suspended);
assert_eq!(updated.execution_pointers[0].status, PointerStatus::Failed); assert_eq!(updated.execution_pointers[0].status, PointerStatus::Failed);
} }
@@ -1023,9 +1097,15 @@ mod tests {
instance.execution_pointers.push(ExecutionPointer::new(0)); instance.execution_pointers.push(ExecutionPointer::new(0));
persistence.create_new_workflow(&instance).await.unwrap(); persistence.create_new_workflow(&instance).await.unwrap();
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
.execute(&instance.id, &def, &registry, None)
.await
.unwrap();
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap(); let updated = persistence
.get_workflow_instance(&instance.id)
.await
.unwrap();
assert_eq!(updated.status, WorkflowStatus::Terminated); assert_eq!(updated.status, WorkflowStatus::Terminated);
assert_eq!(updated.execution_pointers[0].status, PointerStatus::Failed); assert_eq!(updated.execution_pointers[0].status, PointerStatus::Failed);
assert!(updated.complete_time.is_some()); assert!(updated.complete_time.is_some());
@@ -1045,15 +1125,22 @@ mod tests {
s0.error_behavior = Some(ErrorBehavior::Compensate); s0.error_behavior = Some(ErrorBehavior::Compensate);
s0.compensation_step_id = Some(1); s0.compensation_step_id = Some(1);
def.steps.push(s0); def.steps.push(s0);
def.steps.push(WorkflowStep::new(1, step_type::<CompensateStep>())); def.steps
.push(WorkflowStep::new(1, step_type::<CompensateStep>()));
let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({})); let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({}));
instance.execution_pointers.push(ExecutionPointer::new(0)); instance.execution_pointers.push(ExecutionPointer::new(0));
persistence.create_new_workflow(&instance).await.unwrap(); persistence.create_new_workflow(&instance).await.unwrap();
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
.execute(&instance.id, &def, &registry, None)
.await
.unwrap();
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap(); let updated = persistence
.get_workflow_instance(&instance.id)
.await
.unwrap();
assert_eq!(updated.execution_pointers[0].status, PointerStatus::Failed); assert_eq!(updated.execution_pointers[0].status, PointerStatus::Failed);
// Compensation pointer should be created. // Compensation pointer should be created.
assert_eq!(updated.execution_pointers.len(), 2); assert_eq!(updated.execution_pointers.len(), 2);
@@ -1070,8 +1157,10 @@ mod tests {
registry.register::<PassStep>(); registry.register::<PassStep>();
let mut def = WorkflowDefinition::new("test", 1); let mut def = WorkflowDefinition::new("test", 1);
def.steps.push(WorkflowStep::new(0, step_type::<PassStep>())); def.steps
def.steps.push(WorkflowStep::new(1, step_type::<PassStep>())); .push(WorkflowStep::new(0, step_type::<PassStep>()));
def.steps
.push(WorkflowStep::new(1, step_type::<PassStep>()));
let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({})); let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({}));
// Two independent active pointers. // Two independent active pointers.
@@ -1079,14 +1168,22 @@ mod tests {
instance.execution_pointers.push(ExecutionPointer::new(1)); instance.execution_pointers.push(ExecutionPointer::new(1));
persistence.create_new_workflow(&instance).await.unwrap(); persistence.create_new_workflow(&instance).await.unwrap();
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
.execute(&instance.id, &def, &registry, None)
.await
.unwrap();
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap(); let updated = persistence
.get_workflow_instance(&instance.id)
.await
.unwrap();
assert_eq!(updated.status, WorkflowStatus::Complete); assert_eq!(updated.status, WorkflowStatus::Complete);
assert!(updated assert!(
.execution_pointers updated
.iter() .execution_pointers
.all(|p| p.status == PointerStatus::Complete)); .iter()
.all(|p| p.status == PointerStatus::Complete)
);
} }
#[tokio::test] #[tokio::test]
@@ -1114,9 +1211,15 @@ mod tests {
persistence.create_new_workflow(&instance).await.unwrap(); persistence.create_new_workflow(&instance).await.unwrap();
// Should not error on a completed workflow. // Should not error on a completed workflow.
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
.execute(&instance.id, &def, &registry, None)
.await
.unwrap();
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap(); let updated = persistence
.get_workflow_instance(&instance.id)
.await
.unwrap();
assert_eq!(updated.status, WorkflowStatus::Complete); assert_eq!(updated.status, WorkflowStatus::Complete);
} }
@@ -1129,7 +1232,8 @@ mod tests {
registry.register::<SleepStep>(); registry.register::<SleepStep>();
let mut def = WorkflowDefinition::new("test", 1); let mut def = WorkflowDefinition::new("test", 1);
def.steps.push(WorkflowStep::new(0, step_type::<SleepStep>())); def.steps
.push(WorkflowStep::new(0, step_type::<SleepStep>()));
let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({})); let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({}));
let mut pointer = ExecutionPointer::new(0); let mut pointer = ExecutionPointer::new(0);
@@ -1139,11 +1243,20 @@ mod tests {
instance.execution_pointers.push(pointer); instance.execution_pointers.push(pointer);
persistence.create_new_workflow(&instance).await.unwrap(); persistence.create_new_workflow(&instance).await.unwrap();
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
.execute(&instance.id, &def, &registry, None)
.await
.unwrap();
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap(); let updated = persistence
.get_workflow_instance(&instance.id)
.await
.unwrap();
// Should still be sleeping since sleep_until is in the future. // Should still be sleeping since sleep_until is in the future.
assert_eq!(updated.execution_pointers[0].status, PointerStatus::Sleeping); assert_eq!(
updated.execution_pointers[0].status,
PointerStatus::Sleeping
);
} }
#[tokio::test] #[tokio::test]
@@ -1163,7 +1276,10 @@ mod tests {
instance.execution_pointers.push(ExecutionPointer::new(0)); instance.execution_pointers.push(ExecutionPointer::new(0));
persistence.create_new_workflow(&instance).await.unwrap(); persistence.create_new_workflow(&instance).await.unwrap();
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
.execute(&instance.id, &def, &registry, None)
.await
.unwrap();
let errors = persistence.get_errors().await; let errors = persistence.get_errors().await;
assert_eq!(errors.len(), 1); assert_eq!(errors.len(), 1);
@@ -1174,24 +1290,31 @@ mod tests {
async fn lifecycle_events_published() { async fn lifecycle_events_published() {
let (persistence, lock, queue) = create_providers(); let (persistence, lock, queue) = create_providers();
let lifecycle = Arc::new(InMemoryLifecyclePublisher::new()); let lifecycle = Arc::new(InMemoryLifecyclePublisher::new());
let executor = create_executor(persistence.clone(), lock, queue) let executor =
.with_lifecycle(lifecycle.clone()); create_executor(persistence.clone(), lock, queue).with_lifecycle(lifecycle.clone());
let mut registry = StepRegistry::new(); let mut registry = StepRegistry::new();
registry.register::<PassStep>(); registry.register::<PassStep>();
let mut def = WorkflowDefinition::new("test", 1); let mut def = WorkflowDefinition::new("test", 1);
def.steps.push(WorkflowStep::new(0, step_type::<PassStep>())); def.steps
.push(WorkflowStep::new(0, step_type::<PassStep>()));
let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({})); let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({}));
instance.execution_pointers.push(ExecutionPointer::new(0)); instance.execution_pointers.push(ExecutionPointer::new(0));
persistence.create_new_workflow(&instance).await.unwrap(); persistence.create_new_workflow(&instance).await.unwrap();
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
.execute(&instance.id, &def, &registry, None)
.await
.unwrap();
// Executor itself doesn't publish lifecycle events in the current implementation, // Executor itself doesn't publish lifecycle events in the current implementation,
// but the with_lifecycle builder works correctly. // but the with_lifecycle builder works correctly.
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap(); let updated = persistence
.get_workflow_instance(&instance.id)
.await
.unwrap();
assert_eq!(updated.status, WorkflowStatus::Complete); assert_eq!(updated.status, WorkflowStatus::Complete);
} }
@@ -1206,15 +1329,22 @@ mod tests {
let mut def = WorkflowDefinition::new("test", 1); let mut def = WorkflowDefinition::new("test", 1);
def.default_error_behavior = ErrorBehavior::Terminate; def.default_error_behavior = ErrorBehavior::Terminate;
// Step has no error_behavior override. // Step has no error_behavior override.
def.steps.push(WorkflowStep::new(0, step_type::<FailStep>())); def.steps
.push(WorkflowStep::new(0, step_type::<FailStep>()));
let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({})); let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({}));
instance.execution_pointers.push(ExecutionPointer::new(0)); instance.execution_pointers.push(ExecutionPointer::new(0));
persistence.create_new_workflow(&instance).await.unwrap(); persistence.create_new_workflow(&instance).await.unwrap();
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
.execute(&instance.id, &def, &registry, None)
.await
.unwrap();
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap(); let updated = persistence
.get_workflow_instance(&instance.id)
.await
.unwrap();
assert_eq!(updated.status, WorkflowStatus::Terminated); assert_eq!(updated.status, WorkflowStatus::Terminated);
} }
@@ -1227,15 +1357,22 @@ mod tests {
registry.register::<PassStep>(); registry.register::<PassStep>();
let mut def = WorkflowDefinition::new("test", 1); let mut def = WorkflowDefinition::new("test", 1);
def.steps.push(WorkflowStep::new(0, step_type::<PassStep>())); def.steps
.push(WorkflowStep::new(0, step_type::<PassStep>()));
let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({})); let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({}));
instance.execution_pointers.push(ExecutionPointer::new(0)); instance.execution_pointers.push(ExecutionPointer::new(0));
persistence.create_new_workflow(&instance).await.unwrap(); persistence.create_new_workflow(&instance).await.unwrap();
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
.execute(&instance.id, &def, &registry, None)
.await
.unwrap();
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap(); let updated = persistence
.get_workflow_instance(&instance.id)
.await
.unwrap();
assert!(updated.execution_pointers[0].start_time.is_some()); assert!(updated.execution_pointers[0].start_time.is_some());
assert!(updated.execution_pointers[0].end_time.is_some()); assert!(updated.execution_pointers[0].end_time.is_some());
} }
@@ -1257,15 +1394,22 @@ mod tests {
value: Some(serde_json::json!("yes")), value: Some(serde_json::json!("yes")),
}); });
def.steps.push(s0); def.steps.push(s0);
def.steps.push(WorkflowStep::new(1, step_type::<PassStep>())); def.steps
.push(WorkflowStep::new(1, step_type::<PassStep>()));
let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({})); let mut instance = WorkflowInstance::new("test", 1, serde_json::json!({}));
instance.execution_pointers.push(ExecutionPointer::new(0)); instance.execution_pointers.push(ExecutionPointer::new(0));
persistence.create_new_workflow(&instance).await.unwrap(); persistence.create_new_workflow(&instance).await.unwrap();
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
.execute(&instance.id, &def, &registry, None)
.await
.unwrap();
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap(); let updated = persistence
.get_workflow_instance(&instance.id)
.await
.unwrap();
assert_eq!( assert_eq!(
updated.execution_pointers[0].outcome, updated.execution_pointers[0].outcome,
Some(serde_json::json!("yes")) Some(serde_json::json!("yes"))
@@ -1318,15 +1462,33 @@ mod tests {
persistence.create_new_workflow(&instance).await.unwrap(); persistence.create_new_workflow(&instance).await.unwrap();
// First execution: fails, retry scheduled. // First execution: fails, retry scheduled.
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap(); .execute(&instance.id, &def, &registry, None)
.await
.unwrap();
let updated = persistence
.get_workflow_instance(&instance.id)
.await
.unwrap();
assert_eq!(updated.execution_pointers[0].retry_count, 1); assert_eq!(updated.execution_pointers[0].retry_count, 1);
assert_eq!(updated.execution_pointers[0].status, PointerStatus::Sleeping); assert_eq!(
updated.execution_pointers[0].status,
PointerStatus::Sleeping
);
// Second execution: succeeds (sleep_until is in the past with 0ms interval). // Second execution: succeeds (sleep_until is in the past with 0ms interval).
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap(); .execute(&instance.id, &def, &registry, None)
assert_eq!(updated.execution_pointers[0].status, PointerStatus::Complete); .await
.unwrap();
let updated = persistence
.get_workflow_instance(&instance.id)
.await
.unwrap();
assert_eq!(
updated.execution_pointers[0].status,
PointerStatus::Complete
);
assert_eq!(updated.status, WorkflowStatus::Complete); assert_eq!(updated.status, WorkflowStatus::Complete);
} }
@@ -1342,9 +1504,15 @@ mod tests {
// No execution pointers at all. // No execution pointers at all.
persistence.create_new_workflow(&instance).await.unwrap(); persistence.create_new_workflow(&instance).await.unwrap();
executor.execute(&instance.id, &def, &registry, None).await.unwrap(); executor
.execute(&instance.id, &def, &registry, None)
.await
.unwrap();
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap(); let updated = persistence
.get_workflow_instance(&instance.id)
.await
.unwrap();
assert_eq!(updated.status, WorkflowStatus::Runnable); assert_eq!(updated.status, WorkflowStatus::Runnable);
} }
} }

View File

@@ -136,13 +136,11 @@ mod tests {
#[test] #[test]
fn step_condition_any_serde_round_trip() { fn step_condition_any_serde_round_trip() {
let condition = StepCondition::Any(vec![ let condition = StepCondition::Any(vec![StepCondition::Comparison(FieldComparison {
StepCondition::Comparison(FieldComparison { field: ".x".to_string(),
field: ".x".to_string(), operator: ComparisonOp::IsNull,
operator: ComparisonOp::IsNull, value: None,
value: None, })]);
}),
]);
let json_str = serde_json::to_string(&condition).unwrap(); let json_str = serde_json::to_string(&condition).unwrap();
let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap(); let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap();
assert_eq!(condition, deserialized); assert_eq!(condition, deserialized);
@@ -150,13 +148,11 @@ mod tests {
#[test] #[test]
fn step_condition_none_serde_round_trip() { fn step_condition_none_serde_round_trip() {
let condition = StepCondition::None(vec![ let condition = StepCondition::None(vec![StepCondition::Comparison(FieldComparison {
StepCondition::Comparison(FieldComparison { field: ".err".to_string(),
field: ".err".to_string(), operator: ComparisonOp::IsNotNull,
operator: ComparisonOp::IsNotNull, value: None,
value: None, })]);
}),
]);
let json_str = serde_json::to_string(&condition).unwrap(); let json_str = serde_json::to_string(&condition).unwrap();
let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap(); let deserialized: StepCondition = serde_json::from_str(&json_str).unwrap();
assert_eq!(condition, deserialized); assert_eq!(condition, deserialized);

View File

@@ -75,7 +75,11 @@ mod tests {
#[test] #[test]
fn new_event_defaults() { fn new_event_defaults() {
let event = Event::new("order.created", "order-456", serde_json::json!({"amount": 100})); let event = Event::new(
"order.created",
"order-456",
serde_json::json!({"amount": 100}),
);
assert_eq!(event.event_name, "order.created"); assert_eq!(event.event_name, "order.created");
assert_eq!(event.event_key, "order-456"); assert_eq!(event.event_key, "order-456");
assert!(!event.is_processed); assert!(!event.is_processed);

View File

@@ -59,7 +59,10 @@ impl ExecutionResult {
} }
/// Create child branches for parallel/foreach execution. /// Create child branches for parallel/foreach execution.
pub fn branch(values: Vec<serde_json::Value>, persistence_data: Option<serde_json::Value>) -> Self { pub fn branch(
values: Vec<serde_json::Value>,
persistence_data: Option<serde_json::Value>,
) -> Self {
Self { Self {
proceed: false, proceed: false,
branch_values: Some(values), branch_values: Some(values),
@@ -137,7 +140,11 @@ mod tests {
#[test] #[test]
fn branch_creates_child_values() { fn branch_creates_child_values() {
let values = vec![serde_json::json!(1), serde_json::json!(2), serde_json::json!(3)]; let values = vec![
serde_json::json!(1),
serde_json::json!(2),
serde_json::json!(3),
];
let result = ExecutionResult::branch(values.clone(), None); let result = ExecutionResult::branch(values.clone(), None);
assert!(!result.proceed); assert!(!result.proceed);
assert_eq!(result.branch_values, Some(values)); assert_eq!(result.branch_values, Some(values));
@@ -181,7 +188,8 @@ mod tests {
#[test] #[test]
fn serde_round_trip() { fn serde_round_trip() {
let result = ExecutionResult::sleep(Duration::from_secs(30), Some(serde_json::json!({"x": 1}))); let result =
ExecutionResult::sleep(Duration::from_secs(30), Some(serde_json::json!({"x": 1})));
let json = serde_json::to_string(&result).unwrap(); let json = serde_json::to_string(&result).unwrap();
let deserialized: ExecutionResult = serde_json::from_str(&json).unwrap(); let deserialized: ExecutionResult = serde_json::from_str(&json).unwrap();
assert_eq!(result.proceed, deserialized.proceed); assert_eq!(result.proceed, deserialized.proceed);

View File

@@ -18,9 +18,17 @@ pub enum LifecycleEventType {
Suspended, Suspended,
Completed, Completed,
Terminated, Terminated,
Error { message: String }, Error {
StepStarted { step_id: usize, step_name: Option<String> }, message: String,
StepCompleted { step_id: usize, step_name: Option<String> }, },
StepStarted {
step_id: usize,
step_name: Option<String>,
},
StepCompleted {
step_id: usize,
step_name: Option<String>,
},
} }
impl LifecycleEvent { impl LifecycleEvent {
@@ -56,7 +64,10 @@ mod tests {
let event = LifecycleEvent::new("wf-1", "def-1", 1, LifecycleEventType::Started); let event = LifecycleEvent::new("wf-1", "def-1", 1, LifecycleEventType::Started);
let json = serde_json::to_string(&event).unwrap(); let json = serde_json::to_string(&event).unwrap();
let deserialized: LifecycleEvent = serde_json::from_str(&json).unwrap(); let deserialized: LifecycleEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event.workflow_instance_id, deserialized.workflow_instance_id); assert_eq!(
event.workflow_instance_id,
deserialized.workflow_instance_id
);
assert_eq!(event.event_type, deserialized.event_type); assert_eq!(event.event_type, deserialized.event_type);
} }

View File

@@ -1,7 +1,6 @@
pub mod condition; pub mod condition;
pub mod error_behavior; pub mod error_behavior;
pub mod event; pub mod event;
pub mod service;
pub mod execution_error; pub mod execution_error;
pub mod execution_pointer; pub mod execution_pointer;
pub mod execution_result; pub mod execution_result;
@@ -10,6 +9,7 @@ pub mod poll_config;
pub mod queue_type; pub mod queue_type;
pub mod scheduled_command; pub mod scheduled_command;
pub mod schema; pub mod schema;
pub mod service;
pub mod status; pub mod status;
pub mod workflow_definition; pub mod workflow_definition;
pub mod workflow_instance; pub mod workflow_instance;
@@ -25,9 +25,11 @@ pub use poll_config::{HttpMethod, PollCondition, PollEndpointConfig};
pub use queue_type::QueueType; pub use queue_type::QueueType;
pub use scheduled_command::{CommandName, ScheduledCommand}; pub use scheduled_command::{CommandName, ScheduledCommand};
pub use schema::{SchemaType, WorkflowSchema}; pub use schema::{SchemaType, WorkflowSchema};
pub use service::{
ReadinessCheck, ReadinessProbe, ServiceDefinition, ServiceEndpoint, ServicePort,
};
pub use status::{PointerStatus, WorkflowStatus}; pub use status::{PointerStatus, WorkflowStatus};
pub use workflow_definition::{StepOutcome, WorkflowDefinition, WorkflowStep}; pub use workflow_definition::{StepOutcome, WorkflowDefinition, WorkflowStep};
pub use service::{ReadinessCheck, ReadinessProbe, ServiceDefinition, ServiceEndpoint, ServicePort};
pub use workflow_instance::WorkflowInstance; pub use workflow_instance::WorkflowInstance;
/// Serde helper for `Option<Duration>` as milliseconds. /// Serde helper for `Option<Duration>` as milliseconds.

View File

@@ -63,9 +63,7 @@ pub fn parse_type(s: &str) -> crate::Result<SchemaType> {
"integer" => Ok(SchemaType::Integer), "integer" => Ok(SchemaType::Integer),
"bool" => Ok(SchemaType::Bool), "bool" => Ok(SchemaType::Bool),
"any" => Ok(SchemaType::Any), "any" => Ok(SchemaType::Any),
_ => Err(crate::WfeError::StepExecution(format!( _ => Err(crate::WfeError::StepExecution(format!("Unknown type: {s}"))),
"Unknown type: {s}"
))),
} }
} }
@@ -110,8 +108,7 @@ pub fn validate_value(value: &serde_json::Value, expected: &SchemaType) -> Resul
SchemaType::List(inner) => { SchemaType::List(inner) => {
if let Some(arr) = value.as_array() { if let Some(arr) = value.as_array() {
for (i, item) in arr.iter().enumerate() { for (i, item) in arr.iter().enumerate() {
validate_value(item, inner) validate_value(item, inner).map_err(|e| format!("list element [{i}]: {e}"))?;
.map_err(|e| format!("list element [{i}]: {e}"))?;
} }
Ok(()) Ok(())
} else { } else {
@@ -121,8 +118,7 @@ pub fn validate_value(value: &serde_json::Value, expected: &SchemaType) -> Resul
SchemaType::Map(inner) => { SchemaType::Map(inner) => {
if let Some(obj) = value.as_object() { if let Some(obj) = value.as_object() {
for (key, val) in obj { for (key, val) in obj {
validate_value(val, inner) validate_value(val, inner).map_err(|e| format!("map key \"{key}\": {e}"))?;
.map_err(|e| format!("map key \"{key}\": {e}"))?;
} }
Ok(()) Ok(())
} else { } else {

View File

@@ -130,7 +130,10 @@ mod tests {
let result = step.run(&ctx).await.unwrap(); let result = step.run(&ctx).await.unwrap();
assert!(!result.proceed); assert!(!result.proceed);
assert_eq!(result.branch_values, Some(vec![json!(1), json!(2), json!(3)])); assert_eq!(
result.branch_values,
Some(vec![json!(1), json!(2), json!(3)])
);
} }
#[tokio::test] #[tokio::test]

View File

@@ -60,7 +60,10 @@ mod tests {
let result = step.run(&ctx).await.unwrap(); let result = step.run(&ctx).await.unwrap();
assert!(!result.proceed); assert!(!result.proceed);
assert_eq!(result.branch_values, Some(vec![json!(null)])); assert_eq!(result.branch_values, Some(vec![json!(null)]));
assert_eq!(result.persistence_data, Some(json!({"children_active": true}))); assert_eq!(
result.persistence_data,
Some(json!({"children_active": true}))
);
} }
#[tokio::test] #[tokio::test]
@@ -116,6 +119,9 @@ mod tests {
let result = step.run(&ctx).await.unwrap(); let result = step.run(&ctx).await.unwrap();
assert!(!result.proceed); assert!(!result.proceed);
assert_eq!(result.persistence_data, Some(json!({"children_active": true}))); assert_eq!(
result.persistence_data,
Some(json!({"children_active": true}))
);
} }
} }

View File

@@ -45,7 +45,7 @@ mod test_helpers {
workflow, workflow,
cancellation_token: CancellationToken::new(), cancellation_token: CancellationToken::new(),
host_context: None, host_context: None,
log_sink: None, log_sink: None,
} }
} }

View File

@@ -1,7 +1,7 @@
use async_trait::async_trait; use async_trait::async_trait;
use crate::models::poll_config::PollEndpointConfig;
use crate::models::ExecutionResult; use crate::models::ExecutionResult;
use crate::models::poll_config::PollEndpointConfig;
use crate::traits::step::{StepBody, StepExecutionContext}; use crate::traits::step::{StepBody, StepExecutionContext};
/// A step that polls an external HTTP endpoint until a condition is met. /// A step that polls an external HTTP endpoint until a condition is met.
@@ -21,8 +21,8 @@ impl StepBody for PollEndpointStep {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::models::poll_config::{HttpMethod, PollCondition};
use crate::models::ExecutionPointer; use crate::models::ExecutionPointer;
use crate::models::poll_config::{HttpMethod, PollCondition};
use crate::primitives::test_helpers::*; use crate::primitives::test_helpers::*;
use std::collections::HashMap; use std::collections::HashMap;
use std::time::Duration; use std::time::Duration;

View File

@@ -85,7 +85,10 @@ mod tests {
let result = step.run(&ctx).await.unwrap(); let result = step.run(&ctx).await.unwrap();
assert!(!result.proceed); assert!(!result.proceed);
assert_eq!(result.sleep_for, Some(Duration::from_secs(10))); assert_eq!(result.sleep_for, Some(Duration::from_secs(10)));
assert_eq!(result.persistence_data, Some(json!({"children_active": true}))); assert_eq!(
result.persistence_data,
Some(json!({"children_active": true}))
);
} }
#[tokio::test] #[tokio::test]
@@ -130,6 +133,9 @@ mod tests {
let result = step.run(&ctx).await.unwrap(); let result = step.run(&ctx).await.unwrap();
assert!(!result.proceed); assert!(!result.proceed);
assert!(result.sleep_for.is_none()); assert!(result.sleep_for.is_none());
assert_eq!(result.persistence_data, Some(json!({"children_active": true}))); assert_eq!(
result.persistence_data,
Some(json!({"children_active": true}))
);
} }
} }

View File

@@ -89,7 +89,10 @@ mod tests {
let result = step.run(&ctx).await.unwrap(); let result = step.run(&ctx).await.unwrap();
assert!(!result.proceed); assert!(!result.proceed);
assert_eq!(result.branch_values, Some(vec![json!(null)])); assert_eq!(result.branch_values, Some(vec![json!(null)]));
assert_eq!(result.persistence_data, Some(json!({"children_active": true}))); assert_eq!(
result.persistence_data,
Some(json!({"children_active": true}))
);
} }
#[tokio::test] #[tokio::test]

View File

@@ -60,7 +60,10 @@ mod tests {
let result = step.run(&ctx).await.unwrap(); let result = step.run(&ctx).await.unwrap();
assert!(!result.proceed); assert!(!result.proceed);
assert_eq!(result.sleep_for, Some(Duration::from_secs(30))); assert_eq!(result.sleep_for, Some(Duration::from_secs(30)));
assert_eq!(result.persistence_data, Some(json!({"children_active": true}))); assert_eq!(
result.persistence_data,
Some(json!({"children_active": true}))
);
} }
#[tokio::test] #[tokio::test]
@@ -101,6 +104,9 @@ mod tests {
let ctx = make_context(&pointer, &wf_step, &workflow); let ctx = make_context(&pointer, &wf_step, &workflow);
let result = step.run(&ctx).await.unwrap(); let result = step.run(&ctx).await.unwrap();
assert!(!result.proceed); assert!(!result.proceed);
assert_eq!(result.persistence_data, Some(json!({"children_active": true}))); assert_eq!(
result.persistence_data,
Some(json!({"children_active": true}))
);
} }
} }

View File

@@ -61,7 +61,10 @@ mod tests {
let ctx = make_context(&pointer, &wf_step, &workflow); let ctx = make_context(&pointer, &wf_step, &workflow);
let result = step.run(&ctx).await.unwrap(); let result = step.run(&ctx).await.unwrap();
assert!(!result.proceed); assert!(!result.proceed);
assert_eq!(result.persistence_data, Some(json!({"children_active": true}))); assert_eq!(
result.persistence_data,
Some(json!({"children_active": true}))
);
} }
#[tokio::test] #[tokio::test]

View File

@@ -69,7 +69,10 @@ mod tests {
let result = step.run(&ctx).await.unwrap(); let result = step.run(&ctx).await.unwrap();
assert!(!result.proceed); assert!(!result.proceed);
assert_eq!(result.branch_values, Some(vec![json!(null)])); assert_eq!(result.branch_values, Some(vec![json!(null)]));
assert_eq!(result.persistence_data, Some(json!({"children_active": true}))); assert_eq!(
result.persistence_data,
Some(json!({"children_active": true}))
);
} }
#[tokio::test] #[tokio::test]
@@ -141,6 +144,9 @@ mod tests {
let result = step.run(&ctx).await.unwrap(); let result = step.run(&ctx).await.unwrap();
assert!(!result.proceed); assert!(!result.proceed);
assert!(result.branch_values.is_none()); assert!(result.branch_values.is_none());
assert_eq!(result.persistence_data, Some(json!({"children_active": true}))); assert_eq!(
result.persistence_data,
Some(json!({"children_active": true}))
);
} }
} }

View File

@@ -3,9 +3,9 @@ use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::Result;
use crate::models::LifecycleEvent; use crate::models::LifecycleEvent;
use crate::traits::LifecyclePublisher; use crate::traits::LifecyclePublisher;
use crate::Result;
/// An in-memory implementation of `LifecyclePublisher` for testing. /// An in-memory implementation of `LifecyclePublisher` for testing.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@@ -4,8 +4,8 @@ use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::traits::DistributedLockProvider;
use crate::Result; use crate::Result;
use crate::traits::DistributedLockProvider;
/// An in-memory implementation of `DistributedLockProvider` for testing. /// An in-memory implementation of `DistributedLockProvider` for testing.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@@ -4,9 +4,9 @@ use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::Result;
use crate::models::QueueType; use crate::models::QueueType;
use crate::traits::QueueProvider; use crate::traits::QueueProvider;
use crate::Result;
/// An in-memory implementation of `QueueProvider` for testing. /// An in-memory implementation of `QueueProvider` for testing.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@@ -17,18 +17,9 @@ macro_rules! queue_suite {
#[tokio::test] #[tokio::test]
async fn enqueue_dequeue_fifo() { async fn enqueue_dequeue_fifo() {
let provider = ($factory)().await; let provider = ($factory)().await;
provider provider.queue_work("a", QueueType::Workflow).await.unwrap();
.queue_work("a", QueueType::Workflow) provider.queue_work("b", QueueType::Workflow).await.unwrap();
.await provider.queue_work("c", QueueType::Workflow).await.unwrap();
.unwrap();
provider
.queue_work("b", QueueType::Workflow)
.await
.unwrap();
provider
.queue_work("c", QueueType::Workflow)
.await
.unwrap();
assert_eq!( assert_eq!(
provider provider
@@ -94,16 +85,20 @@ macro_rules! queue_suite {
); );
// Both should now be empty // Both should now be empty
assert!(provider assert!(
.dequeue_work(QueueType::Event) provider
.await .dequeue_work(QueueType::Event)
.unwrap() .await
.is_none()); .unwrap()
assert!(provider .is_none()
.dequeue_work(QueueType::Workflow) );
.await assert!(
.unwrap() provider
.is_none()); .dequeue_work(QueueType::Workflow)
.await
.unwrap()
.is_none()
);
} }
} }
}; };

View File

@@ -69,7 +69,7 @@ mod tests {
workflow: &instance, workflow: &instance,
cancellation_token: tokio_util::sync::CancellationToken::new(), cancellation_token: tokio_util::sync::CancellationToken::new(),
host_context: None, host_context: None,
log_sink: None, log_sink: None,
}; };
mw.pre_step(&ctx).await.unwrap(); mw.pre_step(&ctx).await.unwrap();
} }
@@ -89,7 +89,7 @@ mod tests {
workflow: &instance, workflow: &instance,
cancellation_token: tokio_util::sync::CancellationToken::new(), cancellation_token: tokio_util::sync::CancellationToken::new(),
host_context: None, host_context: None,
log_sink: None, log_sink: None,
}; };
let result = ExecutionResult::next(); let result = ExecutionResult::next();
mw.post_step(&ctx, &result).await.unwrap(); mw.post_step(&ctx, &result).await.unwrap();

View File

@@ -1,12 +1,12 @@
pub mod lifecycle; pub mod lifecycle;
pub mod lock; pub mod lock;
pub mod service;
pub mod log_sink; pub mod log_sink;
pub mod middleware; pub mod middleware;
pub mod persistence; pub mod persistence;
pub mod queue; pub mod queue;
pub mod registry; pub mod registry;
pub mod search; pub mod search;
pub mod service;
pub mod step; pub mod step;
pub use lifecycle::LifecyclePublisher; pub use lifecycle::LifecyclePublisher;

View File

@@ -1,6 +1,6 @@
use async_trait::async_trait; use async_trait::async_trait;
use serde::de::DeserializeOwned;
use serde::Serialize; use serde::Serialize;
use serde::de::DeserializeOwned;
use crate::models::{ExecutionPointer, ExecutionResult, WorkflowInstance, WorkflowStep}; use crate::models::{ExecutionPointer, ExecutionResult, WorkflowInstance, WorkflowStep};

View File

@@ -1,7 +1,7 @@
use std::time::Duration; use std::time::Duration;
use deno_core::op2;
use deno_core::OpState; use deno_core::OpState;
use deno_core::op2;
use wfe_core::builder::WorkflowBuilder; use wfe_core::builder::WorkflowBuilder;
use wfe_core::models::ErrorBehavior; use wfe_core::models::ErrorBehavior;

View File

@@ -1,5 +1,5 @@
use deno_core::op2;
use deno_core::OpState; use deno_core::OpState;
use deno_core::op2;
use crate::state::WfeState; use crate::state::WfeState;

View File

@@ -1,7 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use deno_core::op2;
use deno_core::OpState; use deno_core::OpState;
use deno_core::op2;
use crate::state::WfeState; use crate::state::WfeState;

View File

@@ -1,7 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use deno_core::op2;
use deno_core::OpState; use deno_core::OpState;
use deno_core::op2;
use crate::bridge::JsStepBody; use crate::bridge::JsStepBody;
use crate::state::WfeState; use crate::state::WfeState;
@@ -23,10 +23,9 @@ pub async fn op_register_step(
}; };
let counter = Arc::new(std::sync::atomic::AtomicU32::new(0)); let counter = Arc::new(std::sync::atomic::AtomicU32::new(0));
host.register_step_factory( host.register_step_factory(&step_type, move || {
&step_type, Box::new(JsStepBody::new(tx.clone(), counter.clone()))
move || Box::new(JsStepBody::new(tx.clone(), counter.clone())), })
)
.await; .await;
Ok(()) Ok(())

View File

@@ -1,5 +1,5 @@
use deno_core::op2;
use deno_core::OpState; use deno_core::OpState;
use deno_core::op2;
use crate::state::WfeState; use crate::state::WfeState;

View File

@@ -16,8 +16,7 @@ async fn run_js(code: &str) -> Result<(), Box<dyn std::error::Error>> {
/// Helper: run a JS module in a fresh wfe runtime and drive the event loop. /// Helper: run a JS module in a fresh wfe runtime and drive the event loop.
async fn run_module(code: &str) -> Result<(), Box<dyn std::error::Error>> { async fn run_module(code: &str) -> Result<(), Box<dyn std::error::Error>> {
let mut runtime = create_wfe_runtime(); let mut runtime = create_wfe_runtime();
let specifier = let specifier = deno_core::ModuleSpecifier::parse("ext:wfe-deno/test-module.js").unwrap();
deno_core::ModuleSpecifier::parse("ext:wfe-deno/test-module.js").unwrap();
let module_id = runtime let module_id = runtime
.load_main_es_module_from_code(&specifier, code.to_string()) .load_main_es_module_from_code(&specifier, code.to_string())
.await .await
@@ -27,8 +26,7 @@ async fn run_module(code: &str) -> Result<(), Box<dyn std::error::Error>> {
.run_event_loop(Default::default()) .run_event_loop(Default::default())
.await .await
.map_err(|e| format!("event loop error: {e}"))?; .map_err(|e| format!("event loop error: {e}"))?;
eval.await eval.await.map_err(|e| format!("module eval error: {e}"))?;
.map_err(|e| format!("module eval error: {e}"))?;
Ok(()) Ok(())
} }

View File

@@ -1,10 +1,10 @@
use futures::io::AsyncBufReadExt;
use futures::StreamExt; use futures::StreamExt;
use futures::io::AsyncBufReadExt;
use k8s_openapi::api::core::v1::Pod; use k8s_openapi::api::core::v1::Pod;
use kube::api::LogParams; use kube::api::LogParams;
use kube::{Api, Client}; use kube::{Api, Client};
use wfe_core::traits::log_sink::{LogChunk, LogSink, LogStreamType};
use wfe_core::WfeError; use wfe_core::WfeError;
use wfe_core::traits::log_sink::{LogChunk, LogSink, LogStreamType};
/// Stream logs from a pod container, optionally forwarding to a LogSink. /// Stream logs from a pod container, optionally forwarding to a LogSink.
/// ///
@@ -29,9 +29,7 @@ pub async fn stream_logs(
}; };
let stream = pods.log_stream(pod_name, &params).await.map_err(|e| { let stream = pods.log_stream(pod_name, &params).await.map_err(|e| {
WfeError::StepExecution(format!( WfeError::StepExecution(format!("failed to stream logs from pod '{pod_name}': {e}"))
"failed to stream logs from pod '{pod_name}': {e}"
))
})?; })?;
let mut stdout = String::new(); let mut stdout = String::new();

View File

@@ -16,7 +16,13 @@ pub fn namespace_name(prefix: &str, workflow_id: &str) -> String {
let sanitized: String = raw let sanitized: String = raw
.to_lowercase() .to_lowercase()
.chars() .chars()
.map(|c| if c.is_ascii_alphanumeric() || c == '-' { c } else { '-' }) .map(|c| {
if c.is_ascii_alphanumeric() || c == '-' {
c
} else {
'-'
}
})
.take(63) .take(63)
.collect(); .collect();
// Trim trailing hyphens // Trim trailing hyphens
@@ -55,9 +61,9 @@ pub async fn ensure_namespace(
..Default::default() ..Default::default()
}; };
api.create(&PostParams::default(), &ns) api.create(&PostParams::default(), &ns).await.map_err(|e| {
.await WfeError::StepExecution(format!("failed to create namespace '{name}': {e}"))
.map_err(|e| WfeError::StepExecution(format!("failed to create namespace '{name}': {e}")))?; })?;
Ok(()) Ok(())
} }
@@ -65,9 +71,9 @@ pub async fn ensure_namespace(
/// Delete a namespace and all resources within it. /// Delete a namespace and all resources within it.
pub async fn delete_namespace(client: &Client, name: &str) -> Result<(), WfeError> { pub async fn delete_namespace(client: &Client, name: &str) -> Result<(), WfeError> {
let api: Api<Namespace> = Api::all(client.clone()); let api: Api<Namespace> = Api::all(client.clone());
api.delete(name, &Default::default()) api.delete(name, &Default::default()).await.map_err(|e| {
.await WfeError::StepExecution(format!("failed to delete namespace '{name}': {e}"))
.map_err(|e| WfeError::StepExecution(format!("failed to delete namespace '{name}': {e}")))?; })?;
Ok(()) Ok(())
} }

View File

@@ -152,10 +152,12 @@ mod tests {
#[test] #[test]
fn build_output_data_json_value() { fn build_output_data_json_value() {
let parsed: HashMap<String, String> = let parsed: HashMap<String, String> = [
[("count".into(), "42".into()), ("flag".into(), "true".into())] ("count".into(), "42".into()),
.into_iter() ("flag".into(), "true".into()),
.collect(); ]
.into_iter()
.collect();
let data = build_output_data("s", "", "", 0, &parsed); let data = build_output_data("s", "", "", 0, &parsed);
// Numbers and booleans should be parsed as JSON, not strings. // Numbers and booleans should be parsed as JSON, not strings.
assert_eq!(data["count"], 42); assert_eq!(data["count"], 42);

View File

@@ -77,8 +77,16 @@ pub fn build_service_pod(svc: &ServiceDefinition, namespace: &str) -> Pod {
command, command,
args, args,
resources: Some(ResourceRequirements { resources: Some(ResourceRequirements {
limits: if limits.is_empty() { None } else { Some(limits) }, limits: if limits.is_empty() {
requests: if requests.is_empty() { None } else { Some(requests) }, None
} else {
Some(limits)
},
requests: if requests.is_empty() {
None
} else {
Some(requests)
},
..Default::default() ..Default::default()
}), }),
..Default::default() ..Default::default()
@@ -230,10 +238,7 @@ mod tests {
let ports = spec.ports.as_ref().unwrap(); let ports = spec.ports.as_ref().unwrap();
assert_eq!(ports.len(), 1); assert_eq!(ports.len(), 1);
assert_eq!(ports[0].port, 5432); assert_eq!(ports[0].port, 5432);
assert_eq!( assert_eq!(ports[0].target_port, Some(IntOrString::Int(5432)));
ports[0].target_port,
Some(IntOrString::Int(5432))
);
} }
#[test] #[test]
@@ -241,10 +246,7 @@ mod tests {
let svc = ServiceDefinition { let svc = ServiceDefinition {
name: "app".into(), name: "app".into(),
image: "myapp".into(), image: "myapp".into(),
ports: vec![ ports: vec![WfeServicePort::tcp(8080), WfeServicePort::tcp(8443)],
WfeServicePort::tcp(8080),
WfeServicePort::tcp(8443),
],
env: Default::default(), env: Default::default(),
readiness: None, readiness: None,
command: vec![], command: vec![],

View File

@@ -4,9 +4,9 @@ use async_trait::async_trait;
use k8s_openapi::api::core::v1::Pod; use k8s_openapi::api::core::v1::Pod;
use kube::api::PostParams; use kube::api::PostParams;
use kube::{Api, Client}; use kube::{Api, Client};
use wfe_core::WfeError;
use wfe_core::models::service::{ServiceDefinition, ServiceEndpoint}; use wfe_core::models::service::{ServiceDefinition, ServiceEndpoint};
use wfe_core::traits::ServiceProvider; use wfe_core::traits::ServiceProvider;
use wfe_core::WfeError;
use crate::config::ClusterConfig; use crate::config::ClusterConfig;
use crate::logs::wait_for_pod_running; use crate::logs::wait_for_pod_running;
@@ -77,11 +77,8 @@ impl ServiceProvider for KubernetesServiceProvider {
.map(|r| Duration::from_millis(r.timeout_ms)) .map(|r| Duration::from_millis(r.timeout_ms))
.unwrap_or(Duration::from_secs(120)); .unwrap_or(Duration::from_secs(120));
match tokio::time::timeout( match tokio::time::timeout(timeout, wait_for_pod_running(&self.client, &ns, &svc.name))
timeout, .await
wait_for_pod_running(&self.client, &ns, &svc.name),
)
.await
{ {
Ok(Ok(())) => {} Ok(Ok(())) => {}
Ok(Err(e)) => { Ok(Err(e)) => {

View File

@@ -6,9 +6,9 @@ use k8s_openapi::api::batch::v1::Job;
use k8s_openapi::api::core::v1::Pod; use k8s_openapi::api::core::v1::Pod;
use kube::api::{ListParams, PostParams}; use kube::api::{ListParams, PostParams};
use kube::{Api, Client}; use kube::{Api, Client};
use wfe_core::WfeError;
use wfe_core::models::ExecutionResult; use wfe_core::models::ExecutionResult;
use wfe_core::traits::step::{StepBody, StepExecutionContext}; use wfe_core::traits::step::{StepBody, StepExecutionContext};
use wfe_core::WfeError;
use crate::cleanup::delete_job; use crate::cleanup::delete_job;
use crate::config::{ClusterConfig, KubernetesStepConfig}; use crate::config::{ClusterConfig, KubernetesStepConfig};
@@ -86,13 +86,7 @@ impl StepBody for KubernetesStep {
let env_overrides = extract_workflow_env(&context.workflow.data); let env_overrides = extract_workflow_env(&context.workflow.data);
// 4. Build Job manifest. // 4. Build Job manifest.
let job_manifest = build_job( let job_manifest = build_job(&self.config, &step_name, &ns, &env_overrides, &self.cluster);
&self.config,
&step_name,
&ns,
&env_overrides,
&self.cluster,
);
let job_name = job_manifest let job_name = job_manifest
.metadata .metadata
.name .name
@@ -111,7 +105,15 @@ impl StepBody for KubernetesStep {
let result = if let Some(timeout_ms) = self.config.timeout_ms { let result = if let Some(timeout_ms) = self.config.timeout_ms {
match tokio::time::timeout( match tokio::time::timeout(
Duration::from_millis(timeout_ms), Duration::from_millis(timeout_ms),
self.execute_job(&client, &ns, &job_name, &step_name, definition_id, workflow_id, context), self.execute_job(
&client,
&ns,
&job_name,
&step_name,
definition_id,
workflow_id,
context,
),
) )
.await .await
{ {
@@ -125,8 +127,16 @@ impl StepBody for KubernetesStep {
} }
} }
} else { } else {
self.execute_job(&client, &ns, &job_name, &step_name, definition_id, workflow_id, context) self.execute_job(
.await &client,
&ns,
&job_name,
&step_name,
definition_id,
workflow_id,
context,
)
.await
}; };
// Always attempt cleanup. // Always attempt cleanup.
@@ -205,9 +215,7 @@ async fn wait_for_job_pod(
.list(&ListParams::default().labels(&selector)) .list(&ListParams::default().labels(&selector))
.await .await
.map_err(|e| { .map_err(|e| {
WfeError::StepExecution(format!( WfeError::StepExecution(format!("failed to list pods for job '{job_name}': {e}"))
"failed to list pods for job '{job_name}': {e}"
))
})?; })?;
if let Some(pod) = pod_list.items.first() { if let Some(pod) = pod_list.items.first() {
@@ -236,9 +244,10 @@ async fn wait_for_job_completion(
// Poll Job status. // Poll Job status.
for _ in 0..600 { for _ in 0..600 {
let job = jobs.get(job_name).await.map_err(|e| { let job = jobs
WfeError::StepExecution(format!("failed to get job '{job_name}': {e}")) .get(job_name)
})?; .await
.map_err(|e| WfeError::StepExecution(format!("failed to get job '{job_name}': {e}")))?;
if let Some(status) = &job.status { if let Some(status) = &job.status {
if let Some(conditions) = &status.conditions { if let Some(conditions) = &status.conditions {
@@ -352,9 +361,6 @@ mod tests {
let data = serde_json::json!({"config": {"nested": true}}); let data = serde_json::json!({"config": {"nested": true}});
let env = extract_workflow_env(&data); let env = extract_workflow_env(&data);
// Nested object serialized as JSON string. // Nested object serialized as JSON string.
assert_eq!( assert_eq!(env.get("CONFIG"), Some(&r#"{"nested":true}"#.to_string()));
env.get("CONFIG"),
Some(&r#"{"nested":true}"#.to_string())
);
} }
} }

View File

@@ -1,13 +1,13 @@
use std::collections::HashMap; use std::collections::HashMap;
use wfe_core::models::service::{ReadinessCheck, ReadinessProbe, ServiceDefinition, ServicePort}; use wfe_core::models::service::{ReadinessCheck, ReadinessProbe, ServiceDefinition, ServicePort};
use wfe_core::traits::step::StepBody;
use wfe_core::traits::ServiceProvider; use wfe_core::traits::ServiceProvider;
use wfe_kubernetes::config::{ClusterConfig, KubernetesStepConfig}; use wfe_core::traits::step::StepBody;
use wfe_kubernetes::namespace; use wfe_kubernetes::KubernetesServiceProvider;
use wfe_kubernetes::cleanup; use wfe_kubernetes::cleanup;
use wfe_kubernetes::client; use wfe_kubernetes::client;
use wfe_kubernetes::KubernetesServiceProvider; use wfe_kubernetes::config::{ClusterConfig, KubernetesStepConfig};
use wfe_kubernetes::namespace;
/// Path to the Lima sunbeam VM kubeconfig. /// Path to the Lima sunbeam VM kubeconfig.
fn kubeconfig_path() -> String { fn kubeconfig_path() -> String {
@@ -64,10 +64,14 @@ async fn namespace_create_and_delete() {
let client = client::create_client(&config).await.unwrap(); let client = client::create_client(&config).await.unwrap();
let ns = "wfe-test-ns-lifecycle"; let ns = "wfe-test-ns-lifecycle";
namespace::ensure_namespace(&client, ns, "test-wf").await.unwrap(); namespace::ensure_namespace(&client, ns, "test-wf")
.await
.unwrap();
// Idempotent — creating again should succeed. // Idempotent — creating again should succeed.
namespace::ensure_namespace(&client, ns, "test-wf").await.unwrap(); namespace::ensure_namespace(&client, ns, "test-wf")
.await
.unwrap();
namespace::delete_namespace(&client, ns).await.unwrap(); namespace::delete_namespace(&client, ns).await.unwrap();
} }
@@ -107,10 +111,12 @@ async fn run_echo_job() {
assert!(result.proceed); assert!(result.proceed);
let output = result.output_data.unwrap(); let output = result.output_data.unwrap();
assert!(output["echo-step.stdout"] assert!(
.as_str() output["echo-step.stdout"]
.unwrap() .as_str()
.contains("hello from k8s")); .unwrap()
.contains("hello from k8s")
);
assert_eq!(output["echo-step.exit_code"], 0); assert_eq!(output["echo-step.exit_code"], 0);
// Cleanup namespace. // Cleanup namespace.
@@ -132,8 +138,7 @@ async fn run_job_with_wfe_output() {
let mut step = let mut step =
wfe_kubernetes::KubernetesStep::new(step_cfg, config.clone(), k8s_client.clone()); wfe_kubernetes::KubernetesStep::new(step_cfg, config.clone(), k8s_client.clone());
let instance = let instance = wfe_core::models::WorkflowInstance::new("output-wf", 1, serde_json::json!({}));
wfe_core::models::WorkflowInstance::new("output-wf", 1, serde_json::json!({}));
let mut ws = wfe_core::models::WorkflowStep::new(0, "alpine-output"); let mut ws = wfe_core::models::WorkflowStep::new(0, "alpine-output");
ws.name = Some("output-step".into()); ws.name = Some("output-step".into());
let pointer = wfe_core::models::ExecutionPointer::new(0); let pointer = wfe_core::models::ExecutionPointer::new(0);
@@ -250,8 +255,7 @@ async fn run_job_with_timeout() {
let mut step = let mut step =
wfe_kubernetes::KubernetesStep::new(step_cfg, config.clone(), k8s_client.clone()); wfe_kubernetes::KubernetesStep::new(step_cfg, config.clone(), k8s_client.clone());
let instance = let instance = wfe_core::models::WorkflowInstance::new("timeout-wf", 1, serde_json::json!({}));
wfe_core::models::WorkflowInstance::new("timeout-wf", 1, serde_json::json!({}));
let mut ws = wfe_core::models::WorkflowStep::new(0, "alpine-timeout"); let mut ws = wfe_core::models::WorkflowStep::new(0, "alpine-timeout");
ws.name = Some("timeout-step".into()); ws.name = Some("timeout-step".into());
let pointer = wfe_core::models::ExecutionPointer::new(0); let pointer = wfe_core::models::ExecutionPointer::new(0);
@@ -484,7 +488,10 @@ async fn service_provider_provision_duplicate_name_fails() {
}; };
// First provision succeeds. // First provision succeeds.
let endpoints = provider.provision(workflow_id, &[svc.clone()]).await.unwrap(); let endpoints = provider
.provision(workflow_id, &[svc.clone()])
.await
.unwrap();
assert_eq!(endpoints.len(), 1); assert_eq!(endpoints.len(), 1);
// Second provision with same name should fail (pod already exists). // Second provision with same name should fail (pod already exists).
@@ -505,7 +512,9 @@ async fn service_provider_provision_service_object_conflict() {
let workflow_id = &unique_id("svc-conflict"); let workflow_id = &unique_id("svc-conflict");
let ns = namespace::namespace_name(&config.namespace_prefix, workflow_id); let ns = namespace::namespace_name(&config.namespace_prefix, workflow_id);
namespace::ensure_namespace(&k8s_client, &ns, workflow_id).await.unwrap(); namespace::ensure_namespace(&k8s_client, &ns, workflow_id)
.await
.unwrap();
// Pre-create just the K8s Service (not the pod). // Pre-create just the K8s Service (not the pod).
let svc_def = nginx_service(); let svc_def = nginx_service();

View File

@@ -80,7 +80,7 @@ impl SearchIndex for OpenSearchIndex {
.client .client
.indices() .indices()
.exists(opensearch::indices::IndicesExistsParts::Index(&[ .exists(opensearch::indices::IndicesExistsParts::Index(&[
&self.index_name, &self.index_name
])) ]))
.send() .send()
.await .await

View File

@@ -1,6 +1,6 @@
use chrono::Utc; use chrono::Utc;
use opensearch::http::transport::Transport;
use opensearch::OpenSearch; use opensearch::OpenSearch;
use opensearch::http::transport::Transport;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use serde_json::json; use serde_json::json;
use uuid::Uuid; use uuid::Uuid;
@@ -60,7 +60,7 @@ async fn cleanup(provider: &OpenSearchIndex) {
.client() .client()
.indices() .indices()
.delete(opensearch::indices::IndicesDeleteParts::Index(&[ .delete(opensearch::indices::IndicesDeleteParts::Index(&[
provider.index_name(), provider.index_name()
])) ]))
.send() .send()
.await; .await;
@@ -164,7 +164,10 @@ async fn index_multiple_and_paginate() {
refresh_index(&provider).await; refresh_index(&provider).await;
// Search all, but skip 2 and take 2 // Search all, but skip 2 and take 2
let page = provider.search("Paginated workflow", 2, 2, &[]).await.unwrap(); let page = provider
.search("Paginated workflow", 2, 2, &[])
.await
.unwrap();
assert_eq!(page.total, 5); assert_eq!(page.total, 5);
assert_eq!(page.data.len(), 2); assert_eq!(page.data.len(), 2);

View File

@@ -229,7 +229,10 @@ mod tests {
#[test] #[test]
fn subcommand_args_nextest_has_run() { fn subcommand_args_nextest_has_run() {
assert_eq!(CargoCommand::Nextest.subcommand_args(), vec!["nextest", "run"]); assert_eq!(
CargoCommand::Nextest.subcommand_args(),
vec!["nextest", "run"]
);
} }
#[test] #[test]
@@ -241,8 +244,14 @@ mod tests {
fn install_package_external_tools() { fn install_package_external_tools() {
assert_eq!(CargoCommand::Audit.install_package(), Some("cargo-audit")); assert_eq!(CargoCommand::Audit.install_package(), Some("cargo-audit"));
assert_eq!(CargoCommand::Deny.install_package(), Some("cargo-deny")); assert_eq!(CargoCommand::Deny.install_package(), Some("cargo-deny"));
assert_eq!(CargoCommand::Nextest.install_package(), Some("cargo-nextest")); assert_eq!(
assert_eq!(CargoCommand::LlvmCov.install_package(), Some("cargo-llvm-cov")); CargoCommand::Nextest.install_package(),
Some("cargo-nextest")
);
assert_eq!(
CargoCommand::LlvmCov.install_package(),
Some("cargo-llvm-cov")
);
} }
#[test] #[test]

View File

@@ -1,7 +1,7 @@
use async_trait::async_trait; use async_trait::async_trait;
use wfe_core::WfeError;
use wfe_core::models::ExecutionResult; use wfe_core::models::ExecutionResult;
use wfe_core::traits::step::{StepBody, StepExecutionContext}; use wfe_core::traits::step::{StepBody, StepExecutionContext};
use wfe_core::WfeError;
use crate::cargo::config::{CargoCommand, CargoConfig}; use crate::cargo::config::{CargoCommand, CargoConfig};
@@ -88,7 +88,10 @@ impl CargoStep {
/// Ensures an external cargo tool is installed before running it. /// Ensures an external cargo tool is installed before running it.
/// For built-in cargo subcommands, this is a no-op. /// For built-in cargo subcommands, this is a no-op.
async fn ensure_tool_available(&self) -> Result<(), WfeError> { async fn ensure_tool_available(&self) -> Result<(), WfeError> {
let (binary, package) = match (self.config.command.binary_name(), self.config.command.install_package()) { let (binary, package) = match (
self.config.command.binary_name(),
self.config.command.install_package(),
) {
(Some(b), Some(p)) => (b, p), (Some(b), Some(p)) => (b, p),
_ => return Ok(()), _ => return Ok(()),
}; };
@@ -117,9 +120,11 @@ impl CargoStep {
.stderr(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped())
.output() .output()
.await .await
.map_err(|e| WfeError::StepExecution(format!( .map_err(|e| {
"Failed to add llvm-tools-preview component: {e}" WfeError::StepExecution(format!(
)))?; "Failed to add llvm-tools-preview component: {e}"
))
})?;
if !component.status.success() { if !component.status.success() {
let stderr = String::from_utf8_lossy(&component.stderr); let stderr = String::from_utf8_lossy(&component.stderr);
@@ -135,9 +140,7 @@ impl CargoStep {
.stderr(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped())
.output() .output()
.await .await
.map_err(|e| WfeError::StepExecution(format!( .map_err(|e| WfeError::StepExecution(format!("Failed to install {package}: {e}")))?;
"Failed to install {package}: {e}"
)))?;
if !install.status.success() { if !install.status.success() {
let stderr = String::from_utf8_lossy(&install.stderr); let stderr = String::from_utf8_lossy(&install.stderr);
@@ -162,17 +165,16 @@ impl CargoStep {
let doc_dir = std::path::Path::new(working_dir).join("target/doc"); let doc_dir = std::path::Path::new(working_dir).join("target/doc");
let json_path = std::fs::read_dir(&doc_dir) let json_path = std::fs::read_dir(&doc_dir)
.map_err(|e| WfeError::StepExecution(format!( .map_err(|e| WfeError::StepExecution(format!("failed to read target/doc: {e}")))?
"failed to read target/doc: {e}"
)))?
.filter_map(|entry| entry.ok()) .filter_map(|entry| entry.ok())
.find(|entry| { .find(|entry| entry.path().extension().is_some_and(|ext| ext == "json"))
entry.path().extension().is_some_and(|ext| ext == "json")
})
.map(|entry| entry.path()) .map(|entry| entry.path())
.ok_or_else(|| WfeError::StepExecution( .ok_or_else(|| {
"no JSON file found in target/doc/ — did rustdoc --output-format json succeed?".to_string() WfeError::StepExecution(
))?; "no JSON file found in target/doc/ — did rustdoc --output-format json succeed?"
.to_string(),
)
})?;
tracing::info!(path = %json_path.display(), "reading rustdoc JSON"); tracing::info!(path = %json_path.display(), "reading rustdoc JSON");
@@ -180,20 +182,20 @@ impl CargoStep {
WfeError::StepExecution(format!("failed to read {}: {e}", json_path.display())) WfeError::StepExecution(format!("failed to read {}: {e}", json_path.display()))
})?; })?;
let krate: rustdoc_types::Crate = serde_json::from_str(&json_content).map_err(|e| { let krate: rustdoc_types::Crate = serde_json::from_str(&json_content)
WfeError::StepExecution(format!("failed to parse rustdoc JSON: {e}")) .map_err(|e| WfeError::StepExecution(format!("failed to parse rustdoc JSON: {e}")))?;
})?;
let mdx_files = transform_to_mdx(&krate); let mdx_files = transform_to_mdx(&krate);
let output_dir = self.config.output_dir let output_dir = self
.config
.output_dir
.as_deref() .as_deref()
.unwrap_or("target/doc/mdx"); .unwrap_or("target/doc/mdx");
let output_path = std::path::Path::new(working_dir).join(output_dir); let output_path = std::path::Path::new(working_dir).join(output_dir);
write_mdx_files(&mdx_files, &output_path).map_err(|e| { write_mdx_files(&mdx_files, &output_path)
WfeError::StepExecution(format!("failed to write MDX files: {e}")) .map_err(|e| WfeError::StepExecution(format!("failed to write MDX files: {e}")))?;
})?;
let file_count = mdx_files.len(); let file_count = mdx_files.len();
tracing::info!( tracing::info!(
@@ -214,7 +216,10 @@ impl CargoStep {
outputs.insert( outputs.insert(
"mdx.files".to_string(), "mdx.files".to_string(),
serde_json::Value::Array( serde_json::Value::Array(
file_paths.into_iter().map(serde_json::Value::String).collect(), file_paths
.into_iter()
.map(serde_json::Value::String)
.collect(),
), ),
); );
@@ -224,7 +229,10 @@ impl CargoStep {
#[async_trait] #[async_trait]
impl StepBody for CargoStep { impl StepBody for CargoStep {
async fn run(&mut self, context: &StepExecutionContext<'_>) -> wfe_core::Result<ExecutionResult> { async fn run(
&mut self,
context: &StepExecutionContext<'_>,
) -> wfe_core::Result<ExecutionResult> {
let step_name = context.step.name.as_deref().unwrap_or("unknown"); let step_name = context.step.name.as_deref().unwrap_or("unknown");
let subcmd = self.config.command.as_str(); let subcmd = self.config.command.as_str();
@@ -248,9 +256,9 @@ impl StepBody for CargoStep {
} }
} }
} else { } else {
cmd.output() cmd.output().await.map_err(|e| {
.await WfeError::StepExecution(format!("Failed to spawn cargo {subcmd}: {e}"))
.map_err(|e| WfeError::StepExecution(format!("Failed to spawn cargo {subcmd}: {e}")))? })?
}; };
let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stdout = String::from_utf8_lossy(&output.stdout).to_string();
@@ -317,7 +325,11 @@ mod tests {
let cmd = step.build_command(); let cmd = step.build_command();
let prog = cmd.as_std().get_program().to_str().unwrap(); let prog = cmd.as_std().get_program().to_str().unwrap();
assert_eq!(prog, "cargo"); assert_eq!(prog, "cargo");
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
.as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert_eq!(args, vec!["build"]); assert_eq!(args, vec!["build"]);
} }
@@ -329,7 +341,11 @@ mod tests {
let cmd = step.build_command(); let cmd = step.build_command();
let prog = cmd.as_std().get_program().to_str().unwrap(); let prog = cmd.as_std().get_program().to_str().unwrap();
assert_eq!(prog, "rustup"); assert_eq!(prog, "rustup");
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
.as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert_eq!(args, vec!["run", "nightly", "cargo", "test"]); assert_eq!(args, vec!["run", "nightly", "cargo", "test"]);
} }
@@ -340,8 +356,15 @@ mod tests {
config.features = vec!["feat1".to_string(), "feat2".to_string()]; config.features = vec!["feat1".to_string(), "feat2".to_string()];
let step = CargoStep::new(config); let step = CargoStep::new(config);
let cmd = step.build_command(); let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
assert_eq!(args, vec!["check", "-p", "my-crate", "--features", "feat1,feat2"]); .as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert_eq!(
args,
vec!["check", "-p", "my-crate", "--features", "feat1,feat2"]
);
} }
#[test] #[test]
@@ -351,8 +374,20 @@ mod tests {
config.target = Some("aarch64-unknown-linux-gnu".to_string()); config.target = Some("aarch64-unknown-linux-gnu".to_string());
let step = CargoStep::new(config); let step = CargoStep::new(config);
let cmd = step.build_command(); let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
assert_eq!(args, vec!["build", "--release", "--target", "aarch64-unknown-linux-gnu"]); .as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert_eq!(
args,
vec![
"build",
"--release",
"--target",
"aarch64-unknown-linux-gnu"
]
);
} }
#[test] #[test]
@@ -364,10 +399,23 @@ mod tests {
config.extra_args = vec!["--".to_string(), "-D".to_string(), "warnings".to_string()]; config.extra_args = vec!["--".to_string(), "-D".to_string(), "warnings".to_string()];
let step = CargoStep::new(config); let step = CargoStep::new(config);
let cmd = step.build_command(); let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
.as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert_eq!( assert_eq!(
args, args,
vec!["clippy", "--all-features", "--no-default-features", "--profile", "dev", "--", "-D", "warnings"] vec![
"clippy",
"--all-features",
"--no-default-features",
"--profile",
"dev",
"--",
"-D",
"warnings"
]
); );
} }
@@ -377,17 +425,29 @@ mod tests {
config.extra_args = vec!["--check".to_string()]; config.extra_args = vec!["--check".to_string()];
let step = CargoStep::new(config); let step = CargoStep::new(config);
let cmd = step.build_command(); let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
.as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert_eq!(args, vec!["fmt", "--check"]); assert_eq!(args, vec!["fmt", "--check"]);
} }
#[test] #[test]
fn build_command_publish_dry_run() { fn build_command_publish_dry_run() {
let mut config = minimal_config(CargoCommand::Publish); let mut config = minimal_config(CargoCommand::Publish);
config.extra_args = vec!["--dry-run".to_string(), "--registry".to_string(), "my-reg".to_string()]; config.extra_args = vec![
"--dry-run".to_string(),
"--registry".to_string(),
"my-reg".to_string(),
];
let step = CargoStep::new(config); let step = CargoStep::new(config);
let cmd = step.build_command(); let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
.as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert_eq!(args, vec!["publish", "--dry-run", "--registry", "my-reg"]); assert_eq!(args, vec!["publish", "--dry-run", "--registry", "my-reg"]);
} }
@@ -398,18 +458,27 @@ mod tests {
config.release = true; config.release = true;
let step = CargoStep::new(config); let step = CargoStep::new(config);
let cmd = step.build_command(); let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
.as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert_eq!(args, vec!["doc", "--release", "--no-deps"]); assert_eq!(args, vec!["doc", "--release", "--no-deps"]);
} }
#[test] #[test]
fn build_command_env_vars() { fn build_command_env_vars() {
let mut config = minimal_config(CargoCommand::Build); let mut config = minimal_config(CargoCommand::Build);
config.env.insert("RUSTFLAGS".to_string(), "-D warnings".to_string()); config
.env
.insert("RUSTFLAGS".to_string(), "-D warnings".to_string());
let step = CargoStep::new(config); let step = CargoStep::new(config);
let cmd = step.build_command(); let cmd = step.build_command();
let envs: Vec<_> = cmd.as_std().get_envs().collect(); let envs: Vec<_> = cmd.as_std().get_envs().collect();
assert!(envs.iter().any(|(k, v)| *k == "RUSTFLAGS" && v == &Some("-D warnings".as_ref()))); assert!(
envs.iter()
.any(|(k, v)| *k == "RUSTFLAGS" && v == &Some("-D warnings".as_ref()))
);
} }
#[test] #[test]
@@ -418,14 +487,21 @@ mod tests {
config.working_dir = Some("/my/project".to_string()); config.working_dir = Some("/my/project".to_string());
let step = CargoStep::new(config); let step = CargoStep::new(config);
let cmd = step.build_command(); let cmd = step.build_command();
assert_eq!(cmd.as_std().get_current_dir(), Some(std::path::Path::new("/my/project"))); assert_eq!(
cmd.as_std().get_current_dir(),
Some(std::path::Path::new("/my/project"))
);
} }
#[test] #[test]
fn build_command_audit() { fn build_command_audit() {
let step = CargoStep::new(minimal_config(CargoCommand::Audit)); let step = CargoStep::new(minimal_config(CargoCommand::Audit));
let cmd = step.build_command(); let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
.as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert_eq!(args, vec!["audit"]); assert_eq!(args, vec!["audit"]);
} }
@@ -435,7 +511,11 @@ mod tests {
config.extra_args = vec!["check".to_string()]; config.extra_args = vec!["check".to_string()];
let step = CargoStep::new(config); let step = CargoStep::new(config);
let cmd = step.build_command(); let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
.as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert_eq!(args, vec!["deny", "check"]); assert_eq!(args, vec!["deny", "check"]);
} }
@@ -443,7 +523,11 @@ mod tests {
fn build_command_nextest() { fn build_command_nextest() {
let step = CargoStep::new(minimal_config(CargoCommand::Nextest)); let step = CargoStep::new(minimal_config(CargoCommand::Nextest));
let cmd = step.build_command(); let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
.as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert_eq!(args, vec!["nextest", "run"]); assert_eq!(args, vec!["nextest", "run"]);
} }
@@ -454,25 +538,44 @@ mod tests {
config.extra_args = vec!["--no-fail-fast".to_string()]; config.extra_args = vec!["--no-fail-fast".to_string()];
let step = CargoStep::new(config); let step = CargoStep::new(config);
let cmd = step.build_command(); let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
assert_eq!(args, vec!["nextest", "run", "--features", "feat1", "--no-fail-fast"]); .as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert_eq!(
args,
vec!["nextest", "run", "--features", "feat1", "--no-fail-fast"]
);
} }
#[test] #[test]
fn build_command_llvm_cov() { fn build_command_llvm_cov() {
let step = CargoStep::new(minimal_config(CargoCommand::LlvmCov)); let step = CargoStep::new(minimal_config(CargoCommand::LlvmCov));
let cmd = step.build_command(); let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
.as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert_eq!(args, vec!["llvm-cov"]); assert_eq!(args, vec!["llvm-cov"]);
} }
#[test] #[test]
fn build_command_llvm_cov_with_args() { fn build_command_llvm_cov_with_args() {
let mut config = minimal_config(CargoCommand::LlvmCov); let mut config = minimal_config(CargoCommand::LlvmCov);
config.extra_args = vec!["--html".to_string(), "--output-dir".to_string(), "coverage".to_string()]; config.extra_args = vec![
"--html".to_string(),
"--output-dir".to_string(),
"coverage".to_string(),
];
let step = CargoStep::new(config); let step = CargoStep::new(config);
let cmd = step.build_command(); let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
.as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert_eq!(args, vec!["llvm-cov", "--html", "--output-dir", "coverage"]); assert_eq!(args, vec!["llvm-cov", "--html", "--output-dir", "coverage"]);
} }
@@ -482,10 +585,24 @@ mod tests {
let cmd = step.build_command(); let cmd = step.build_command();
let prog = cmd.as_std().get_program().to_str().unwrap(); let prog = cmd.as_std().get_program().to_str().unwrap();
assert_eq!(prog, "rustup"); assert_eq!(prog, "rustup");
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
.as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert_eq!( assert_eq!(
args, args,
vec!["run", "nightly", "cargo", "rustdoc", "--", "-Z", "unstable-options", "--output-format", "json"] vec![
"run",
"nightly",
"cargo",
"rustdoc",
"--",
"-Z",
"unstable-options",
"--output-format",
"json"
]
); );
} }
@@ -496,10 +613,27 @@ mod tests {
config.extra_args = vec!["--no-deps".to_string()]; config.extra_args = vec!["--no-deps".to_string()];
let step = CargoStep::new(config); let step = CargoStep::new(config);
let cmd = step.build_command(); let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
.as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert_eq!( assert_eq!(
args, args,
vec!["run", "nightly", "cargo", "rustdoc", "-p", "my-crate", "--no-deps", "--", "-Z", "unstable-options", "--output-format", "json"] vec![
"run",
"nightly",
"cargo",
"rustdoc",
"-p",
"my-crate",
"--no-deps",
"--",
"-Z",
"unstable-options",
"--output-format",
"json"
]
); );
} }
@@ -509,7 +643,11 @@ mod tests {
config.toolchain = Some("nightly-2024-06-01".to_string()); config.toolchain = Some("nightly-2024-06-01".to_string());
let step = CargoStep::new(config); let step = CargoStep::new(config);
let cmd = step.build_command(); let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
.as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert!(args.contains(&"nightly-2024-06-01")); assert!(args.contains(&"nightly-2024-06-01"));
} }

View File

@@ -117,8 +117,7 @@ fn render_module(module_path: &str, items: &[(&Item, &str)], krate: &Crate) -> S
items items
.iter() .iter()
.find(|(item, kind)| { .find(|(item, kind)| {
*kind == "Modules" *kind == "Modules" && item.name.as_deref() == module_path.split("::").last()
&& item.name.as_deref() == module_path.split("::").last()
}) })
.and_then(|(item, _)| item.docs.as_ref()) .and_then(|(item, _)| item.docs.as_ref())
.map(|d| first_sentence(d)) .map(|d| first_sentence(d))
@@ -136,8 +135,15 @@ fn render_module(module_path: &str, items: &[(&Item, &str)], krate: &Crate) -> S
} }
let kind_order = [ let kind_order = [
"Modules", "Structs", "Enums", "Traits", "Functions", "Modules",
"Type Aliases", "Constants", "Statics", "Macros", "Structs",
"Enums",
"Traits",
"Functions",
"Type Aliases",
"Constants",
"Statics",
"Macros",
]; ];
for kind in &kind_order { for kind in &kind_order {
@@ -266,16 +272,15 @@ fn render_signature(item: &Item, krate: &Crate) -> Option<String> {
} }
Some(sig) Some(sig)
} }
ItemEnum::TypeAlias(ta) => { ItemEnum::TypeAlias(ta) => Some(format!(
Some(format!("pub type {name} = {}", render_type(&ta.type_, krate))) "pub type {name} = {}",
} render_type(&ta.type_, krate)
ItemEnum::Constant { type_, const_: c } => { )),
Some(format!( ItemEnum::Constant { type_, const_: c } => Some(format!(
"pub const {name}: {} = {}", "pub const {name}: {} = {}",
render_type(type_, krate), render_type(type_, krate),
c.value.as_deref().unwrap_or("...") c.value.as_deref().unwrap_or("...")
)) )),
}
ItemEnum::Macro(_) => Some(format!("macro_rules! {name} {{ ... }}")), ItemEnum::Macro(_) => Some(format!("macro_rules! {name} {{ ... }}")),
_ => None, _ => None,
} }
@@ -309,7 +314,11 @@ fn render_type(ty: &Type, krate: &Crate) -> String {
} }
Type::Generic(name) => name.clone(), Type::Generic(name) => name.clone(),
Type::Primitive(name) => name.clone(), Type::Primitive(name) => name.clone(),
Type::BorrowedRef { lifetime, is_mutable, type_ } => { Type::BorrowedRef {
lifetime,
is_mutable,
type_,
} => {
let mut s = String::from("&"); let mut s = String::from("&");
if let Some(lt) = lifetime { if let Some(lt) = lifetime {
s.push_str(lt); s.push_str(lt);
@@ -346,7 +355,12 @@ fn render_type(ty: &Type, krate: &Crate) -> String {
.collect(); .collect();
format!("impl {}", rendered.join(" + ")) format!("impl {}", rendered.join(" + "))
} }
Type::QualifiedPath { name, self_type, trait_, .. } => { Type::QualifiedPath {
name,
self_type,
trait_,
..
} => {
let self_str = render_type(self_type, krate); let self_str = render_type(self_type, krate);
if let Some(t) = trait_ { if let Some(t) = trait_ {
format!("<{self_str} as {}>::{name}", t.path) format!("<{self_str} as {}>::{name}", t.path)
@@ -417,11 +431,17 @@ mod tests {
deprecation: None, deprecation: None,
inner: ItemEnum::Function(Function { inner: ItemEnum::Function(Function {
sig: FunctionSignature { sig: FunctionSignature {
inputs: params.into_iter().map(|(n, t)| (n.to_string(), t)).collect(), inputs: params
.into_iter()
.map(|(n, t)| (n.to_string(), t))
.collect(),
output, output,
is_c_variadic: false, is_c_variadic: false,
}, },
generics: Generics { params: vec![], where_predicates: vec![] }, generics: Generics {
params: vec![],
where_predicates: vec![],
},
header: FunctionHeader { header: FunctionHeader {
is_const: false, is_const: false,
is_unsafe: false, is_unsafe: false,
@@ -446,7 +466,10 @@ mod tests {
deprecation: None, deprecation: None,
inner: ItemEnum::Struct(Struct { inner: ItemEnum::Struct(Struct {
kind: StructKind::Unit, kind: StructKind::Unit,
generics: Generics { params: vec![], where_predicates: vec![] }, generics: Generics {
params: vec![],
where_predicates: vec![],
},
impls: vec![], impls: vec![],
}), }),
} }
@@ -464,7 +487,10 @@ mod tests {
attrs: vec![], attrs: vec![],
deprecation: None, deprecation: None,
inner: ItemEnum::Enum(Enum { inner: ItemEnum::Enum(Enum {
generics: Generics { params: vec![], where_predicates: vec![] }, generics: Generics {
params: vec![],
where_predicates: vec![],
},
variants: vec![], variants: vec![],
has_stripped_variants: false, has_stripped_variants: false,
impls: vec![], impls: vec![],
@@ -488,7 +514,10 @@ mod tests {
is_unsafe: false, is_unsafe: false,
is_dyn_compatible: true, is_dyn_compatible: true,
items: vec![], items: vec![],
generics: Generics { params: vec![], where_predicates: vec![] }, generics: Generics {
params: vec![],
where_predicates: vec![],
},
bounds: vec![], bounds: vec![],
implementations: vec![], implementations: vec![],
}), }),
@@ -540,35 +569,57 @@ mod tests {
#[test] #[test]
fn render_type_tuple() { fn render_type_tuple() {
let krate = empty_crate(); let krate = empty_crate();
let ty = Type::Tuple(vec![Type::Primitive("u32".into()), Type::Primitive("String".into())]); let ty = Type::Tuple(vec![
Type::Primitive("u32".into()),
Type::Primitive("String".into()),
]);
assert_eq!(render_type(&ty, &krate), "(u32, String)"); assert_eq!(render_type(&ty, &krate), "(u32, String)");
} }
#[test] #[test]
fn render_type_slice() { fn render_type_slice() {
let krate = empty_crate(); let krate = empty_crate();
assert_eq!(render_type(&Type::Slice(Box::new(Type::Primitive("u8".into()))), &krate), "[u8]"); assert_eq!(
render_type(&Type::Slice(Box::new(Type::Primitive("u8".into()))), &krate),
"[u8]"
);
} }
#[test] #[test]
fn render_type_array() { fn render_type_array() {
let krate = empty_crate(); let krate = empty_crate();
let ty = Type::Array { type_: Box::new(Type::Primitive("u8".into())), len: "32".into() }; let ty = Type::Array {
type_: Box::new(Type::Primitive("u8".into())),
len: "32".into(),
};
assert_eq!(render_type(&ty, &krate), "[u8; 32]"); assert_eq!(render_type(&ty, &krate), "[u8; 32]");
} }
#[test] #[test]
fn render_type_raw_pointer() { fn render_type_raw_pointer() {
let krate = empty_crate(); let krate = empty_crate();
let ty = Type::RawPointer { is_mutable: true, type_: Box::new(Type::Primitive("u8".into())) }; let ty = Type::RawPointer {
is_mutable: true,
type_: Box::new(Type::Primitive("u8".into())),
};
assert_eq!(render_type(&ty, &krate), "*mut u8"); assert_eq!(render_type(&ty, &krate), "*mut u8");
} }
#[test] #[test]
fn render_function_signature() { fn render_function_signature() {
let krate = empty_crate(); let krate = empty_crate();
let item = make_function("add", vec![("a", Type::Primitive("u32".into())), ("b", Type::Primitive("u32".into()))], Some(Type::Primitive("u32".into()))); let item = make_function(
assert_eq!(render_signature(&item, &krate).unwrap(), "fn add(a: u32, b: u32) -> u32"); "add",
vec![
("a", Type::Primitive("u32".into())),
("b", Type::Primitive("u32".into())),
],
Some(Type::Primitive("u32".into())),
);
assert_eq!(
render_signature(&item, &krate).unwrap(),
"fn add(a: u32, b: u32) -> u32"
);
} }
#[test] #[test]
@@ -581,25 +632,51 @@ mod tests {
#[test] #[test]
fn render_struct_signature() { fn render_struct_signature() {
let krate = empty_crate(); let krate = empty_crate();
assert_eq!(render_signature(&make_struct("MyStruct"), &krate).unwrap(), "pub struct MyStruct;"); assert_eq!(
render_signature(&make_struct("MyStruct"), &krate).unwrap(),
"pub struct MyStruct;"
);
} }
#[test] #[test]
fn render_enum_signature() { fn render_enum_signature() {
let krate = empty_crate(); let krate = empty_crate();
assert_eq!(render_signature(&make_enum("Color"), &krate).unwrap(), "pub enum Color { }"); assert_eq!(
render_signature(&make_enum("Color"), &krate).unwrap(),
"pub enum Color { }"
);
} }
#[test] #[test]
fn render_trait_signature() { fn render_trait_signature() {
let krate = empty_crate(); let krate = empty_crate();
assert_eq!(render_signature(&make_trait("Drawable"), &krate).unwrap(), "pub trait Drawable"); assert_eq!(
render_signature(&make_trait("Drawable"), &krate).unwrap(),
"pub trait Drawable"
);
} }
#[test] #[test]
fn item_kind_labels() { fn item_kind_labels() {
assert_eq!(item_kind_label(&ItemEnum::Module(Module { is_crate: false, items: vec![], is_stripped: false })), Some("Modules")); assert_eq!(
assert_eq!(item_kind_label(&ItemEnum::Struct(Struct { kind: StructKind::Unit, generics: Generics { params: vec![], where_predicates: vec![] }, impls: vec![] })), Some("Structs")); item_kind_label(&ItemEnum::Module(Module {
is_crate: false,
items: vec![],
is_stripped: false
})),
Some("Modules")
);
assert_eq!(
item_kind_label(&ItemEnum::Struct(Struct {
kind: StructKind::Unit,
generics: Generics {
params: vec![],
where_predicates: vec![]
},
impls: vec![]
})),
Some("Structs")
);
} }
#[test] #[test]
@@ -613,7 +690,14 @@ mod tests {
let func = make_function("hello", vec![], None); let func = make_function("hello", vec![], None);
let id = Id(1); let id = Id(1);
krate.index.insert(id.clone(), func); krate.index.insert(id.clone(), func);
krate.paths.insert(id, ItemSummary { crate_id: 0, path: vec!["my_crate".into(), "hello".into()], kind: ItemKind::Function }); krate.paths.insert(
id,
ItemSummary {
crate_id: 0,
path: vec!["my_crate".into(), "hello".into()],
kind: ItemKind::Function,
},
);
let files = transform_to_mdx(&krate); let files = transform_to_mdx(&krate);
assert_eq!(files.len(), 1); assert_eq!(files.len(), 1);
@@ -628,11 +712,25 @@ mod tests {
let mut krate = empty_crate(); let mut krate = empty_crate();
let func = make_function("do_thing", vec![], None); let func = make_function("do_thing", vec![], None);
krate.index.insert(Id(1), func); krate.index.insert(Id(1), func);
krate.paths.insert(Id(1), ItemSummary { crate_id: 0, path: vec!["mc".into(), "do_thing".into()], kind: ItemKind::Function }); krate.paths.insert(
Id(1),
ItemSummary {
crate_id: 0,
path: vec!["mc".into(), "do_thing".into()],
kind: ItemKind::Function,
},
);
let st = make_struct("Widget"); let st = make_struct("Widget");
krate.index.insert(Id(2), st); krate.index.insert(Id(2), st);
krate.paths.insert(Id(2), ItemSummary { crate_id: 0, path: vec!["mc".into(), "Widget".into()], kind: ItemKind::Struct }); krate.paths.insert(
Id(2),
ItemSummary {
crate_id: 0,
path: vec!["mc".into(), "Widget".into()],
kind: ItemKind::Struct,
},
);
let files = transform_to_mdx(&krate); let files = transform_to_mdx(&krate);
assert_eq!(files.len(), 1); assert_eq!(files.len(), 1);
@@ -654,7 +752,11 @@ mod tests {
links: HashMap::new(), links: HashMap::new(),
attrs: vec![], attrs: vec![],
deprecation: None, deprecation: None,
inner: ItemEnum::Module(Module { is_crate: true, items: vec![Id(1)], is_stripped: false }), inner: ItemEnum::Module(Module {
is_crate: true,
items: vec![Id(1)],
is_stripped: false,
}),
}; };
krate.root = Id(0); krate.root = Id(0);
krate.index.insert(Id(0), root_module); krate.index.insert(Id(0), root_module);
@@ -662,12 +764,23 @@ mod tests {
// Add a function so the module generates a file. // Add a function so the module generates a file.
let func = make_function("f", vec![], None); let func = make_function("f", vec![], None);
krate.index.insert(Id(1), func); krate.index.insert(Id(1), func);
krate.paths.insert(Id(1), ItemSummary { crate_id: 0, path: vec!["f".into()], kind: ItemKind::Function }); krate.paths.insert(
Id(1),
ItemSummary {
crate_id: 0,
path: vec!["f".into()],
kind: ItemKind::Function,
},
);
let files = transform_to_mdx(&krate); let files = transform_to_mdx(&krate);
// The root module's description in frontmatter should have escaped quotes. // The root module's description in frontmatter should have escaped quotes.
let index = files.iter().find(|f| f.path == "index.mdx").unwrap(); let index = files.iter().find(|f| f.path == "index.mdx").unwrap();
assert!(index.content.contains("\\\"quoted\\\""), "content: {}", index.content); assert!(
index.content.contains("\\\"quoted\\\""),
"content: {}",
index.content
);
} }
#[test] #[test]
@@ -677,7 +790,9 @@ mod tests {
path: "Option".into(), path: "Option".into(),
id: Id(99), id: Id(99),
args: Some(Box::new(rustdoc_types::GenericArgs::AngleBracketed { args: Some(Box::new(rustdoc_types::GenericArgs::AngleBracketed {
args: vec![rustdoc_types::GenericArg::Type(Type::Primitive("u32".into()))], args: vec![rustdoc_types::GenericArg::Type(Type::Primitive(
"u32".into(),
))],
constraints: vec![], constraints: vec![],
})), })),
}); });
@@ -687,13 +802,15 @@ mod tests {
#[test] #[test]
fn render_type_impl_trait() { fn render_type_impl_trait() {
let krate = empty_crate(); let krate = empty_crate();
let ty = Type::ImplTrait(vec![ let ty = Type::ImplTrait(vec![rustdoc_types::GenericBound::TraitBound {
rustdoc_types::GenericBound::TraitBound { trait_: rustdoc_types::Path {
trait_: rustdoc_types::Path { path: "Display".into(), id: Id(99), args: None }, path: "Display".into(),
generic_params: vec![], id: Id(99),
modifier: rustdoc_types::TraitBoundModifier::None, args: None,
}, },
]); generic_params: vec![],
modifier: rustdoc_types::TraitBoundModifier::None,
}]);
assert_eq!(render_type(&ty, &krate), "impl Display"); assert_eq!(render_type(&ty, &krate), "impl Display");
} }
@@ -702,7 +819,11 @@ mod tests {
let krate = empty_crate(); let krate = empty_crate();
let ty = Type::DynTrait(rustdoc_types::DynTrait { let ty = Type::DynTrait(rustdoc_types::DynTrait {
traits: vec![rustdoc_types::PolyTrait { traits: vec![rustdoc_types::PolyTrait {
trait_: rustdoc_types::Path { path: "Error".into(), id: Id(99), args: None }, trait_: rustdoc_types::Path {
path: "Error".into(),
id: Id(99),
args: None,
},
generic_params: vec![], generic_params: vec![],
}], }],
lifetime: None, lifetime: None,
@@ -720,7 +841,12 @@ mod tests {
is_c_variadic: false, is_c_variadic: false,
}, },
generic_params: vec![], generic_params: vec![],
header: FunctionHeader { is_const: false, is_unsafe: false, is_async: false, abi: Abi::Rust }, header: FunctionHeader {
is_const: false,
is_unsafe: false,
is_async: false,
abi: Abi::Rust,
},
})); }));
assert_eq!(render_type(&ty, &krate), "fn(u32) -> bool"); assert_eq!(render_type(&ty, &krate), "fn(u32) -> bool");
} }
@@ -728,7 +854,10 @@ mod tests {
#[test] #[test]
fn render_type_const_pointer() { fn render_type_const_pointer() {
let krate = empty_crate(); let krate = empty_crate();
let ty = Type::RawPointer { is_mutable: false, type_: Box::new(Type::Primitive("u8".into())) }; let ty = Type::RawPointer {
is_mutable: false,
type_: Box::new(Type::Primitive("u8".into())),
};
assert_eq!(render_type(&ty, &krate), "*const u8"); assert_eq!(render_type(&ty, &krate), "*const u8");
} }
@@ -743,9 +872,16 @@ mod tests {
let krate = empty_crate(); let krate = empty_crate();
let ty = Type::QualifiedPath { let ty = Type::QualifiedPath {
name: "Item".into(), name: "Item".into(),
args: Box::new(rustdoc_types::GenericArgs::AngleBracketed { args: vec![], constraints: vec![] }), args: Box::new(rustdoc_types::GenericArgs::AngleBracketed {
args: vec![],
constraints: vec![],
}),
self_type: Box::new(Type::Generic("T".into())), self_type: Box::new(Type::Generic("T".into())),
trait_: Some(rustdoc_types::Path { path: "Iterator".into(), id: Id(99), args: None }), trait_: Some(rustdoc_types::Path {
path: "Iterator".into(),
id: Id(99),
args: None,
}),
}; };
assert_eq!(render_type(&ty, &krate), "<T as Iterator>::Item"); assert_eq!(render_type(&ty, &krate), "<T as Iterator>::Item");
} }
@@ -753,74 +889,137 @@ mod tests {
#[test] #[test]
fn item_kind_label_all_variants() { fn item_kind_label_all_variants() {
// Test the remaining untested variants // Test the remaining untested variants
assert_eq!(item_kind_label(&ItemEnum::Enum(Enum { assert_eq!(
generics: Generics { params: vec![], where_predicates: vec![] }, item_kind_label(&ItemEnum::Enum(Enum {
variants: vec![], has_stripped_variants: false, impls: vec![], generics: Generics {
})), Some("Enums")); params: vec![],
assert_eq!(item_kind_label(&ItemEnum::Trait(Trait { where_predicates: vec![]
is_auto: false, is_unsafe: false, is_dyn_compatible: true, },
items: vec![], generics: Generics { params: vec![], where_predicates: vec![] }, variants: vec![],
bounds: vec![], implementations: vec![], has_stripped_variants: false,
})), Some("Traits")); impls: vec![],
})),
Some("Enums")
);
assert_eq!(
item_kind_label(&ItemEnum::Trait(Trait {
is_auto: false,
is_unsafe: false,
is_dyn_compatible: true,
items: vec![],
generics: Generics {
params: vec![],
where_predicates: vec![]
},
bounds: vec![],
implementations: vec![],
})),
Some("Traits")
);
assert_eq!(item_kind_label(&ItemEnum::Macro("".into())), Some("Macros")); assert_eq!(item_kind_label(&ItemEnum::Macro("".into())), Some("Macros"));
assert_eq!(item_kind_label(&ItemEnum::Static(rustdoc_types::Static { assert_eq!(
type_: Type::Primitive("u32".into()), item_kind_label(&ItemEnum::Static(rustdoc_types::Static {
is_mutable: false, type_: Type::Primitive("u32".into()),
is_unsafe: false, is_mutable: false,
expr: String::new(), is_unsafe: false,
})), Some("Statics")); expr: String::new(),
})),
Some("Statics")
);
// Impl blocks should be skipped // Impl blocks should be skipped
assert_eq!(item_kind_label(&ItemEnum::Impl(rustdoc_types::Impl { assert_eq!(
is_unsafe: false, generics: Generics { params: vec![], where_predicates: vec![] }, item_kind_label(&ItemEnum::Impl(rustdoc_types::Impl {
provided_trait_methods: vec![], trait_: None, for_: Type::Primitive("u32".into()), is_unsafe: false,
items: vec![], is_negative: false, is_synthetic: false, generics: Generics {
blanket_impl: None, params: vec![],
})), None); where_predicates: vec![]
},
provided_trait_methods: vec![],
trait_: None,
for_: Type::Primitive("u32".into()),
items: vec![],
is_negative: false,
is_synthetic: false,
blanket_impl: None,
})),
None
);
} }
#[test] #[test]
fn render_constant_signature() { fn render_constant_signature() {
let krate = empty_crate(); let krate = empty_crate();
let item = Item { let item = Item {
id: Id(5), crate_id: 0, id: Id(5),
name: Some("MAX_SIZE".into()), span: None, crate_id: 0,
visibility: Visibility::Public, docs: None, name: Some("MAX_SIZE".into()),
links: HashMap::new(), attrs: vec![], deprecation: None, span: None,
visibility: Visibility::Public,
docs: None,
links: HashMap::new(),
attrs: vec![],
deprecation: None,
inner: ItemEnum::Constant { inner: ItemEnum::Constant {
type_: Type::Primitive("usize".into()), type_: Type::Primitive("usize".into()),
const_: rustdoc_types::Constant { expr: "1024".into(), value: Some("1024".into()), is_literal: true }, const_: rustdoc_types::Constant {
expr: "1024".into(),
value: Some("1024".into()),
is_literal: true,
},
}, },
}; };
assert_eq!(render_signature(&item, &krate).unwrap(), "pub const MAX_SIZE: usize = 1024"); assert_eq!(
render_signature(&item, &krate).unwrap(),
"pub const MAX_SIZE: usize = 1024"
);
} }
#[test] #[test]
fn render_type_alias_signature() { fn render_type_alias_signature() {
let krate = empty_crate(); let krate = empty_crate();
let item = Item { let item = Item {
id: Id(6), crate_id: 0, id: Id(6),
name: Some("Result".into()), span: None, crate_id: 0,
visibility: Visibility::Public, docs: None, name: Some("Result".into()),
links: HashMap::new(), attrs: vec![], deprecation: None, span: None,
visibility: Visibility::Public,
docs: None,
links: HashMap::new(),
attrs: vec![],
deprecation: None,
inner: ItemEnum::TypeAlias(rustdoc_types::TypeAlias { inner: ItemEnum::TypeAlias(rustdoc_types::TypeAlias {
type_: Type::Primitive("u32".into()), type_: Type::Primitive("u32".into()),
generics: Generics { params: vec![], where_predicates: vec![] }, generics: Generics {
params: vec![],
where_predicates: vec![],
},
}), }),
}; };
assert_eq!(render_signature(&item, &krate).unwrap(), "pub type Result = u32"); assert_eq!(
render_signature(&item, &krate).unwrap(),
"pub type Result = u32"
);
} }
#[test] #[test]
fn render_macro_signature() { fn render_macro_signature() {
let krate = empty_crate(); let krate = empty_crate();
let item = Item { let item = Item {
id: Id(7), crate_id: 0, id: Id(7),
name: Some("my_macro".into()), span: None, crate_id: 0,
visibility: Visibility::Public, docs: None, name: Some("my_macro".into()),
links: HashMap::new(), attrs: vec![], deprecation: None, span: None,
visibility: Visibility::Public,
docs: None,
links: HashMap::new(),
attrs: vec![],
deprecation: None,
inner: ItemEnum::Macro("macro body".into()), inner: ItemEnum::Macro("macro body".into()),
}; };
assert_eq!(render_signature(&item, &krate).unwrap(), "macro_rules! my_macro { ... }"); assert_eq!(
render_signature(&item, &krate).unwrap(),
"macro_rules! my_macro { ... }"
);
} }
#[test] #[test]
@@ -839,9 +1038,15 @@ mod tests {
#[test] #[test]
fn write_mdx_files_creates_directories() { fn write_mdx_files_creates_directories() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let files = vec![MdxFile { path: "nested/module.mdx".into(), content: "# Test\n".into() }]; let files = vec![MdxFile {
path: "nested/module.mdx".into(),
content: "# Test\n".into(),
}];
write_mdx_files(&files, tmp.path()).unwrap(); write_mdx_files(&files, tmp.path()).unwrap();
assert!(tmp.path().join("nested/module.mdx").exists()); assert!(tmp.path().join("nested/module.mdx").exists());
assert_eq!(std::fs::read_to_string(tmp.path().join("nested/module.mdx")).unwrap(), "# Test\n"); assert_eq!(
std::fs::read_to_string(tmp.path().join("nested/module.mdx")).unwrap(),
"# Test\n"
);
} }
} }

View File

@@ -60,7 +60,10 @@ mod tests {
#[test] #[test]
fn command_as_str() { fn command_as_str() {
assert_eq!(RustupCommand::Install.as_str(), "install"); assert_eq!(RustupCommand::Install.as_str(), "install");
assert_eq!(RustupCommand::ToolchainInstall.as_str(), "toolchain-install"); assert_eq!(
RustupCommand::ToolchainInstall.as_str(),
"toolchain-install"
);
assert_eq!(RustupCommand::ComponentAdd.as_str(), "component-add"); assert_eq!(RustupCommand::ComponentAdd.as_str(), "component-add");
assert_eq!(RustupCommand::TargetAdd.as_str(), "target-add"); assert_eq!(RustupCommand::TargetAdd.as_str(), "target-add");
} }
@@ -118,7 +121,11 @@ mod tests {
let config = RustupConfig { let config = RustupConfig {
command: RustupCommand::ComponentAdd, command: RustupCommand::ComponentAdd,
toolchain: Some("nightly".to_string()), toolchain: Some("nightly".to_string()),
components: vec!["clippy".to_string(), "rustfmt".to_string(), "rust-src".to_string()], components: vec![
"clippy".to_string(),
"rustfmt".to_string(),
"rust-src".to_string(),
],
targets: vec![], targets: vec![],
profile: None, profile: None,
default_toolchain: None, default_toolchain: None,
@@ -138,7 +145,10 @@ mod tests {
command: RustupCommand::TargetAdd, command: RustupCommand::TargetAdd,
toolchain: Some("stable".to_string()), toolchain: Some("stable".to_string()),
components: vec![], components: vec![],
targets: vec!["wasm32-unknown-unknown".to_string(), "aarch64-linux-android".to_string()], targets: vec![
"wasm32-unknown-unknown".to_string(),
"aarch64-linux-android".to_string(),
],
profile: None, profile: None,
default_toolchain: None, default_toolchain: None,
extra_args: vec![], extra_args: vec![],
@@ -147,7 +157,10 @@ mod tests {
let json = serde_json::to_string(&config).unwrap(); let json = serde_json::to_string(&config).unwrap();
let de: RustupConfig = serde_json::from_str(&json).unwrap(); let de: RustupConfig = serde_json::from_str(&json).unwrap();
assert_eq!(de.command, RustupCommand::TargetAdd); assert_eq!(de.command, RustupCommand::TargetAdd);
assert_eq!(de.targets, vec!["wasm32-unknown-unknown", "aarch64-linux-android"]); assert_eq!(
de.targets,
vec!["wasm32-unknown-unknown", "aarch64-linux-android"]
);
} }
#[test] #[test]

View File

@@ -1,7 +1,7 @@
use async_trait::async_trait; use async_trait::async_trait;
use wfe_core::WfeError;
use wfe_core::models::ExecutionResult; use wfe_core::models::ExecutionResult;
use wfe_core::traits::step::{StepBody, StepExecutionContext}; use wfe_core::traits::step::{StepBody, StepExecutionContext};
use wfe_core::WfeError;
use crate::rustup::config::{RustupCommand, RustupConfig}; use crate::rustup::config::{RustupCommand, RustupConfig};
@@ -26,7 +26,8 @@ impl RustupStep {
fn build_install_command(&self) -> tokio::process::Command { fn build_install_command(&self) -> tokio::process::Command {
let mut cmd = tokio::process::Command::new("sh"); let mut cmd = tokio::process::Command::new("sh");
// Pipe rustup-init through sh with non-interactive flag. // Pipe rustup-init through sh with non-interactive flag.
let mut script = "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y".to_string(); let mut script =
"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y".to_string();
if let Some(ref profile) = self.config.profile { if let Some(ref profile) = self.config.profile {
script.push_str(&format!(" --profile {profile}")); script.push_str(&format!(" --profile {profile}"));
@@ -112,7 +113,10 @@ impl RustupStep {
#[async_trait] #[async_trait]
impl StepBody for RustupStep { impl StepBody for RustupStep {
async fn run(&mut self, context: &StepExecutionContext<'_>) -> wfe_core::Result<ExecutionResult> { async fn run(
&mut self,
context: &StepExecutionContext<'_>,
) -> wfe_core::Result<ExecutionResult> {
let step_name = context.step.name.as_deref().unwrap_or("unknown"); let step_name = context.step.name.as_deref().unwrap_or("unknown");
let subcmd = self.config.command.as_str(); let subcmd = self.config.command.as_str();
@@ -133,9 +137,9 @@ impl StepBody for RustupStep {
} }
} }
} else { } else {
cmd.output() cmd.output().await.map_err(|e| {
.await WfeError::StepExecution(format!("Failed to spawn rustup {subcmd}: {e}"))
.map_err(|e| WfeError::StepExecution(format!("Failed to spawn rustup {subcmd}: {e}")))? })?
}; };
let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stdout = String::from_utf8_lossy(&output.stdout).to_string();
@@ -189,7 +193,11 @@ mod tests {
let cmd = step.build_command(); let cmd = step.build_command();
let prog = cmd.as_std().get_program().to_str().unwrap(); let prog = cmd.as_std().get_program().to_str().unwrap();
assert_eq!(prog, "sh"); assert_eq!(prog, "sh");
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
.as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert_eq!(args[0], "-c"); assert_eq!(args[0], "-c");
assert!(args[1].contains("rustup.rs")); assert!(args[1].contains("rustup.rs"));
assert!(args[1].contains("-y")); assert!(args[1].contains("-y"));
@@ -202,7 +210,11 @@ mod tests {
config.default_toolchain = Some("nightly".to_string()); config.default_toolchain = Some("nightly".to_string());
let step = RustupStep::new(config); let step = RustupStep::new(config);
let cmd = step.build_command(); let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
.as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert!(args[1].contains("--profile minimal")); assert!(args[1].contains("--profile minimal"));
assert!(args[1].contains("--default-toolchain nightly")); assert!(args[1].contains("--default-toolchain nightly"));
} }
@@ -213,7 +225,11 @@ mod tests {
config.extra_args = vec!["--no-modify-path".to_string()]; config.extra_args = vec!["--no-modify-path".to_string()];
let step = RustupStep::new(config); let step = RustupStep::new(config);
let cmd = step.build_command(); let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
.as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert!(args[1].contains("--no-modify-path")); assert!(args[1].contains("--no-modify-path"));
} }
@@ -233,8 +249,21 @@ mod tests {
let cmd = step.build_command(); let cmd = step.build_command();
let prog = cmd.as_std().get_program().to_str().unwrap(); let prog = cmd.as_std().get_program().to_str().unwrap();
assert_eq!(prog, "rustup"); assert_eq!(prog, "rustup");
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
assert_eq!(args, vec!["toolchain", "install", "nightly-2024-06-01", "--profile", "minimal"]); .as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert_eq!(
args,
vec![
"toolchain",
"install",
"nightly-2024-06-01",
"--profile",
"minimal"
]
);
} }
#[test] #[test]
@@ -251,7 +280,11 @@ mod tests {
}; };
let step = RustupStep::new(config); let step = RustupStep::new(config);
let cmd = step.build_command(); let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
.as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert_eq!(args, vec!["toolchain", "install", "stable", "--force"]); assert_eq!(args, vec!["toolchain", "install", "stable", "--force"]);
} }
@@ -271,8 +304,22 @@ mod tests {
let cmd = step.build_command(); let cmd = step.build_command();
let prog = cmd.as_std().get_program().to_str().unwrap(); let prog = cmd.as_std().get_program().to_str().unwrap();
assert_eq!(prog, "rustup"); assert_eq!(prog, "rustup");
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
assert_eq!(args, vec!["component", "add", "clippy", "rustfmt", "--toolchain", "nightly"]); .as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert_eq!(
args,
vec![
"component",
"add",
"clippy",
"rustfmt",
"--toolchain",
"nightly"
]
);
} }
#[test] #[test]
@@ -289,7 +336,11 @@ mod tests {
}; };
let step = RustupStep::new(config); let step = RustupStep::new(config);
let cmd = step.build_command(); let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
.as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert_eq!(args, vec!["component", "add", "rust-src"]); assert_eq!(args, vec!["component", "add", "rust-src"]);
} }
@@ -309,8 +360,21 @@ mod tests {
let cmd = step.build_command(); let cmd = step.build_command();
let prog = cmd.as_std().get_program().to_str().unwrap(); let prog = cmd.as_std().get_program().to_str().unwrap();
assert_eq!(prog, "rustup"); assert_eq!(prog, "rustup");
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
assert_eq!(args, vec!["target", "add", "wasm32-unknown-unknown", "--toolchain", "stable"]); .as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert_eq!(
args,
vec![
"target",
"add",
"wasm32-unknown-unknown",
"--toolchain",
"stable"
]
);
} }
#[test] #[test]
@@ -330,8 +394,20 @@ mod tests {
}; };
let step = RustupStep::new(config); let step = RustupStep::new(config);
let cmd = step.build_command(); let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
assert_eq!(args, vec!["target", "add", "wasm32-unknown-unknown", "aarch64-linux-android"]); .as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert_eq!(
args,
vec![
"target",
"add",
"wasm32-unknown-unknown",
"aarch64-linux-android"
]
);
} }
#[test] #[test]
@@ -348,10 +424,21 @@ mod tests {
}; };
let step = RustupStep::new(config); let step = RustupStep::new(config);
let cmd = step.build_command(); let cmd = step.build_command();
let args: Vec<_> = cmd.as_std().get_args().map(|a| a.to_str().unwrap()).collect(); let args: Vec<_> = cmd
.as_std()
.get_args()
.map(|a| a.to_str().unwrap())
.collect();
assert_eq!( assert_eq!(
args, args,
vec!["target", "add", "x86_64-unknown-linux-musl", "--toolchain", "nightly", "--force"] vec![
"target",
"add",
"x86_64-unknown-linux-musl",
"--toolchain",
"nightly",
"--force"
]
); );
} }
} }

View File

@@ -13,11 +13,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.build_server(true) .build_server(true)
.build_client(true) .build_client(true)
.file_descriptor_set_path(&descriptor_path) .file_descriptor_set_path(&descriptor_path)
.compile_with_config( .compile_with_config(prost_config, &proto_files, &["proto"])?;
prost_config,
&proto_files,
&["proto"],
)?;
Ok(()) Ok(())
} }

View File

@@ -17,4 +17,5 @@ pub use prost_types;
pub use tonic; pub use tonic;
/// Encoded file descriptor set for gRPC reflection. /// Encoded file descriptor set for gRPC reflection.
pub const FILE_DESCRIPTOR_SET: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/wfe_descriptor.bin")); pub const FILE_DESCRIPTOR_SET: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/wfe_descriptor.bin"));

View File

@@ -1,6 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode};
use serde::Deserialize; use serde::Deserialize;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tonic::{Request, Status}; use tonic::{Request, Status};
@@ -99,7 +99,10 @@ impl AuthState {
let resp: JwksResponse = reqwest::get(uri).await?.json().await?; let resp: JwksResponse = reqwest::get(uri).await?.json().await?;
let mut cache = self.jwks.write().await; let mut cache = self.jwks.write().await;
*cache = Some(JwksCache { keys: resp.keys }); *cache = Some(JwksCache { keys: resp.keys });
tracing::debug!(key_count = cache.as_ref().unwrap().keys.len(), "JWKS refreshed"); tracing::debug!(
key_count = cache.as_ref().unwrap().keys.len(),
"JWKS refreshed"
);
Ok(()) Ok(())
} }
@@ -128,7 +131,9 @@ impl AuthState {
/// Validate a JWT against the cached JWKS (synchronous — for use in interceptors). /// Validate a JWT against the cached JWKS (synchronous — for use in interceptors).
/// Shared logic used by both `check()` and `make_interceptor()`. /// Shared logic used by both `check()` and `make_interceptor()`.
fn validate_jwt_cached(&self, token: &str) -> Result<(), Status> { fn validate_jwt_cached(&self, token: &str) -> Result<(), Status> {
let cache = self.jwks.try_read() let cache = self
.jwks
.try_read()
.map_err(|_| Status::unavailable("JWKS refresh in progress"))?; .map_err(|_| Status::unavailable("JWKS refresh in progress"))?;
let jwks = cache let jwks = cache
.as_ref() .as_ref()
@@ -228,9 +233,7 @@ fn extract_bearer_token<T>(request: &Request<T>) -> Result<&str, Status> {
} }
/// Map JWK key algorithm to jsonwebtoken Algorithm. /// Map JWK key algorithm to jsonwebtoken Algorithm.
fn key_algorithm_to_jwt_algorithm( fn key_algorithm_to_jwt_algorithm(ka: jsonwebtoken::jwk::KeyAlgorithm) -> Option<Algorithm> {
ka: jsonwebtoken::jwk::KeyAlgorithm,
) -> Option<Algorithm> {
use jsonwebtoken::jwk::KeyAlgorithm as KA; use jsonwebtoken::jwk::KeyAlgorithm as KA;
match ka { match ka {
KA::RS256 => Some(Algorithm::RS256), KA::RS256 => Some(Algorithm::RS256),
@@ -473,7 +476,7 @@ mod tests {
issuer: &str, issuer: &str,
audience: Option<&str>, audience: Option<&str>,
) -> (Vec<jsonwebtoken::jwk::Jwk>, String) { ) -> (Vec<jsonwebtoken::jwk::Jwk>, String) {
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use rsa::RsaPrivateKey; use rsa::RsaPrivateKey;
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
@@ -498,8 +501,7 @@ mod tests {
let pem = private_key let pem = private_key
.to_pkcs1_pem(rsa::pkcs1::LineEnding::LF) .to_pkcs1_pem(rsa::pkcs1::LineEnding::LF)
.unwrap(); .unwrap();
let encoding_key = let encoding_key = jsonwebtoken::EncodingKey::from_rsa_pem(pem.as_bytes()).unwrap();
jsonwebtoken::EncodingKey::from_rsa_pem(pem.as_bytes()).unwrap();
let mut header = jsonwebtoken::Header::new(jsonwebtoken::Algorithm::RS256); let mut header = jsonwebtoken::Header::new(jsonwebtoken::Algorithm::RS256);
header.kid = Some("test-key-1".to_string()); header.kid = Some("test-key-1".to_string());
@@ -684,9 +686,18 @@ mod tests {
#[test] #[test]
fn key_algorithm_mapping() { fn key_algorithm_mapping() {
use jsonwebtoken::jwk::KeyAlgorithm as KA; use jsonwebtoken::jwk::KeyAlgorithm as KA;
assert_eq!(key_algorithm_to_jwt_algorithm(KA::RS256), Some(Algorithm::RS256)); assert_eq!(
assert_eq!(key_algorithm_to_jwt_algorithm(KA::ES256), Some(Algorithm::ES256)); key_algorithm_to_jwt_algorithm(KA::RS256),
assert_eq!(key_algorithm_to_jwt_algorithm(KA::EdDSA), Some(Algorithm::EdDSA)); Some(Algorithm::RS256)
);
assert_eq!(
key_algorithm_to_jwt_algorithm(KA::ES256),
Some(Algorithm::ES256)
);
assert_eq!(
key_algorithm_to_jwt_algorithm(KA::EdDSA),
Some(Algorithm::EdDSA)
);
// HS256 should be rejected (symmetric algorithm). // HS256 should be rejected (symmetric algorithm).
assert_eq!(key_algorithm_to_jwt_algorithm(KA::HS256), None); assert_eq!(key_algorithm_to_jwt_algorithm(KA::HS256), None);
assert_eq!(key_algorithm_to_jwt_algorithm(KA::HS384), None); assert_eq!(key_algorithm_to_jwt_algorithm(KA::HS384), None);

View File

@@ -174,10 +174,7 @@ pub fn load(cli: &Cli) -> ServerConfig {
// Persistence override. // Persistence override.
if let Some(ref backend) = cli.persistence { if let Some(ref backend) = cli.persistence {
let url = cli let url = cli.db_url.clone().unwrap_or_else(|| "wfe.db".to_string());
.db_url
.clone()
.unwrap_or_else(|| "wfe.db".to_string());
config.persistence = match backend.as_str() { config.persistence = match backend.as_str() {
"postgres" => PersistenceConfig::Postgres { url }, "postgres" => PersistenceConfig::Postgres { url },
_ => PersistenceConfig::Sqlite { path: url }, _ => PersistenceConfig::Sqlite { path: url },
@@ -231,7 +228,10 @@ mod tests {
let config = ServerConfig::default(); let config = ServerConfig::default();
assert_eq!(config.grpc_addr, "0.0.0.0:50051".parse().unwrap()); assert_eq!(config.grpc_addr, "0.0.0.0:50051".parse().unwrap());
assert_eq!(config.http_addr, "0.0.0.0:8080".parse().unwrap()); assert_eq!(config.http_addr, "0.0.0.0:8080".parse().unwrap());
assert!(matches!(config.persistence, PersistenceConfig::Sqlite { .. })); assert!(matches!(
config.persistence,
PersistenceConfig::Sqlite { .. }
));
assert!(matches!(config.queue, QueueConfig::InMemory)); assert!(matches!(config.queue, QueueConfig::InMemory));
assert!(config.search.is_none()); assert!(config.search.is_none());
assert!(config.auth.tokens.is_empty()); assert!(config.auth.tokens.is_empty());
@@ -270,11 +270,17 @@ version = 1
"#; "#;
let config: ServerConfig = toml::from_str(toml).unwrap(); let config: ServerConfig = toml::from_str(toml).unwrap();
assert_eq!(config.grpc_addr, "127.0.0.1:9090".parse().unwrap()); assert_eq!(config.grpc_addr, "127.0.0.1:9090".parse().unwrap());
assert!(matches!(config.persistence, PersistenceConfig::Postgres { .. })); assert!(matches!(
config.persistence,
PersistenceConfig::Postgres { .. }
));
assert!(matches!(config.queue, QueueConfig::Valkey { .. })); assert!(matches!(config.queue, QueueConfig::Valkey { .. }));
assert!(config.search.is_some()); assert!(config.search.is_some());
assert_eq!(config.auth.tokens.len(), 2); assert_eq!(config.auth.tokens.len(), 2);
assert_eq!(config.auth.webhook_secrets.get("github").unwrap(), "mysecret"); assert_eq!(
config.auth.webhook_secrets.get("github").unwrap(),
"mysecret"
);
assert_eq!(config.webhook.triggers.len(), 1); assert_eq!(config.webhook.triggers.len(), 1);
assert_eq!(config.webhook.triggers[0].workflow_id, "ci"); assert_eq!(config.webhook.triggers[0].workflow_id, "ci");
} }
@@ -295,8 +301,12 @@ version = 1
}; };
let config = load(&cli); let config = load(&cli);
assert_eq!(config.grpc_addr, "127.0.0.1:9999".parse().unwrap()); assert_eq!(config.grpc_addr, "127.0.0.1:9999".parse().unwrap());
assert!(matches!(config.persistence, PersistenceConfig::Postgres { ref url } if url == "postgres://db/wfe")); assert!(
assert!(matches!(config.queue, QueueConfig::Valkey { ref url } if url == "redis://valkey:6379")); matches!(config.persistence, PersistenceConfig::Postgres { ref url } if url == "postgres://db/wfe")
);
assert!(
matches!(config.queue, QueueConfig::Valkey { ref url } if url == "redis://valkey:6379")
);
assert_eq!(config.search.unwrap().url, "http://os:9200"); assert_eq!(config.search.unwrap().url, "http://os:9200");
assert_eq!(config.workflows_dir.unwrap(), PathBuf::from("/workflows")); assert_eq!(config.workflows_dir.unwrap(), PathBuf::from("/workflows"));
assert_eq!(config.auth.tokens, vec!["tok1", "tok2"]); assert_eq!(config.auth.tokens, vec!["tok1", "tok2"]);
@@ -317,7 +327,10 @@ version = 1
auth_tokens: None, auth_tokens: None,
}; };
let config = load(&cli); let config = load(&cli);
assert!(matches!(config.persistence, PersistenceConfig::Postgres { .. })); assert!(matches!(
config.persistence,
PersistenceConfig::Postgres { .. }
));
} }
// ── Security regression tests ── // ── Security regression tests ──
@@ -358,6 +371,9 @@ commit = "$.head_commit.id"
"#; "#;
let config: WebhookConfig = toml::from_str(toml).unwrap(); let config: WebhookConfig = toml::from_str(toml).unwrap();
assert_eq!(config.triggers[0].data_mapping.len(), 2); assert_eq!(config.triggers[0].data_mapping.len(), 2);
assert_eq!(config.triggers[0].data_mapping["repo"], "$.repository.full_name"); assert_eq!(
config.triggers[0].data_mapping["repo"],
"$.repository.full_name"
);
} }
} }

View File

@@ -52,9 +52,14 @@ mod tests {
let mut rx1 = bus.subscribe(); let mut rx1 = bus.subscribe();
let mut rx2 = bus.subscribe(); let mut rx2 = bus.subscribe();
bus.publish(LifecycleEvent::new("wf-1", "def-1", 1, LifecycleEventType::Completed)) bus.publish(LifecycleEvent::new(
.await "wf-1",
.unwrap(); "def-1",
1,
LifecycleEventType::Completed,
))
.await
.unwrap();
let e1 = rx1.recv().await.unwrap(); let e1 = rx1.recv().await.unwrap();
let e2 = rx2.recv().await.unwrap(); let e2 = rx2.recv().await.unwrap();
@@ -66,9 +71,14 @@ mod tests {
async fn no_subscribers_does_not_error() { async fn no_subscribers_does_not_error() {
let bus = BroadcastLifecyclePublisher::new(16); let bus = BroadcastLifecyclePublisher::new(16);
// No subscribers — should not panic. // No subscribers — should not panic.
bus.publish(LifecycleEvent::new("wf-1", "def-1", 1, LifecycleEventType::Started)) bus.publish(LifecycleEvent::new(
.await "wf-1",
.unwrap(); "def-1",
1,
LifecycleEventType::Started,
))
.await
.unwrap();
} }
#[tokio::test] #[tokio::test]

View File

@@ -304,8 +304,8 @@ mod tests {
// ── OpenSearch integration tests ──────────────────────────────── // ── OpenSearch integration tests ────────────────────────────────
fn opensearch_url() -> Option<String> { fn opensearch_url() -> Option<String> {
let url = std::env::var("WFE_SEARCH_URL") let url =
.unwrap_or_else(|_| "http://localhost:9200".to_string()); std::env::var("WFE_SEARCH_URL").unwrap_or_else(|_| "http://localhost:9200".to_string());
// Quick TCP probe to check if OpenSearch is reachable. // Quick TCP probe to check if OpenSearch is reachable.
let addr = url let addr = url
.strip_prefix("http://") .strip_prefix("http://")
@@ -340,10 +340,7 @@ mod tests {
/// Delete the test index to start clean. /// Delete the test index to start clean.
async fn cleanup_index(url: &str) { async fn cleanup_index(url: &str) {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let _ = client let _ = client.delete(format!("{url}/{LOG_INDEX}")).send().await;
.delete(format!("{url}/{LOG_INDEX}"))
.send()
.await;
} }
#[tokio::test] #[tokio::test]
@@ -375,18 +372,37 @@ mod tests {
index.ensure_index().await.unwrap(); index.ensure_index().await.unwrap();
// Index some log chunks. // Index some log chunks.
let chunk = make_test_chunk("wf-search-1", "build", LogStreamType::Stdout, "compiling wfe-core v1.5.0"); let chunk = make_test_chunk(
"wf-search-1",
"build",
LogStreamType::Stdout,
"compiling wfe-core v1.5.0",
);
index.index_chunk(&chunk).await.unwrap(); index.index_chunk(&chunk).await.unwrap();
let chunk = make_test_chunk("wf-search-1", "build", LogStreamType::Stderr, "warning: unused variable"); let chunk = make_test_chunk(
"wf-search-1",
"build",
LogStreamType::Stderr,
"warning: unused variable",
);
index.index_chunk(&chunk).await.unwrap(); index.index_chunk(&chunk).await.unwrap();
let chunk = make_test_chunk("wf-search-1", "test", LogStreamType::Stdout, "test result: ok. 79 passed"); let chunk = make_test_chunk(
"wf-search-1",
"test",
LogStreamType::Stdout,
"test result: ok. 79 passed",
);
index.index_chunk(&chunk).await.unwrap(); index.index_chunk(&chunk).await.unwrap();
// OpenSearch needs a refresh to make docs searchable. // OpenSearch needs a refresh to make docs searchable.
let client = reqwest::Client::new(); let client = reqwest::Client::new();
client.post(format!("{url}/{LOG_INDEX}/_refresh")).send().await.unwrap(); client
.post(format!("{url}/{LOG_INDEX}/_refresh"))
.send()
.await
.unwrap();
// Search by text. // Search by text.
let (results, total) = index let (results, total) = index
@@ -456,12 +472,21 @@ mod tests {
// Index 5 chunks. // Index 5 chunks.
for i in 0..5 { for i in 0..5 {
let chunk = make_test_chunk("wf-page", "build", LogStreamType::Stdout, &format!("line {i}")); let chunk = make_test_chunk(
"wf-page",
"build",
LogStreamType::Stdout,
&format!("line {i}"),
);
index.index_chunk(&chunk).await.unwrap(); index.index_chunk(&chunk).await.unwrap();
} }
let client = reqwest::Client::new(); let client = reqwest::Client::new();
client.post(format!("{url}/{LOG_INDEX}/_refresh")).send().await.unwrap(); client
.post(format!("{url}/{LOG_INDEX}/_refresh"))
.send()
.await
.unwrap();
// Get first 2. // Get first 2.
let (results, total) = index let (results, total) = index
@@ -506,11 +531,20 @@ mod tests {
let index = LogSearchIndex::new(&url).unwrap(); let index = LogSearchIndex::new(&url).unwrap();
index.ensure_index().await.unwrap(); index.ensure_index().await.unwrap();
let chunk = make_test_chunk("wf-fields", "clippy", LogStreamType::Stderr, "error: type mismatch"); let chunk = make_test_chunk(
"wf-fields",
"clippy",
LogStreamType::Stderr,
"error: type mismatch",
);
index.index_chunk(&chunk).await.unwrap(); index.index_chunk(&chunk).await.unwrap();
let client = reqwest::Client::new(); let client = reqwest::Client::new();
client.post(format!("{url}/{LOG_INDEX}/_refresh")).send().await.unwrap(); client
.post(format!("{url}/{LOG_INDEX}/_refresh"))
.send()
.await
.unwrap();
let (results, _) = index let (results, _) = index
.search("type mismatch", None, None, None, 0, 10) .search("type mismatch", None, None, None, 0, 10)

View File

@@ -109,8 +109,12 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn write_and_read_history() { async fn write_and_read_history() {
let store = LogStore::new(); let store = LogStore::new();
store.write_chunk(make_chunk("wf-1", 0, "build", "line 1\n")).await; store
store.write_chunk(make_chunk("wf-1", 0, "build", "line 2\n")).await; .write_chunk(make_chunk("wf-1", 0, "build", "line 1\n"))
.await;
store
.write_chunk(make_chunk("wf-1", 0, "build", "line 2\n"))
.await;
let history = store.get_history("wf-1", None); let history = store.get_history("wf-1", None);
assert_eq!(history.len(), 2); assert_eq!(history.len(), 2);
@@ -121,8 +125,12 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn history_filtered_by_step() { async fn history_filtered_by_step() {
let store = LogStore::new(); let store = LogStore::new();
store.write_chunk(make_chunk("wf-1", 0, "build", "build log\n")).await; store
store.write_chunk(make_chunk("wf-1", 1, "test", "test log\n")).await; .write_chunk(make_chunk("wf-1", 0, "build", "build log\n"))
.await;
store
.write_chunk(make_chunk("wf-1", 1, "test", "test log\n"))
.await;
let build_only = store.get_history("wf-1", Some(0)); let build_only = store.get_history("wf-1", Some(0));
assert_eq!(build_only.len(), 1); assert_eq!(build_only.len(), 1);
@@ -144,7 +152,9 @@ mod tests {
let store = LogStore::new(); let store = LogStore::new();
let mut rx = store.subscribe("wf-1"); let mut rx = store.subscribe("wf-1");
store.write_chunk(make_chunk("wf-1", 0, "build", "hello\n")).await; store
.write_chunk(make_chunk("wf-1", 0, "build", "hello\n"))
.await;
let received = rx.recv().await.unwrap(); let received = rx.recv().await.unwrap();
assert_eq!(received.data, b"hello\n"); assert_eq!(received.data, b"hello\n");
@@ -157,8 +167,12 @@ mod tests {
let mut rx1 = store.subscribe("wf-1"); let mut rx1 = store.subscribe("wf-1");
let mut rx2 = store.subscribe("wf-2"); let mut rx2 = store.subscribe("wf-2");
store.write_chunk(make_chunk("wf-1", 0, "build", "wf1 log\n")).await; store
store.write_chunk(make_chunk("wf-2", 0, "test", "wf2 log\n")).await; .write_chunk(make_chunk("wf-1", 0, "build", "wf1 log\n"))
.await;
store
.write_chunk(make_chunk("wf-2", 0, "test", "wf2 log\n"))
.await;
let e1 = rx1.recv().await.unwrap(); let e1 = rx1.recv().await.unwrap();
assert_eq!(e1.workflow_id, "wf-1"); assert_eq!(e1.workflow_id, "wf-1");
@@ -171,7 +185,9 @@ mod tests {
async fn no_subscribers_does_not_error() { async fn no_subscribers_does_not_error() {
let store = LogStore::new(); let store = LogStore::new();
// No subscribers — should not panic. // No subscribers — should not panic.
store.write_chunk(make_chunk("wf-1", 0, "build", "orphan log\n")).await; store
.write_chunk(make_chunk("wf-1", 0, "build", "orphan log\n"))
.await;
// History should still be stored. // History should still be stored.
assert_eq!(store.get_history("wf-1", None).len(), 1); assert_eq!(store.get_history("wf-1", None).len(), 1);
} }
@@ -182,7 +198,9 @@ mod tests {
let mut rx1 = store.subscribe("wf-1"); let mut rx1 = store.subscribe("wf-1");
let mut rx2 = store.subscribe("wf-1"); let mut rx2 = store.subscribe("wf-1");
store.write_chunk(make_chunk("wf-1", 0, "build", "shared\n")).await; store
.write_chunk(make_chunk("wf-1", 0, "build", "shared\n"))
.await;
let e1 = rx1.recv().await.unwrap(); let e1 = rx1.recv().await.unwrap();
let e2 = rx2.recv().await.unwrap(); let e2 = rx2.recv().await.unwrap();

View File

@@ -152,9 +152,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
wfe_service = wfe_service.with_log_search(index); wfe_service = wfe_service.with_log_search(index);
} }
let (health_reporter, health_service) = tonic_health::server::health_reporter(); let (health_reporter, health_service) = tonic_health::server::health_reporter();
health_reporter health_reporter.set_serving::<WfeServer<WfeService>>().await;
.set_serving::<WfeServer<WfeService>>()
.await;
// 11. Build auth state. // 11. Build auth state.
let auth_state = Arc::new(auth::AuthState::new(config.auth.clone()).await); let auth_state = Arc::new(auth::AuthState::new(config.auth.clone()).await);
@@ -168,13 +166,31 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// HIGH-08: Limit webhook payload size to 2 MB to prevent OOM DoS. // HIGH-08: Limit webhook payload size to 2 MB to prevent OOM DoS.
let http_router = axum::Router::new() let http_router = axum::Router::new()
.route("/webhooks/events", axum::routing::post(webhook::handle_generic_event)) .route(
.route("/webhooks/github", axum::routing::post(webhook::handle_github_webhook)) "/webhooks/events",
.route("/webhooks/gitea", axum::routing::post(webhook::handle_gitea_webhook)) axum::routing::post(webhook::handle_generic_event),
)
.route(
"/webhooks/github",
axum::routing::post(webhook::handle_github_webhook),
)
.route(
"/webhooks/gitea",
axum::routing::post(webhook::handle_gitea_webhook),
)
.route("/healthz", axum::routing::get(webhook::health_check)) .route("/healthz", axum::routing::get(webhook::health_check))
.route("/schema/workflow.proto", axum::routing::get(serve_proto_schema)) .route(
.route("/schema/workflow.json", axum::routing::get(serve_json_schema)) "/schema/workflow.proto",
.route("/schema/workflow.yaml", axum::routing::get(serve_yaml_example)) axum::routing::get(serve_proto_schema),
)
.route(
"/schema/workflow.json",
axum::routing::get(serve_json_schema),
)
.route(
"/schema/workflow.yaml",
axum::routing::get(serve_yaml_example),
)
.layer(axum::extract::DefaultBodyLimit::max(2 * 1024 * 1024)) .layer(axum::extract::DefaultBodyLimit::max(2 * 1024 * 1024))
.with_state(webhook_state); .with_state(webhook_state);
@@ -234,7 +250,10 @@ async fn load_yaml_definitions(host: &wfe::WorkflowHost, dir: &std::path::Path)
for entry in entries.flatten() { for entry in entries.flatten() {
let path = entry.path(); let path = entry.path();
if path.extension().is_some_and(|ext| ext == "yaml" || ext == "yml") { if path
.extension()
.is_some_and(|ext| ext == "yaml" || ext == "yml")
{
match wfe_yaml::load_workflow_from_str( match wfe_yaml::load_workflow_from_str(
&std::fs::read_to_string(&path).unwrap_or_default(), &std::fs::read_to_string(&path).unwrap_or_default(),
&config, &config,
@@ -261,7 +280,10 @@ async fn load_yaml_definitions(host: &wfe::WorkflowHost, dir: &std::path::Path)
/// Serve the raw .proto schema file. /// Serve the raw .proto schema file.
async fn serve_proto_schema() -> impl axum::response::IntoResponse { async fn serve_proto_schema() -> impl axum::response::IntoResponse {
( (
[(axum::http::header::CONTENT_TYPE, "text/plain; charset=utf-8")], [(
axum::http::header::CONTENT_TYPE,
"text/plain; charset=utf-8",
)],
include_str!("../../wfe-server-protos/proto/wfe/v1/wfe.proto"), include_str!("../../wfe-server-protos/proto/wfe/v1/wfe.proto"),
) )
} }

View File

@@ -1,10 +1,10 @@
use std::sync::Arc; use std::sync::Arc;
use axum::Json;
use axum::body::Bytes; use axum::body::Bytes;
use axum::extract::State; use axum::extract::State;
use axum::http::{HeaderMap, StatusCode}; use axum::http::{HeaderMap, StatusCode};
use axum::response::IntoResponse; use axum::response::IntoResponse;
use axum::Json;
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac};
use sha2::Sha256; use sha2::Sha256;
@@ -107,7 +107,11 @@ pub async fn handle_github_webhook(
// Publish as event (for workflows waiting on events). // Publish as event (for workflows waiting on events).
if let Err(e) = state if let Err(e) = state
.host .host
.publish_event(&forge_event.event_name, &forge_event.event_key, forge_event.data.clone()) .publish_event(
&forge_event.event_name,
&forge_event.event_key,
forge_event.data.clone(),
)
.await .await
{ {
tracing::error!(error = %e, "failed to publish forge event"); tracing::error!(error = %e, "failed to publish forge event");
@@ -208,7 +212,11 @@ pub async fn handle_gitea_webhook(
if let Err(e) = state if let Err(e) = state
.host .host
.publish_event(&forge_event.event_name, &forge_event.event_key, forge_event.data.clone()) .publish_event(
&forge_event.event_name,
&forge_event.event_key,
forge_event.data.clone(),
)
.await .await
{ {
tracing::error!(error = %e, "failed to publish forge event"); tracing::error!(error = %e, "failed to publish forge event");
@@ -362,10 +370,7 @@ fn map_forge_event(event_type: &str, payload: &serde_json::Value) -> ForgeEvent
/// Extract data fields from payload using simple JSONPath-like mapping. /// Extract data fields from payload using simple JSONPath-like mapping.
/// Supports `$.field.nested` syntax. /// Supports `$.field.nested` syntax.
fn map_trigger_data( fn map_trigger_data(trigger: &WebhookTrigger, payload: &serde_json::Value) -> serde_json::Value {
trigger: &WebhookTrigger,
payload: &serde_json::Value,
) -> serde_json::Value {
let mut data = serde_json::Map::new(); let mut data = serde_json::Map::new();
for (key, path) in &trigger.data_mapping { for (key, path) in &trigger.data_mapping {
if let Some(value) = resolve_json_path(payload, path) { if let Some(value) = resolve_json_path(payload, path) {
@@ -376,7 +381,10 @@ fn map_trigger_data(
} }
/// Resolve a simple JSONPath expression like `$.repository.full_name`. /// Resolve a simple JSONPath expression like `$.repository.full_name`.
fn resolve_json_path<'a>(value: &'a serde_json::Value, path: &str) -> Option<&'a serde_json::Value> { fn resolve_json_path<'a>(
value: &'a serde_json::Value,
path: &str,
) -> Option<&'a serde_json::Value> {
let path = path.strip_prefix("$.").unwrap_or(path); let path = path.strip_prefix("$.").unwrap_or(path);
let mut current = value; let mut current = value;
for segment in path.split('.') { for segment in path.split('.') {

View File

@@ -28,10 +28,7 @@ impl LifecyclePublisher for ValkeyLifecyclePublisher {
let mut conn = self.conn.clone(); let mut conn = self.conn.clone();
let json = serde_json::to_string(&event)?; let json = serde_json::to_string(&event)?;
let instance_channel = format!( let instance_channel = format!("{}:lifecycle:{}", self.prefix, event.workflow_instance_id);
"{}:lifecycle:{}",
self.prefix, event.workflow_instance_id
);
let all_channel = format!("{}:lifecycle:all", self.prefix); let all_channel = format!("{}:lifecycle:all", self.prefix);
// Publish to the instance-specific channel. // Publish to the instance-specific channel.

View File

@@ -17,8 +17,9 @@ async fn publish_subscribe_round_trip() {
} }
let prefix = format!("wfe_test_{}", uuid::Uuid::new_v4().simple()); let prefix = format!("wfe_test_{}", uuid::Uuid::new_v4().simple());
let publisher = let publisher = wfe_valkey::ValkeyLifecyclePublisher::new("redis://localhost:6379", &prefix)
wfe_valkey::ValkeyLifecyclePublisher::new("redis://localhost:6379", &prefix).await.unwrap(); .await
.unwrap();
let instance_id = "wf-lifecycle-test-1"; let instance_id = "wf-lifecycle-test-1";
let channel = format!("{}:lifecycle:{}", prefix, instance_id); let channel = format!("{}:lifecycle:{}", prefix, instance_id);
@@ -42,12 +43,7 @@ async fn publish_subscribe_round_trip() {
// Small delay to ensure the subscription is active before publishing. // Small delay to ensure the subscription is active before publishing.
tokio::time::sleep(Duration::from_millis(200)).await; tokio::time::sleep(Duration::from_millis(200)).await;
let event = LifecycleEvent::new( let event = LifecycleEvent::new(instance_id, "def-1", 1, LifecycleEventType::Started);
instance_id,
"def-1",
1,
LifecycleEventType::Started,
);
publisher.publish(event).await.unwrap(); publisher.publish(event).await.unwrap();
// Wait for the message with a timeout. // Wait for the message with a timeout.
@@ -71,8 +67,9 @@ async fn publish_to_all_channel() {
} }
let prefix = format!("wfe_test_{}", uuid::Uuid::new_v4().simple()); let prefix = format!("wfe_test_{}", uuid::Uuid::new_v4().simple());
let publisher = let publisher = wfe_valkey::ValkeyLifecyclePublisher::new("redis://localhost:6379", &prefix)
wfe_valkey::ValkeyLifecyclePublisher::new("redis://localhost:6379", &prefix).await.unwrap(); .await
.unwrap();
let all_channel = format!("{}:lifecycle:all", prefix); let all_channel = format!("{}:lifecycle:all", prefix);
@@ -93,12 +90,7 @@ async fn publish_to_all_channel() {
tokio::time::sleep(Duration::from_millis(200)).await; tokio::time::sleep(Duration::from_millis(200)).await;
let event = LifecycleEvent::new( let event = LifecycleEvent::new("wf-all-test", "def-1", 1, LifecycleEventType::Completed);
"wf-all-test",
"def-1",
1,
LifecycleEventType::Completed,
);
publisher.publish(event).await.unwrap(); publisher.publish(event).await.unwrap();
let received = tokio::time::timeout(Duration::from_secs(5), rx.recv()) let received = tokio::time::timeout(Duration::from_secs(5), rx.recv())

View File

@@ -2,7 +2,9 @@ use wfe_core::lock_suite;
async fn make_provider() -> wfe_valkey::ValkeyLockProvider { async fn make_provider() -> wfe_valkey::ValkeyLockProvider {
let prefix = format!("wfe_test_{}", uuid::Uuid::new_v4().simple()); let prefix = format!("wfe_test_{}", uuid::Uuid::new_v4().simple());
wfe_valkey::ValkeyLockProvider::new("redis://localhost:6379", &prefix).await.unwrap() wfe_valkey::ValkeyLockProvider::new("redis://localhost:6379", &prefix)
.await
.unwrap()
} }
lock_suite!(make_provider); lock_suite!(make_provider);

View File

@@ -2,7 +2,9 @@ use wfe_core::queue_suite;
async fn make_provider() -> wfe_valkey::ValkeyQueueProvider { async fn make_provider() -> wfe_valkey::ValkeyQueueProvider {
let prefix = format!("wfe_test_{}", uuid::Uuid::new_v4().simple()); let prefix = format!("wfe_test_{}", uuid::Uuid::new_v4().simple());
wfe_valkey::ValkeyQueueProvider::new("redis://localhost:6379", &prefix).await.unwrap() wfe_valkey::ValkeyQueueProvider::new("redis://localhost:6379", &prefix)
.await
.unwrap()
} }
queue_suite!(make_provider); queue_suite!(make_provider);

View File

@@ -96,15 +96,13 @@ impl ModuleLoader for WfeModuleLoader {
// Relative or bare path — resolve against referrer. // Relative or bare path — resolve against referrer.
// This handles ./foo, ../foo, and /foo (absolute path on same origin, e.g. esm.sh redirects) // This handles ./foo, ../foo, and /foo (absolute path on same origin, e.g. esm.sh redirects)
if specifier.starts_with("./") if specifier.starts_with("./") || specifier.starts_with("../") || specifier.starts_with('/')
|| specifier.starts_with("../")
|| specifier.starts_with('/')
{ {
let base = ModuleSpecifier::parse(referrer) let base = ModuleSpecifier::parse(referrer)
.map_err(|e| JsErrorBox::generic(format!("Invalid referrer '{referrer}': {e}")))?; .map_err(|e| JsErrorBox::generic(format!("Invalid referrer '{referrer}': {e}")))?;
let resolved = base let resolved = base.join(specifier).map_err(|e| {
.join(specifier) JsErrorBox::generic(format!("Failed to resolve '{specifier}': {e}"))
.map_err(|e| JsErrorBox::generic(format!("Failed to resolve '{specifier}': {e}")))?; })?;
// Check permissions based on scheme. // Check permissions based on scheme.
match resolved.scheme() { match resolved.scheme() {
@@ -172,11 +170,9 @@ impl ModuleLoader for WfeModuleLoader {
.map_err(|e| JsErrorBox::new("PermissionError", e.to_string()))?; .map_err(|e| JsErrorBox::new("PermissionError", e.to_string()))?;
} }
let response = reqwest::get(&url) let response = reqwest::get(&url).await.map_err(|e| {
.await JsErrorBox::generic(format!("Failed to fetch module '{url}': {e}"))
.map_err(|e| { })?;
JsErrorBox::generic(format!("Failed to fetch module '{url}': {e}"))
})?;
if !response.status().is_success() { if !response.status().is_success() {
return Err(JsErrorBox::generic(format!( return Err(JsErrorBox::generic(format!(
@@ -224,9 +220,10 @@ impl ModuleLoader for WfeModuleLoader {
&specifier, &specifier,
None, None,
))), ))),
Err(e) => ModuleLoadResponse::Sync(Err(JsErrorBox::generic( Err(e) => ModuleLoadResponse::Sync(Err(JsErrorBox::generic(format!(
format!("Failed to read module '{}': {e}", path.display()), "Failed to read module '{}': {e}",
))), path.display()
)))),
} }
} }
Err(e) => ModuleLoadResponse::Sync(Err(e)), Err(e) => ModuleLoadResponse::Sync(Err(e)),
@@ -274,7 +271,11 @@ mod tests {
..Default::default() ..Default::default()
}); });
let result = loader let result = loader
.resolve("npm:lodash@4", "ext:wfe/bootstrap.js", ResolutionKind::Import) .resolve(
"npm:lodash@4",
"ext:wfe/bootstrap.js",
ResolutionKind::Import,
)
.unwrap(); .unwrap();
assert_eq!(result.as_str(), "https://esm.sh/lodash@4"); assert_eq!(result.as_str(), "https://esm.sh/lodash@4");
} }
@@ -304,7 +305,12 @@ mod tests {
ResolutionKind::Import, ResolutionKind::Import,
); );
assert!(result.is_err()); assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Permission denied")); assert!(
result
.unwrap_err()
.to_string()
.contains("Permission denied")
);
} }
#[test] #[test]
@@ -320,10 +326,12 @@ mod tests {
ResolutionKind::DynamicImport, ResolutionKind::DynamicImport,
); );
assert!(result.is_err()); assert!(result.is_err());
assert!(result assert!(
.unwrap_err() result
.to_string() .unwrap_err()
.contains("Dynamic import is not allowed")); .to_string()
.contains("Dynamic import is not allowed")
);
} }
#[test] #[test]
@@ -361,11 +369,7 @@ mod tests {
..Default::default() ..Default::default()
}); });
let result = loader let result = loader
.resolve( .resolve("./helper.js", "file:///tmp/main.js", ResolutionKind::Import)
"./helper.js",
"file:///tmp/main.js",
ResolutionKind::Import,
)
.unwrap(); .unwrap();
assert_eq!(result.as_str(), "file:///tmp/helper.js"); assert_eq!(result.as_str(), "file:///tmp/helper.js");
} }

View File

@@ -2,8 +2,8 @@ use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
use std::rc::Rc; use std::rc::Rc;
use deno_core::op2;
use deno_core::OpState; use deno_core::OpState;
use deno_core::op2;
use deno_error::JsErrorBox; use deno_error::JsErrorBox;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View File

@@ -1,7 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use deno_core::op2;
use deno_core::OpState; use deno_core::OpState;
use deno_core::op2;
/// Workflow data available to the script via `inputs()`. /// Workflow data available to the script via `inputs()`.
pub struct WorkflowInputs { pub struct WorkflowInputs {
@@ -28,11 +28,7 @@ pub fn op_inputs(state: &mut OpState) -> serde_json::Value {
/// Stores a key/value pair in the step outputs. /// Stores a key/value pair in the step outputs.
#[op2] #[op2]
pub fn op_output( pub fn op_output(state: &mut OpState, #[string] key: String, #[serde] value: serde_json::Value) {
state: &mut OpState,
#[string] key: String,
#[serde] value: serde_json::Value,
) {
let outputs = state.borrow_mut::<StepOutputs>(); let outputs = state.borrow_mut::<StepOutputs>();
outputs.map.insert(key, value); outputs.map.insert(key, value);
} }
@@ -56,7 +52,8 @@ pub async fn op_read_file(
{ {
let s = state.borrow(); let s = state.borrow();
let checker = s.borrow::<super::super::permissions::PermissionChecker>(); let checker = s.borrow::<super::super::permissions::PermissionChecker>();
checker.check_read(&path) checker
.check_read(&path)
.map_err(|e| deno_error::JsErrorBox::new("PermissionError", e.to_string()))?; .map_err(|e| deno_error::JsErrorBox::new("PermissionError", e.to_string()))?;
} }
tokio::fs::read_to_string(&path) tokio::fs::read_to_string(&path)
@@ -66,7 +63,13 @@ pub async fn op_read_file(
deno_core::extension!( deno_core::extension!(
wfe_ops, wfe_ops,
ops = [op_inputs, op_output, op_log, op_read_file, super::http::op_fetch], ops = [
op_inputs,
op_output,
op_log,
op_read_file,
super::http::op_fetch
],
esm_entry_point = "ext:wfe/bootstrap.js", esm_entry_point = "ext:wfe/bootstrap.js",
esm = ["ext:wfe/bootstrap.js" = "src/executors/deno/js/bootstrap.js"], esm = ["ext:wfe/bootstrap.js" = "src/executors/deno/js/bootstrap.js"],
); );

View File

@@ -120,9 +120,9 @@ impl PermissionChecker {
/// Detect `..` path traversal components. /// Detect `..` path traversal components.
fn has_traversal(path: &str) -> bool { fn has_traversal(path: &str) -> bool {
Path::new(path).components().any(|c| { Path::new(path)
matches!(c, std::path::Component::ParentDir) .components()
}) .any(|c| matches!(c, std::path::Component::ParentDir))
} }
} }
@@ -130,12 +130,7 @@ impl PermissionChecker {
mod tests { mod tests {
use super::*; use super::*;
fn perms( fn perms(net: &[&str], read: &[&str], write: &[&str], env: &[&str]) -> PermissionChecker {
net: &[&str],
read: &[&str],
write: &[&str],
env: &[&str],
) -> PermissionChecker {
PermissionChecker::from_config(&DenoPermissions { PermissionChecker::from_config(&DenoPermissions {
net: net.iter().map(|s| s.to_string()).collect(), net: net.iter().map(|s| s.to_string()).collect(),
read: read.iter().map(|s| s.to_string()).collect(), read: read.iter().map(|s| s.to_string()).collect(),
@@ -182,9 +177,7 @@ mod tests {
#[test] #[test]
fn read_path_traversal_blocked() { fn read_path_traversal_blocked() {
let checker = perms(&[], &["/tmp"], &[], &[]); let checker = perms(&[], &["/tmp"], &[], &[]);
let err = checker let err = checker.check_read("/tmp/../../../etc/passwd").unwrap_err();
.check_read("/tmp/../../../etc/passwd")
.unwrap_err();
assert_eq!(err.kind, "read"); assert_eq!(err.kind, "read");
assert!(err.resource.contains("..")); assert!(err.resource.contains(".."));
} }
@@ -205,9 +198,7 @@ mod tests {
#[test] #[test]
fn write_path_traversal_blocked() { fn write_path_traversal_blocked() {
let checker = perms(&[], &[], &["/tmp/out"], &[]); let checker = perms(&[], &[], &["/tmp/out"], &[]);
assert!(checker assert!(checker.check_write("/tmp/out/../../etc/shadow").is_err());
.check_write("/tmp/out/../../etc/shadow")
.is_err());
} }
#[test] #[test]

View File

@@ -8,7 +8,7 @@ use wfe_core::WfeError;
use super::config::DenoConfig; use super::config::DenoConfig;
use super::module_loader::WfeModuleLoader; use super::module_loader::WfeModuleLoader;
use super::ops::workflow::{wfe_ops, StepMeta, StepOutputs, WorkflowInputs}; use super::ops::workflow::{StepMeta, StepOutputs, WorkflowInputs, wfe_ops};
use super::permissions::PermissionChecker; use super::permissions::PermissionChecker;
/// Create a configured `JsRuntime` for executing a workflow step script. /// Create a configured `JsRuntime` for executing a workflow step script.
@@ -61,8 +61,8 @@ pub fn would_auto_add_esm_sh(config: &DenoConfig) -> bool {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use super::super::config::DenoPermissions; use super::super::config::DenoPermissions;
use super::*;
#[test] #[test]
fn create_runtime_succeeds() { fn create_runtime_succeeds() {

View File

@@ -1,7 +1,7 @@
use async_trait::async_trait; use async_trait::async_trait;
use wfe_core::WfeError;
use wfe_core::models::ExecutionResult; use wfe_core::models::ExecutionResult;
use wfe_core::traits::step::{StepBody, StepExecutionContext}; use wfe_core::traits::step::{StepBody, StepExecutionContext};
use wfe_core::WfeError;
use super::config::DenoConfig; use super::config::DenoConfig;
use super::ops::workflow::StepOutputs; use super::ops::workflow::StepOutputs;
@@ -95,7 +95,9 @@ impl StepBody for DenoStep {
/// Check if the source code uses ES module syntax or top-level await. /// Check if the source code uses ES module syntax or top-level await.
fn needs_module_evaluation(source: &str) -> bool { fn needs_module_evaluation(source: &str) -> bool {
// Top-level await requires module evaluation. ES import/export also require it. // Top-level await requires module evaluation. ES import/export also require it.
source.contains("import ") || source.contains("import(") || source.contains("export ") source.contains("import ")
|| source.contains("import(")
|| source.contains("export ")
|| source.contains("await ") || source.contains("await ")
} }
@@ -191,9 +193,8 @@ async fn run_module_inner(
"wfe:///inline-module.js".to_string() "wfe:///inline-module.js".to_string()
}; };
let specifier = deno_core::ModuleSpecifier::parse(&module_url).map_err(|e| { let specifier = deno_core::ModuleSpecifier::parse(&module_url)
WfeError::StepExecution(format!("Invalid module URL '{module_url}': {e}")) .map_err(|e| WfeError::StepExecution(format!("Invalid module URL '{module_url}': {e}")))?;
})?;
let module_id = runtime let module_id = runtime
.load_main_es_module_from_code(&specifier, source.to_string()) .load_main_es_module_from_code(&specifier, source.to_string())

View File

@@ -2,9 +2,9 @@ use std::collections::HashMap;
use async_trait::async_trait; use async_trait::async_trait;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use wfe_core::WfeError;
use wfe_core::models::ExecutionResult; use wfe_core::models::ExecutionResult;
use wfe_core::traits::step::{StepBody, StepExecutionContext}; use wfe_core::traits::step::{StepBody, StepExecutionContext};
use wfe_core::WfeError;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShellConfig { pub struct ShellConfig {
@@ -31,8 +31,15 @@ impl ShellStep {
// Inject workflow data as UPPER_CASE env vars (top-level keys only). // Inject workflow data as UPPER_CASE env vars (top-level keys only).
// Skip keys that would override security-sensitive environment variables. // Skip keys that would override security-sensitive environment variables.
const BLOCKED_KEYS: &[&str] = &[ const BLOCKED_KEYS: &[&str] = &[
"PATH", "LD_PRELOAD", "LD_LIBRARY_PATH", "DYLD_LIBRARY_PATH", "PATH",
"HOME", "SHELL", "USER", "LOGNAME", "TERM", "LD_PRELOAD",
"LD_LIBRARY_PATH",
"DYLD_LIBRARY_PATH",
"HOME",
"SHELL",
"USER",
"LOGNAME",
"TERM",
]; ];
if let Some(data_obj) = context.workflow.data.as_object() { if let Some(data_obj) = context.workflow.data.as_object() {
for (key, value) in data_obj { for (key, value) in data_obj {
@@ -78,19 +85,25 @@ impl ShellStep {
let workflow_id = context.workflow.id.clone(); let workflow_id = context.workflow.id.clone();
let definition_id = context.workflow.workflow_definition_id.clone(); let definition_id = context.workflow.workflow_definition_id.clone();
let step_id = context.step.id; let step_id = context.step.id;
let step_name = context.step.name.clone().unwrap_or_else(|| "unknown".to_string()); let step_name = context
.step
.name
.clone()
.unwrap_or_else(|| "unknown".to_string());
let mut cmd = self.build_command(context); let mut cmd = self.build_command(context);
let mut child = cmd.spawn().map_err(|e| { let mut child = cmd
WfeError::StepExecution(format!("Failed to spawn shell command: {e}")) .spawn()
})?; .map_err(|e| WfeError::StepExecution(format!("Failed to spawn shell command: {e}")))?;
let stdout_pipe = child.stdout.take().ok_or_else(|| { let stdout_pipe = child
WfeError::StepExecution("failed to capture stdout pipe".to_string()) .stdout
})?; .take()
let stderr_pipe = child.stderr.take().ok_or_else(|| { .ok_or_else(|| WfeError::StepExecution("failed to capture stdout pipe".to_string()))?;
WfeError::StepExecution("failed to capture stderr pipe".to_string()) let stderr_pipe = child
})?; .stderr
.take()
.ok_or_else(|| WfeError::StepExecution("failed to capture stderr pipe".to_string()))?;
let mut stdout_lines = BufReader::new(stdout_pipe).lines(); let mut stdout_lines = BufReader::new(stdout_pipe).lines();
let mut stderr_lines = BufReader::new(stderr_pipe).lines(); let mut stderr_lines = BufReader::new(stderr_pipe).lines();
@@ -194,9 +207,9 @@ impl ShellStep {
} }
} }
} else { } else {
cmd.output() cmd.output().await.map_err(|e| {
.await WfeError::StepExecution(format!("Failed to spawn shell command: {e}"))
.map_err(|e| WfeError::StepExecution(format!("Failed to spawn shell command: {e}")))? })?
}; };
let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stdout = String::from_utf8_lossy(&output.stdout).to_string();
@@ -209,7 +222,10 @@ impl ShellStep {
#[async_trait] #[async_trait]
impl StepBody for ShellStep { impl StepBody for ShellStep {
async fn run(&mut self, context: &StepExecutionContext<'_>) -> wfe_core::Result<ExecutionResult> { async fn run(
&mut self,
context: &StepExecutionContext<'_>,
) -> wfe_core::Result<ExecutionResult> {
let (stdout, stderr, exit_code) = if context.log_sink.is_some() { let (stdout, stderr, exit_code) = if context.log_sink.is_some() {
self.run_streaming(context).await? self.run_streaming(context).await?
} else { } else {

View File

@@ -9,8 +9,8 @@ pub mod validation;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::path::Path; use std::path::Path;
use serde::de::Error as _;
use serde::Deserialize; use serde::Deserialize;
use serde::de::Error as _;
use crate::compiler::CompiledWorkflow; use crate::compiler::CompiledWorkflow;
use crate::error::YamlWorkflowError; use crate::error::YamlWorkflowError;
@@ -50,8 +50,11 @@ pub fn load_workflow_from_str(
// Parse to a generic YAML value first, then resolve merge keys (<<:). // Parse to a generic YAML value first, then resolve merge keys (<<:).
// This adds YAML 1.1 merge key support on top of serde_yaml 0.9's YAML 1.2 parser. // This adds YAML 1.1 merge key support on top of serde_yaml 0.9's YAML 1.2 parser.
let raw_value: serde_yaml::Value = serde_yaml::from_str(&interpolated)?; let raw_value: serde_yaml::Value = serde_yaml::from_str(&interpolated)?;
let merged_value = yaml_merge_keys::merge_keys_serde(raw_value) let merged_value = yaml_merge_keys::merge_keys_serde(raw_value).map_err(|e| {
.map_err(|e| YamlWorkflowError::Parse(serde_yaml::Error::custom(format!("merge key resolution failed: {e}"))))?; YamlWorkflowError::Parse(serde_yaml::Error::custom(format!(
"merge key resolution failed: {e}"
)))
})?;
// Deserialize the merge-resolved value into our schema. // Deserialize the merge-resolved value into our schema.
let file: schema::YamlWorkflowFile = serde_yaml::from_value(merged_value)?; let file: schema::YamlWorkflowFile = serde_yaml::from_value(merged_value)?;
@@ -108,12 +111,11 @@ pub fn load_workflow_with_includes(
let interpolated = interpolation::interpolate(yaml, config)?; let interpolated = interpolation::interpolate(yaml, config)?;
let raw_value: serde_yaml::Value = serde_yaml::from_str(&interpolated)?; let raw_value: serde_yaml::Value = serde_yaml::from_str(&interpolated)?;
let merged_value = yaml_merge_keys::merge_keys_serde(raw_value) let merged_value = yaml_merge_keys::merge_keys_serde(raw_value).map_err(|e| {
.map_err(|e| { YamlWorkflowError::Parse(serde_yaml::Error::custom(format!(
YamlWorkflowError::Parse(serde_yaml::Error::custom(format!( "merge key resolution failed: {e}"
"merge key resolution failed: {e}" )))
))) })?;
})?;
let with_includes: YamlWorkflowFileWithIncludes = serde_yaml::from_value(merged_value)?; let with_includes: YamlWorkflowFileWithIncludes = serde_yaml::from_value(merged_value)?;
@@ -121,13 +123,11 @@ pub fn load_workflow_with_includes(
// Process includes. // Process includes.
for include_path_str in &with_includes.include { for include_path_str in &with_includes.include {
let include_path = base_path.parent().unwrap_or(base_path).join(include_path_str); let include_path = base_path
load_includes_recursive( .parent()
&include_path, .unwrap_or(base_path)
config, .join(include_path_str);
&mut main_specs, load_includes_recursive(&include_path, config, &mut main_specs, &mut visited)?;
&mut visited,
)?;
} }
// Main file takes precedence: included specs are only added if their ID // Main file takes precedence: included specs are only added if their ID
@@ -149,14 +149,12 @@ fn load_includes_recursive(
specs: &mut Vec<schema::WorkflowSpec>, specs: &mut Vec<schema::WorkflowSpec>,
visited: &mut HashSet<String>, visited: &mut HashSet<String>,
) -> Result<(), YamlWorkflowError> { ) -> Result<(), YamlWorkflowError> {
let canonical = path let canonical = path.canonicalize().map_err(|e| {
.canonicalize() YamlWorkflowError::Io(std::io::Error::new(
.map_err(|e| { std::io::ErrorKind::NotFound,
YamlWorkflowError::Io(std::io::Error::new( format!("Include file not found: {}: {e}", path.display()),
std::io::ErrorKind::NotFound, ))
format!("Include file not found: {}: {e}", path.display()), })?;
))
})?;
let canonical_str = canonical.to_string_lossy().to_string(); let canonical_str = canonical.to_string_lossy().to_string();
if !visited.insert(canonical_str.clone()) { if !visited.insert(canonical_str.clone()) {
@@ -169,12 +167,11 @@ fn load_includes_recursive(
let yaml = std::fs::read_to_string(&canonical)?; let yaml = std::fs::read_to_string(&canonical)?;
let interpolated = interpolation::interpolate(&yaml, config)?; let interpolated = interpolation::interpolate(&yaml, config)?;
let raw_value: serde_yaml::Value = serde_yaml::from_str(&interpolated)?; let raw_value: serde_yaml::Value = serde_yaml::from_str(&interpolated)?;
let merged_value = yaml_merge_keys::merge_keys_serde(raw_value) let merged_value = yaml_merge_keys::merge_keys_serde(raw_value).map_err(|e| {
.map_err(|e| { YamlWorkflowError::Parse(serde_yaml::Error::custom(format!(
YamlWorkflowError::Parse(serde_yaml::Error::custom(format!( "merge key resolution failed: {e}"
"merge key resolution failed: {e}" )))
))) })?;
})?;
let with_includes: YamlWorkflowFileWithIncludes = serde_yaml::from_value(merged_value)?; let with_includes: YamlWorkflowFileWithIncludes = serde_yaml::from_value(merged_value)?;
@@ -190,7 +187,10 @@ fn load_includes_recursive(
// Recurse into nested includes. // Recurse into nested includes.
for nested_include in &with_includes.include { for nested_include in &with_includes.include {
let nested_path = canonical.parent().unwrap_or(&canonical).join(nested_include); let nested_path = canonical
.parent()
.unwrap_or(&canonical)
.join(nested_include);
load_includes_recursive(&nested_path, config, specs, visited)?; load_includes_recursive(&nested_path, config, specs, visited)?;
} }

View File

@@ -59,7 +59,9 @@ pub fn parse_type_string(s: &str) -> Result<SchemaType, String> {
// Check for generic types: list<...> or map<...> // Check for generic types: list<...> or map<...>
if let Some(inner_start) = s.find('<') { if let Some(inner_start) = s.find('<') {
if !s.ends_with('>') { if !s.ends_with('>') {
return Err(format!("Malformed generic type: '{s}' (missing closing '>')")); return Err(format!(
"Malformed generic type: '{s}' (missing closing '>')"
));
} }
let container = &s[..inner_start]; let container = &s[..inner_start];
let inner_str = &s[inner_start + 1..s.len() - 1]; let inner_str = &s[inner_start + 1..s.len() - 1];

View File

@@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet};
use crate::error::YamlWorkflowError; use crate::error::YamlWorkflowError;
use crate::schema::{WorkflowSpec, YamlCombinator, YamlComparison, YamlCondition, YamlStep}; use crate::schema::{WorkflowSpec, YamlCombinator, YamlComparison, YamlCondition, YamlStep};
use crate::types::{parse_type_string, SchemaType}; use crate::types::{SchemaType, parse_type_string};
/// Validate a parsed workflow spec. /// Validate a parsed workflow spec.
pub fn validate(spec: &WorkflowSpec) -> Result<(), YamlWorkflowError> { pub fn validate(spec: &WorkflowSpec) -> Result<(), YamlWorkflowError> {
@@ -494,11 +494,7 @@ fn validate_field_path(
return Err(YamlWorkflowError::Validation(format!( return Err(YamlWorkflowError::Validation(format!(
"Condition references unknown input field '{field_name}'. \ "Condition references unknown input field '{field_name}'. \
Available inputs: [{}]", Available inputs: [{}]",
spec.inputs spec.inputs.keys().cloned().collect::<Vec<_>>().join(", ")
.keys()
.cloned()
.collect::<Vec<_>>()
.join(", ")
))); )));
} }
} }
@@ -509,11 +505,7 @@ fn validate_field_path(
return Err(YamlWorkflowError::Validation(format!( return Err(YamlWorkflowError::Validation(format!(
"Condition references unknown output field '{field_name}'. \ "Condition references unknown output field '{field_name}'. \
Available outputs: [{}]", Available outputs: [{}]",
spec.outputs spec.outputs.keys().cloned().collect::<Vec<_>>().join(", ")
.keys()
.cloned()
.collect::<Vec<_>>()
.join(", ")
))); )));
} }
} }

View File

@@ -221,7 +221,10 @@ workflow:
let test_config: wfe_yaml::executors::shell::ShellConfig = let test_config: wfe_yaml::executors::shell::ShellConfig =
serde_json::from_value(test_step.step_config.clone().unwrap()).unwrap(); serde_json::from_value(test_step.step_config.clone().unwrap()).unwrap();
assert_eq!(test_config.run, "cargo build"); assert_eq!(test_config.run, "cargo build");
assert_eq!(test_config.shell, "bash", "shell should be inherited from YAML anchor alias"); assert_eq!(
test_config.shell, "bash",
"shell should be inherited from YAML anchor alias"
);
} }
#[test] #[test]
@@ -473,8 +476,14 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
assert!(err.contains("explode"), "Error should mention the invalid type, got: {err}"); Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!(
err.contains("explode"),
"Error should mention the invalid type, got: {err}"
);
} }
#[test] #[test]
@@ -517,7 +526,10 @@ workflow:
.iter() .iter()
.find(|s| s.name.as_deref() == Some(*child_name)) .find(|s| s.name.as_deref() == Some(*child_name))
.unwrap(); .unwrap();
assert!(child.step_config.is_some(), "Child {child_name} should have step_config"); assert!(
child.step_config.is_some(),
"Child {child_name} should have step_config"
);
} }
// Factories should include entries for all 3 children. // Factories should include entries for all 3 children.
@@ -806,7 +818,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("config"), err.contains("config"),
"Error should mention missing config, got: {err}" "Error should mention missing config, got: {err}"
@@ -1019,11 +1034,11 @@ workflow:
/// SubWorkflowStep (from wfe-core), not a placeholder. /// SubWorkflowStep (from wfe-core), not a placeholder.
#[tokio::test] #[tokio::test]
async fn workflow_step_factory_produces_real_sub_workflow_step() { async fn workflow_step_factory_produces_real_sub_workflow_step() {
use std::future::Future;
use std::pin::Pin;
use std::sync::Mutex;
use wfe_core::models::{ExecutionPointer, WorkflowInstance, WorkflowStep as WfStep}; use wfe_core::models::{ExecutionPointer, WorkflowInstance, WorkflowStep as WfStep};
use wfe_core::traits::step::{HostContext, StepExecutionContext}; use wfe_core::traits::step::{HostContext, StepExecutionContext};
use std::pin::Pin;
use std::future::Future;
use std::sync::Mutex;
let yaml = r#" let yaml = r#"
workflows: workflows:
@@ -1047,30 +1062,45 @@ workflows:
let workflows = load_workflow_from_str(yaml, &config).unwrap(); let workflows = load_workflow_from_str(yaml, &config).unwrap();
// Find the parent workflow's factory for the "run-child" step // Find the parent workflow's factory for the "run-child" step
let parent = workflows.iter().find(|w| w.definition.id == "parent").unwrap(); let parent = workflows
let factory_key = parent.step_factories.iter() .iter()
.find(|w| w.definition.id == "parent")
.unwrap();
let factory_key = parent
.step_factories
.iter()
.find(|(k, _)| k.contains("run-child")) .find(|(k, _)| k.contains("run-child"))
.map(|(k, _)| k.clone()) .map(|(k, _)| k.clone())
.expect("run-child factory should exist"); .expect("run-child factory should exist");
// Create a step from the factory // Create a step from the factory
let factory = &parent.step_factories.iter() let factory = &parent
.step_factories
.iter()
.find(|(k, _)| *k == factory_key) .find(|(k, _)| *k == factory_key)
.unwrap().1; .unwrap()
.1;
let mut step = factory(); let mut step = factory();
// Mock host context that records the start_workflow call // Mock host context that records the start_workflow call
struct MockHost { called: Mutex<bool> } struct MockHost {
called: Mutex<bool>,
}
impl HostContext for MockHost { impl HostContext for MockHost {
fn start_workflow(&self, _def: &str, _ver: u32, _data: serde_json::Value) fn start_workflow(
-> Pin<Box<dyn Future<Output = wfe_core::Result<String>> + Send + '_>> &self,
{ _def: &str,
_ver: u32,
_data: serde_json::Value,
) -> Pin<Box<dyn Future<Output = wfe_core::Result<String>> + Send + '_>> {
*self.called.lock().unwrap() = true; *self.called.lock().unwrap() = true;
Box::pin(async { Ok("child-instance-id".to_string()) }) Box::pin(async { Ok("child-instance-id".to_string()) })
} }
} }
let host = MockHost { called: Mutex::new(false) }; let host = MockHost {
called: Mutex::new(false),
};
let pointer = ExecutionPointer::new(0); let pointer = ExecutionPointer::new(0);
let wf_step = WfStep::new(0, &factory_key); let wf_step = WfStep::new(0, &factory_key);
let workflow = WorkflowInstance::new("parent", 1, serde_json::json!({})); let workflow = WorkflowInstance::new("parent", 1, serde_json::json!({}));
@@ -1182,15 +1212,13 @@ workflow:
} }
// Second child: not // Second child: not
match &children[1] { match &children[1] {
wfe_core::models::StepCondition::Not(inner) => { wfe_core::models::StepCondition::Not(inner) => match inner.as_ref() {
match inner.as_ref() { wfe_core::models::StepCondition::Comparison(cmp) => {
wfe_core::models::StepCondition::Comparison(cmp) => { assert_eq!(cmp.field, ".inputs.skip");
assert_eq!(cmp.field, ".inputs.skip"); assert_eq!(cmp.operator, wfe_core::models::ComparisonOp::Equals);
assert_eq!(cmp.operator, wfe_core::models::ComparisonOp::Equals);
}
other => panic!("Expected Comparison inside Not, got: {other:?}"),
} }
} other => panic!("Expected Comparison inside Not, got: {other:?}"),
},
other => panic!("Expected Not, got: {other:?}"), other => panic!("Expected Not, got: {other:?}"),
} }
} }

View File

@@ -42,7 +42,7 @@ fn make_context<'a>(
workflow, workflow,
cancellation_token: tokio_util::sync::CancellationToken::new(), cancellation_token: tokio_util::sync::CancellationToken::new(),
host_context: None, host_context: None,
log_sink: None, log_sink: None,
} }
} }
@@ -224,10 +224,7 @@ workflow:
let result = wfe_yaml::load_single_workflow_from_str(yaml, &config); let result = wfe_yaml::load_single_workflow_from_str(yaml, &config);
assert!(result.is_err()); assert!(result.is_err());
let msg = result.err().unwrap().to_string(); let msg = result.err().unwrap().to_string();
assert!( assert!(msg.contains("config") || msg.contains("Deno"), "got: {msg}");
msg.contains("config") || msg.contains("Deno"),
"got: {msg}"
);
} }
#[test] #[test]
@@ -247,10 +244,7 @@ workflow:
let result = wfe_yaml::load_single_workflow_from_str(yaml, &config); let result = wfe_yaml::load_single_workflow_from_str(yaml, &config);
assert!(result.is_err()); assert!(result.is_err());
let msg = result.err().unwrap().to_string(); let msg = result.err().unwrap().to_string();
assert!( assert!(msg.contains("script") || msg.contains("file"), "got: {msg}");
msg.contains("script") || msg.contains("file"),
"got: {msg}"
);
} }
#[test] #[test]
@@ -269,7 +263,10 @@ workflow:
let compiled = wfe_yaml::load_single_workflow_from_str(yaml, &config).unwrap(); let compiled = wfe_yaml::load_single_workflow_from_str(yaml, &config).unwrap();
assert!(!compiled.step_factories.is_empty()); assert!(!compiled.step_factories.is_empty());
let (key, _factory) = &compiled.step_factories[0]; let (key, _factory) = &compiled.step_factories[0];
assert!(key.contains("deno"), "factory key should contain 'deno', got: {key}"); assert!(
key.contains("deno"),
"factory key should contain 'deno', got: {key}"
);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -345,10 +342,7 @@ async fn deno_fetch_denied_host() {
"expected permission error, got: {data:?}" "expected permission error, got: {data:?}"
); );
let err_msg = data["error"].as_str().unwrap(); let err_msg = data["error"].as_str().unwrap();
assert!( assert!(err_msg.contains("Permission denied"), "got: {err_msg}");
err_msg.contains("Permission denied"),
"got: {err_msg}"
);
} }
#[tokio::test] #[tokio::test]
@@ -384,8 +378,13 @@ async fn deno_fetch_returns_json() {
async fn deno_fetch_post_with_body() { async fn deno_fetch_post_with_body() {
let server = wiremock::MockServer::start().await; let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("POST")) wiremock::Mock::given(wiremock::matchers::method("POST"))
.and(wiremock::matchers::header("content-type", "application/json")) .and(wiremock::matchers::header(
.and(wiremock::matchers::body_json(serde_json::json!({"key": "val"}))) "content-type",
"application/json",
))
.and(wiremock::matchers::body_json(
serde_json::json!({"key": "val"}),
))
.respond_with(wiremock::ResponseTemplate::new(201).set_body_string("created")) .respond_with(wiremock::ResponseTemplate::new(201).set_body_string("created"))
.mount(&server) .mount(&server)
.await; .await;
@@ -501,7 +500,11 @@ async fn deno_import_local_file() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let helper_path = dir.path().join("helper.js"); let helper_path = dir.path().join("helper.js");
let mut f = std::fs::File::create(&helper_path).unwrap(); let mut f = std::fs::File::create(&helper_path).unwrap();
writeln!(f, "export function greet(name) {{ return `hello ${{name}}`; }}").unwrap(); writeln!(
f,
"export function greet(name) {{ return `hello ${{name}}`; }}"
)
.unwrap();
drop(f); drop(f);
let main_path = dir.path().join("main.js"); let main_path = dir.path().join("main.js");
@@ -536,10 +539,7 @@ output("greeting", greet("world"));"#,
async fn deno_dynamic_import_denied() { async fn deno_dynamic_import_denied() {
let server = wiremock::MockServer::start().await; let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::any()) wiremock::Mock::given(wiremock::matchers::any())
.respond_with( .respond_with(wiremock::ResponseTemplate::new(200).set_body_string("export const x = 1;"))
wiremock::ResponseTemplate::new(200)
.set_body_string("export const x = 1;"),
)
.mount(&server) .mount(&server)
.await; .await;

View File

@@ -105,7 +105,10 @@ workflow:
"#; "#;
let instance = run_yaml_workflow(yaml).await; let instance = run_yaml_workflow(yaml).await;
assert_eq!(instance.status, WorkflowStatus::Complete); assert_eq!(instance.status, WorkflowStatus::Complete);
assert_eq!(instance.data["message"], serde_json::json!("hello from deno")); assert_eq!(
instance.data["message"],
serde_json::json!("hello from deno")
);
} }
#[tokio::test] #[tokio::test]
@@ -114,8 +117,7 @@ async fn yaml_deno_with_fetch_wiremock() {
wiremock::Mock::given(wiremock::matchers::method("GET")) wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/api/data")) .and(wiremock::matchers::path("/api/data"))
.respond_with( .respond_with(
wiremock::ResponseTemplate::new(200) wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({"value": 42})),
.set_body_json(serde_json::json!({"value": 42})),
) )
.mount(&server) .mount(&server)
.await; .await;
@@ -325,8 +327,7 @@ workflow:
const data = inputs(); const data = inputs();
output("doubled", data.value * 2); output("doubled", data.value * 2);
"#; "#;
let instance = let instance = run_yaml_workflow_with_data(yaml, serde_json::json!({"value": 21})).await;
run_yaml_workflow_with_data(yaml, serde_json::json!({"value": 21})).await;
assert_eq!(instance.status, WorkflowStatus::Complete); assert_eq!(instance.status, WorkflowStatus::Complete);
assert_eq!(instance.data["doubled"], serde_json::json!(42)); assert_eq!(instance.data["doubled"], serde_json::json!(42));
} }
@@ -547,10 +548,7 @@ workflow:
match result { match result {
Err(e) => { Err(e) => {
let msg = e.to_string(); let msg = e.to_string();
assert!( assert!(msg.contains("config") || msg.contains("Deno"), "got: {msg}");
msg.contains("config") || msg.contains("Deno"),
"got: {msg}"
);
} }
Ok(_) => panic!("expected error for deno step without config"), Ok(_) => panic!("expected error for deno step without config"),
} }
@@ -574,10 +572,7 @@ workflow:
match result { match result {
Err(e) => { Err(e) => {
let msg = e.to_string(); let msg = e.to_string();
assert!( assert!(msg.contains("script") || msg.contains("file"), "got: {msg}");
msg.contains("script") || msg.contains("file"),
"got: {msg}"
);
} }
Ok(_) => panic!("expected error for deno step without script or file"), Ok(_) => panic!("expected error for deno step without script or file"),
} }
@@ -633,8 +628,14 @@ workflow:
"#; "#;
let config = HashMap::new(); let config = HashMap::new();
let compiled = load_single_workflow_from_str(yaml, &config).unwrap(); let compiled = load_single_workflow_from_str(yaml, &config).unwrap();
let has_shell = compiled.step_factories.iter().any(|(k, _)| k.contains("shell")); let has_shell = compiled
let has_deno = compiled.step_factories.iter().any(|(k, _)| k.contains("deno")); .step_factories
.iter()
.any(|(k, _)| k.contains("shell"));
let has_deno = compiled
.step_factories
.iter()
.any(|(k, _)| k.contains("deno"));
assert!(has_shell, "should have shell factory"); assert!(has_shell, "should have shell factory");
assert!(has_deno, "should have deno factory"); assert!(has_deno, "should have deno factory");
} }
@@ -785,7 +786,10 @@ workflow:
"#; "#;
let instance = run_yaml_workflow(yaml).await; let instance = run_yaml_workflow(yaml).await;
assert_eq!(instance.status, WorkflowStatus::Complete); assert_eq!(instance.status, WorkflowStatus::Complete);
assert_eq!(instance.data["nested"]["a"]["b"]["c"], serde_json::json!(42)); assert_eq!(
instance.data["nested"]["a"]["b"]["c"],
serde_json::json!(42)
);
assert_eq!(instance.data["array"], serde_json::json!([1, 2, 3])); assert_eq!(instance.data["array"], serde_json::json!([1, 2, 3]));
assert!(instance.data["null_val"].is_null()); assert!(instance.data["null_val"].is_null());
assert_eq!(instance.data["bool_val"], serde_json::json!(false)); assert_eq!(instance.data["bool_val"], serde_json::json!(false));

View File

@@ -1,7 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::io::Write; use std::io::Write;
use wfe_yaml::{load_workflow, load_single_workflow_from_str}; use wfe_yaml::{load_single_workflow_from_str, load_workflow};
#[test] #[test]
fn load_workflow_from_file() { fn load_workflow_from_file() {
@@ -35,7 +35,10 @@ fn load_workflow_from_nonexistent_file_returns_error() {
let path = std::path::Path::new("/tmp/nonexistent_wfe_test_file_12345.yaml"); let path = std::path::Path::new("/tmp/nonexistent_wfe_test_file_12345.yaml");
let result = load_workflow(path, &HashMap::new()); let result = load_workflow(path, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("IO error") || err.contains("No such file"), err.contains("IO error") || err.contains("No such file"),
"Expected IO error, got: {err}" "Expected IO error, got: {err}"
@@ -47,7 +50,10 @@ fn load_workflow_from_str_with_invalid_yaml_returns_error() {
let yaml = "this is not valid yaml: [[["; let yaml = "this is not valid yaml: [[[";
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("YAML parse error"), err.contains("YAML parse error"),
"Expected YAML parse error, got: {err}" "Expected YAML parse error, got: {err}"
@@ -96,7 +102,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("missing"), err.contains("missing"),
"Expected unresolved variable error, got: {err}" "Expected unresolved variable error, got: {err}"

View File

@@ -55,9 +55,19 @@ async fn run_yaml_workflow_with_config(
) -> wfe::models::WorkflowInstance { ) -> wfe::models::WorkflowInstance {
let compiled = load_single_workflow_from_str(yaml, config).unwrap(); let compiled = load_single_workflow_from_str(yaml, config).unwrap();
for step in &compiled.definition.steps { for step in &compiled.definition.steps {
eprintln!(" step: {:?} type={} config={:?}", step.name, step.step_type, step.step_config); eprintln!(
" step: {:?} type={} config={:?}",
step.name, step.step_type, step.step_config
);
} }
eprintln!(" factories: {:?}", compiled.step_factories.iter().map(|(k, _)| k.clone()).collect::<Vec<_>>()); eprintln!(
" factories: {:?}",
compiled
.step_factories
.iter()
.map(|(k, _)| k.clone())
.collect::<Vec<_>>()
);
let persistence = Arc::new(InMemoryPersistenceProvider::new()); let persistence = Arc::new(InMemoryPersistenceProvider::new());
let lock = Arc::new(InMemoryLockProvider::new()); let lock = Arc::new(InMemoryLockProvider::new());
@@ -197,7 +207,9 @@ fn make_config(
#[tokio::test] #[tokio::test]
#[ignore = "requires containerd daemon"] #[ignore = "requires containerd daemon"]
async fn minimal_echo_in_containerd_via_workflow() { async fn minimal_echo_in_containerd_via_workflow() {
let _ = tracing_subscriber::fmt().with_env_filter("wfe_containerd=debug,wfe_core::executor=debug").try_init(); let _ = tracing_subscriber::fmt()
.with_env_filter("wfe_containerd=debug,wfe_core::executor=debug")
.try_init();
let Some(addr) = containerd_addr() else { let Some(addr) = containerd_addr() else {
eprintln!("SKIP: containerd not available"); eprintln!("SKIP: containerd not available");
return; return;
@@ -237,10 +249,7 @@ async fn minimal_echo_in_containerd_via_workflow() {
eprintln!("Status: {:?}, Data: {:?}", instance.status, instance.data); eprintln!("Status: {:?}, Data: {:?}", instance.status, instance.data);
assert_eq!(instance.status, WorkflowStatus::Complete); assert_eq!(instance.status, WorkflowStatus::Complete);
let data = instance.data.as_object().unwrap(); let data = instance.data.as_object().unwrap();
assert_eq!( assert_eq!(data.get("echo.status").and_then(|v| v.as_str()), Some("ok"),);
data.get("echo.status").and_then(|v| v.as_str()),
Some("ok"),
);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -259,79 +268,129 @@ async fn full_rust_pipeline_in_container() {
let rustup_home = shared_tempdir("rustup"); let rustup_home = shared_tempdir("rustup");
let workspace = shared_tempdir("workspace"); let workspace = shared_tempdir("workspace");
let config = make_config( let config = make_config(&addr, &cargo_home, &rustup_home, Some(&workspace));
&addr,
&cargo_home,
&rustup_home,
Some(&workspace),
);
let steps = [ let steps = [
containerd_step_yaml( containerd_step_yaml(
"install-rust", "host", "if-not-present", "10m", None, false, "install-rust",
"host",
"if-not-present",
"10m",
None,
false,
" apt-get update && apt-get install -y curl gcc pkg-config libssl-dev\n\ " apt-get update && apt-get install -y curl gcc pkg-config libssl-dev\n\
\x20 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable", \x20 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable",
), ),
containerd_step_yaml( containerd_step_yaml(
"install-tools", "host", "never", "10m", None, false, "install-tools",
"host",
"never",
"10m",
None,
false,
" rustup component add clippy rustfmt llvm-tools-preview\n\ " rustup component add clippy rustfmt llvm-tools-preview\n\
\x20 cargo install cargo-audit cargo-deny cargo-nextest cargo-llvm-cov", \x20 cargo install cargo-audit cargo-deny cargo-nextest cargo-llvm-cov",
), ),
containerd_step_yaml( containerd_step_yaml(
"create-project", "host", "never", "2m", None, true, "create-project",
"host",
"never",
"2m",
None,
true,
" cargo init /workspace/test-crate --name test-crate\n\ " cargo init /workspace/test-crate --name test-crate\n\
\x20 cd /workspace/test-crate\n\ \x20 cd /workspace/test-crate\n\
\x20 echo '#[cfg(test)] mod tests { #[test] fn it_works() { assert_eq!(2+2,4); } }' >> src/main.rs", \x20 echo '#[cfg(test)] mod tests { #[test] fn it_works() { assert_eq!(2+2,4); } }' >> src/main.rs",
), ),
containerd_step_yaml( containerd_step_yaml(
"cargo-fmt", "none", "never", "2m", "cargo-fmt",
Some("/workspace/test-crate"), true, "none",
"never",
"2m",
Some("/workspace/test-crate"),
true,
" cargo fmt -- --check || cargo fmt", " cargo fmt -- --check || cargo fmt",
), ),
containerd_step_yaml( containerd_step_yaml(
"cargo-check", "none", "never", "5m", "cargo-check",
Some("/workspace/test-crate"), true, "none",
"never",
"5m",
Some("/workspace/test-crate"),
true,
" cargo check", " cargo check",
), ),
containerd_step_yaml( containerd_step_yaml(
"cargo-clippy", "none", "never", "5m", "cargo-clippy",
Some("/workspace/test-crate"), true, "none",
"never",
"5m",
Some("/workspace/test-crate"),
true,
" cargo clippy -- -D warnings", " cargo clippy -- -D warnings",
), ),
containerd_step_yaml( containerd_step_yaml(
"cargo-test", "none", "never", "5m", "cargo-test",
Some("/workspace/test-crate"), true, "none",
"never",
"5m",
Some("/workspace/test-crate"),
true,
" cargo test", " cargo test",
), ),
containerd_step_yaml( containerd_step_yaml(
"cargo-build", "none", "never", "5m", "cargo-build",
Some("/workspace/test-crate"), true, "none",
"never",
"5m",
Some("/workspace/test-crate"),
true,
" cargo build --release", " cargo build --release",
), ),
containerd_step_yaml( containerd_step_yaml(
"cargo-nextest", "none", "never", "5m", "cargo-nextest",
Some("/workspace/test-crate"), true, "none",
"never",
"5m",
Some("/workspace/test-crate"),
true,
" cargo nextest run", " cargo nextest run",
), ),
containerd_step_yaml( containerd_step_yaml(
"cargo-llvm-cov", "none", "never", "5m", "cargo-llvm-cov",
Some("/workspace/test-crate"), true, "none",
"never",
"5m",
Some("/workspace/test-crate"),
true,
" cargo llvm-cov --summary-only", " cargo llvm-cov --summary-only",
), ),
containerd_step_yaml( containerd_step_yaml(
"cargo-audit", "host", "never", "5m", "cargo-audit",
Some("/workspace/test-crate"), true, "host",
"never",
"5m",
Some("/workspace/test-crate"),
true,
" cargo audit || true", " cargo audit || true",
), ),
containerd_step_yaml( containerd_step_yaml(
"cargo-deny", "none", "never", "5m", "cargo-deny",
Some("/workspace/test-crate"), true, "none",
"never",
"5m",
Some("/workspace/test-crate"),
true,
" cargo deny init\n\ " cargo deny init\n\
\x20 cargo deny check || true", \x20 cargo deny check || true",
), ),
containerd_step_yaml( containerd_step_yaml(
"cargo-doc", "none", "never", "5m", "cargo-doc",
Some("/workspace/test-crate"), true, "none",
"never",
"5m",
Some("/workspace/test-crate"),
true,
" cargo doc --no-deps", " cargo doc --no-deps",
), ),
]; ];

View File

@@ -17,10 +17,7 @@ workflow:
assert_eq!(parsed.workflow.version, 1); assert_eq!(parsed.workflow.version, 1);
assert_eq!(parsed.workflow.steps.len(), 1); assert_eq!(parsed.workflow.steps.len(), 1);
assert_eq!(parsed.workflow.steps[0].name, "hello"); assert_eq!(parsed.workflow.steps[0].name, "hello");
assert_eq!( assert_eq!(parsed.workflow.steps[0].step_type.as_deref(), Some("shell"));
parsed.workflow.steps[0].step_type.as_deref(),
Some("shell")
);
} }
#[test] #[test]
@@ -263,20 +260,14 @@ workflow:
let parsed: YamlWorkflow = serde_yaml::from_str(yaml).unwrap(); let parsed: YamlWorkflow = serde_yaml::from_str(yaml).unwrap();
assert_eq!(parsed.workflow.inputs.len(), 3); assert_eq!(parsed.workflow.inputs.len(), 3);
assert_eq!(parsed.workflow.inputs.get("repo_url").unwrap(), "string"); assert_eq!(parsed.workflow.inputs.get("repo_url").unwrap(), "string");
assert_eq!( assert_eq!(parsed.workflow.inputs.get("tags").unwrap(), "list<string>");
parsed.workflow.inputs.get("tags").unwrap(),
"list<string>"
);
assert_eq!(parsed.workflow.inputs.get("verbose").unwrap(), "bool?"); assert_eq!(parsed.workflow.inputs.get("verbose").unwrap(), "bool?");
assert_eq!(parsed.workflow.outputs.len(), 2); assert_eq!(parsed.workflow.outputs.len(), 2);
assert_eq!( assert_eq!(
parsed.workflow.outputs.get("artifact_path").unwrap(), parsed.workflow.outputs.get("artifact_path").unwrap(),
"string" "string"
); );
assert_eq!( assert_eq!(parsed.workflow.outputs.get("exit_code").unwrap(), "integer");
parsed.workflow.outputs.get("exit_code").unwrap(),
"integer"
);
} }
#[test] #[test]

View File

@@ -60,7 +60,9 @@ struct CollectingLogSink {
impl CollectingLogSink { impl CollectingLogSink {
fn new() -> Self { fn new() -> Self {
Self { chunks: tokio::sync::Mutex::new(Vec::new()) } Self {
chunks: tokio::sync::Mutex::new(Vec::new()),
}
} }
async fn chunks(&self) -> Vec<wfe_core::traits::LogChunk> { async fn chunks(&self) -> Vec<wfe_core::traits::LogChunk> {
@@ -242,11 +244,8 @@ workflow:
run: echo "{wfe_prefix}[output result=$GREETING]" run: echo "{wfe_prefix}[output result=$GREETING]"
"# "#
); );
let instance = run_yaml_workflow_with_data( let instance =
&yaml, run_yaml_workflow_with_data(&yaml, serde_json::json!({"greeting": "hi there"})).await;
serde_json::json!({"greeting": "hi there"}),
)
.await;
assert_eq!(instance.status, WorkflowStatus::Complete); assert_eq!(instance.status, WorkflowStatus::Complete);
if let Some(data) = instance.data.as_object() { if let Some(data) = instance.data.as_object() {
@@ -320,19 +319,33 @@ workflow:
assert_eq!(instance.status, WorkflowStatus::Complete); assert_eq!(instance.status, WorkflowStatus::Complete);
let chunks = log_sink.chunks().await; let chunks = log_sink.chunks().await;
assert!(chunks.len() >= 2, "expected at least 2 stdout chunks, got {}", chunks.len()); assert!(
chunks.len() >= 2,
"expected at least 2 stdout chunks, got {}",
chunks.len()
);
let stdout_chunks: Vec<_> = chunks let stdout_chunks: Vec<_> = chunks
.iter() .iter()
.filter(|c| c.stream == wfe_core::traits::LogStreamType::Stdout) .filter(|c| c.stream == wfe_core::traits::LogStreamType::Stdout)
.collect(); .collect();
assert!(stdout_chunks.len() >= 2, "expected at least 2 stdout chunks"); assert!(
stdout_chunks.len() >= 2,
"expected at least 2 stdout chunks"
);
let all_data: String = stdout_chunks.iter() let all_data: String = stdout_chunks
.iter()
.map(|c| String::from_utf8_lossy(&c.data).to_string()) .map(|c| String::from_utf8_lossy(&c.data).to_string())
.collect(); .collect();
assert!(all_data.contains("line one"), "stdout should contain 'line one', got: {all_data}"); assert!(
assert!(all_data.contains("line two"), "stdout should contain 'line two', got: {all_data}"); all_data.contains("line one"),
"stdout should contain 'line one', got: {all_data}"
);
assert!(
all_data.contains("line two"),
"stdout should contain 'line two', got: {all_data}"
);
// Verify chunk metadata. // Verify chunk metadata.
for chunk in &stdout_chunks { for chunk in &stdout_chunks {
@@ -364,10 +377,14 @@ workflow:
.collect(); .collect();
assert!(!stderr_chunks.is_empty(), "expected stderr chunks"); assert!(!stderr_chunks.is_empty(), "expected stderr chunks");
let stderr_data: String = stderr_chunks.iter() let stderr_data: String = stderr_chunks
.iter()
.map(|c| String::from_utf8_lossy(&c.data).to_string()) .map(|c| String::from_utf8_lossy(&c.data).to_string())
.collect(); .collect();
assert!(stderr_data.contains("stderr output"), "stderr should contain 'stderr output', got: {stderr_data}"); assert!(
stderr_data.contains("stderr output"),
"stderr should contain 'stderr output', got: {stderr_data}"
);
} }
#[tokio::test] #[tokio::test]
@@ -392,8 +409,14 @@ workflow:
let chunks = log_sink.chunks().await; let chunks = log_sink.chunks().await;
let step_names: Vec<_> = chunks.iter().map(|c| c.step_name.as_str()).collect(); let step_names: Vec<_> = chunks.iter().map(|c| c.step_name.as_str()).collect();
assert!(step_names.contains(&"step-a"), "should have chunks from step-a"); assert!(
assert!(step_names.contains(&"step-b"), "should have chunks from step-b"); step_names.contains(&"step-a"),
"should have chunks from step-a"
);
assert!(
step_names.contains(&"step-b"),
"should have chunks from step-b"
);
} }
#[tokio::test] #[tokio::test]
@@ -412,7 +435,13 @@ workflow:
let instance = run_yaml_workflow(yaml).await; let instance = run_yaml_workflow(yaml).await;
assert_eq!(instance.status, WorkflowStatus::Complete); assert_eq!(instance.status, WorkflowStatus::Complete);
let data = instance.data.as_object().unwrap(); let data = instance.data.as_object().unwrap();
assert!(data.get("echo-step.stdout").unwrap().as_str().unwrap().contains("no sink")); assert!(
data.get("echo-step.stdout")
.unwrap()
.as_str()
.unwrap()
.contains("no sink")
);
} }
// ── Security regression tests ──────────────────────────────────────── // ── Security regression tests ────────────────────────────────────────
@@ -431,11 +460,8 @@ workflow:
run: echo "$PATH" run: echo "$PATH"
"#; "#;
// Set a workflow data key "path" that would override PATH if not blocked. // Set a workflow data key "path" that would override PATH if not blocked.
let instance = run_yaml_workflow_with_data( let instance =
yaml, run_yaml_workflow_with_data(yaml, serde_json::json!({"path": "/attacker/bin"})).await;
serde_json::json!({"path": "/attacker/bin"}),
)
.await;
assert_eq!(instance.status, WorkflowStatus::Complete); assert_eq!(instance.status, WorkflowStatus::Complete);
let data = instance.data.as_object().unwrap(); let data = instance.data.as_object().unwrap();
@@ -463,11 +489,8 @@ workflow:
run: echo "{wfe_prefix}[output val=$MY_CUSTOM_VAR]" run: echo "{wfe_prefix}[output val=$MY_CUSTOM_VAR]"
"# "#
); );
let instance = run_yaml_workflow_with_data( let instance =
&yaml, run_yaml_workflow_with_data(&yaml, serde_json::json!({"my_custom_var": "works"})).await;
serde_json::json!({"my_custom_var": "works"}),
)
.await;
assert_eq!(instance.status, WorkflowStatus::Complete); assert_eq!(instance.status, WorkflowStatus::Complete);
let data = instance.data.as_object().unwrap(); let data = instance.data.as_object().unwrap();

View File

@@ -1,4 +1,4 @@
use wfe_yaml::types::{parse_type_string, SchemaType}; use wfe_yaml::types::{SchemaType, parse_type_string};
#[test] #[test]
fn parse_all_primitives() { fn parse_all_primitives() {

View File

@@ -12,7 +12,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("at least one step"), err.contains("at least one step"),
"Expected 'at least one step' error, got: {err}" "Expected 'at least one step' error, got: {err}"
@@ -30,7 +33,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("type") && err.contains("parallel"), err.contains("type") && err.contains("parallel"),
"Expected error about missing type or parallel, got: {err}" "Expected error about missing type or parallel, got: {err}"
@@ -54,7 +60,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("cannot have both"), err.contains("cannot have both"),
"Expected 'cannot have both' error, got: {err}" "Expected 'cannot have both' error, got: {err}"
@@ -79,7 +88,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("Duplicate step name") && err.contains("deploy"), err.contains("Duplicate step name") && err.contains("deploy"),
"Expected duplicate name error, got: {err}" "Expected duplicate name error, got: {err}"
@@ -100,7 +112,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("config.run") || err.contains("config.file"), err.contains("config.run") || err.contains("config.file"),
"Expected error about missing run/file, got: {err}" "Expected error about missing run/file, got: {err}"
@@ -119,7 +134,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("config"), err.contains("config"),
"Expected error about missing config, got: {err}" "Expected error about missing config, got: {err}"
@@ -142,7 +160,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("panic"), err.contains("panic"),
"Expected error mentioning invalid type, got: {err}" "Expected error mentioning invalid type, got: {err}"
@@ -165,7 +186,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("crash"), err.contains("crash"),
"Expected error mentioning invalid type, got: {err}" "Expected error mentioning invalid type, got: {err}"
@@ -185,7 +209,11 @@ workflow:
run: echo hello run: echo hello
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_ok(), "Valid workflow should pass, got: {:?}", result.err()); assert!(
result.is_ok(),
"Valid workflow should pass, got: {:?}",
result.err()
);
} }
#[test] #[test]
@@ -203,7 +231,11 @@ workflow:
run: echo a run: echo a
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_ok(), "Valid parallel workflow should pass, got: {:?}", result.err()); assert!(
result.is_ok(),
"Valid parallel workflow should pass, got: {:?}",
result.err()
);
} }
#[test] #[test]
@@ -225,7 +257,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("Duplicate step name"), err.contains("Duplicate step name"),
"Expected duplicate name error for hook, got: {err}" "Expected duplicate name error for hook, got: {err}"
@@ -250,7 +285,11 @@ workflow:
run: echo ok run: echo ok
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_ok(), "Valid on_success hook should pass, got: {:?}", result.err()); assert!(
result.is_ok(),
"Valid on_success hook should pass, got: {:?}",
result.err()
);
} }
#[test] #[test]
@@ -271,7 +310,11 @@ workflow:
run: cleanup.sh run: cleanup.sh
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_ok(), "Valid ensure hook should pass, got: {:?}", result.err()); assert!(
result.is_ok(),
"Valid ensure hook should pass, got: {:?}",
result.err()
);
} }
#[test] #[test]
@@ -320,7 +363,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("Duplicate step name") && err.contains("task-a"), err.contains("Duplicate step name") && err.contains("task-a"),
"Expected duplicate name in parallel children, got: {err}" "Expected duplicate name in parallel children, got: {err}"
@@ -341,7 +387,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("config"), err.contains("config"),
"Expected error about missing config, got: {err}" "Expected error about missing config, got: {err}"
@@ -362,7 +411,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("config.workflow"), err.contains("config.workflow"),
"Expected error about missing config.workflow, got: {err}" "Expected error about missing config.workflow, got: {err}"
@@ -382,7 +434,11 @@ workflow:
workflow: child-wf workflow: child-wf
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_ok(), "Valid workflow step should pass, got: {:?}", result.err()); assert!(
result.is_ok(),
"Valid workflow step should pass, got: {:?}",
result.err()
);
} }
// --- Multi-workflow validation tests --- // --- Multi-workflow validation tests ---
@@ -407,7 +463,11 @@ workflows:
run: cargo test run: cargo test
"#; "#;
let result = load_workflow_from_str(yaml, &HashMap::new()); let result = load_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_ok(), "Valid multi-workflow should pass, got: {:?}", result.err()); assert!(
result.is_ok(),
"Valid multi-workflow should pass, got: {:?}",
result.err()
);
assert_eq!(result.unwrap().len(), 2); assert_eq!(result.unwrap().len(), 2);
} }
@@ -432,7 +492,10 @@ workflows:
"#; "#;
let result = load_workflow_from_str(yaml, &HashMap::new()); let result = load_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("Duplicate workflow ID"), err.contains("Duplicate workflow ID"),
"Expected duplicate workflow ID error, got: {err}" "Expected duplicate workflow ID error, got: {err}"
@@ -461,7 +524,10 @@ workflows:
"#; "#;
let result = load_workflow_from_str(yaml, &HashMap::new()); let result = load_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("Cannot specify both"), err.contains("Cannot specify both"),
"Expected error about both workflow and workflows, got: {err}" "Expected error about both workflow and workflows, got: {err}"
@@ -476,7 +542,10 @@ something_else:
"#; "#;
let result = load_workflow_from_str(yaml, &HashMap::new()); let result = load_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("Must specify either"), err.contains("Must specify either"),
"Expected error about missing workflow/workflows, got: {err}" "Expected error about missing workflow/workflows, got: {err}"
@@ -490,7 +559,10 @@ workflows: []
"#; "#;
let result = load_workflow_from_str(yaml, &HashMap::new()); let result = load_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("empty"), err.contains("empty"),
"Expected error about empty workflows, got: {err}" "Expected error about empty workflows, got: {err}"
@@ -520,7 +592,10 @@ workflows:
"#; "#;
let result = load_workflow_from_str(yaml, &HashMap::new()); let result = load_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("Circular workflow reference"), err.contains("Circular workflow reference"),
"Expected circular reference error, got: {err}" "Expected circular reference error, got: {err}"
@@ -541,7 +616,10 @@ workflows:
"#; "#;
let result = load_workflow_from_str(yaml, &HashMap::new()); let result = load_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("Circular workflow reference"), err.contains("Circular workflow reference"),
"Expected circular reference error, got: {err}" "Expected circular reference error, got: {err}"
@@ -568,7 +646,11 @@ workflows:
run: echo working run: echo working
"#; "#;
let result = load_workflow_from_str(yaml, &HashMap::new()); let result = load_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_ok(), "Valid workflow reference should pass, got: {:?}", result.err()); assert!(
result.is_ok(),
"Valid workflow reference should pass, got: {:?}",
result.err()
);
} }
#[test] #[test]
@@ -585,7 +667,11 @@ workflow:
workflow: some-external-wf workflow: some-external-wf
"#; "#;
let result = load_workflow_from_str(yaml, &HashMap::new()); let result = load_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_ok(), "External workflow ref should not error, got: {:?}", result.err()); assert!(
result.is_ok(),
"External workflow ref should not error, got: {:?}",
result.err()
);
} }
#[test] #[test]
@@ -609,7 +695,10 @@ workflows:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("Expected single workflow"), err.contains("Expected single workflow"),
"Expected single workflow error, got: {err}" "Expected single workflow error, got: {err}"
@@ -631,7 +720,11 @@ workflows:
run: echo hello run: echo hello
"#; "#;
let result = load_workflow_from_str(yaml, &HashMap::new()); let result = load_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_ok(), "Single workflow in multi-mode should pass, got: {:?}", result.err()); assert!(
result.is_ok(),
"Single workflow in multi-mode should pass, got: {:?}",
result.err()
);
assert_eq!(result.unwrap().len(), 1); assert_eq!(result.unwrap().len(), 1);
} }
@@ -662,7 +755,11 @@ workflows:
run: echo gamma run: echo gamma
"#; "#;
let result = load_workflow_from_str(yaml, &HashMap::new()); let result = load_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_ok(), "Multiple independent workflows should pass, got: {:?}", result.err()); assert!(
result.is_ok(),
"Multiple independent workflows should pass, got: {:?}",
result.err()
);
assert_eq!(result.unwrap().len(), 3); assert_eq!(result.unwrap().len(), 3);
} }
@@ -686,7 +783,11 @@ workflows:
run: echo working run: echo working
"#; "#;
let result = load_workflow_from_str(yaml, &HashMap::new()); let result = load_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_ok(), "Cross-referenced workflows should pass, got: {:?}", result.err()); assert!(
result.is_ok(),
"Cross-referenced workflows should pass, got: {:?}",
result.err()
);
} }
// --- Cycle detection edge cases --- // --- Cycle detection edge cases ---
@@ -719,7 +820,10 @@ workflows:
"#; "#;
let result = load_workflow_from_str(yaml, &HashMap::new()); let result = load_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("Circular workflow reference"), err.contains("Circular workflow reference"),
"Expected circular reference error for 3-node cycle, got: {err}" "Expected circular reference error for 3-node cycle, got: {err}"
@@ -753,7 +857,11 @@ workflows:
run: echo done run: echo done
"#; "#;
let result = load_workflow_from_str(yaml, &HashMap::new()); let result = load_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_ok(), "Linear chain should not be a cycle, got: {:?}", result.err()); assert!(
result.is_ok(),
"Linear chain should not be a cycle, got: {:?}",
result.err()
);
} }
#[test] #[test]
@@ -794,7 +902,11 @@ workflows:
run: echo done run: echo done
"#; "#;
let result = load_workflow_from_str(yaml, &HashMap::new()); let result = load_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_ok(), "Diamond dependency should not be a cycle, got: {:?}", result.err()); assert!(
result.is_ok(),
"Diamond dependency should not be a cycle, got: {:?}",
result.err()
);
} }
// --- Deno step validation --- // --- Deno step validation ---
@@ -811,7 +923,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("Deno") && err.contains("config"), err.contains("Deno") && err.contains("config"),
"Expected Deno config error, got: {err}" "Expected Deno config error, got: {err}"
@@ -833,7 +948,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("Deno") && (err.contains("script") || err.contains("file")), err.contains("Deno") && (err.contains("script") || err.contains("file")),
"Expected Deno script/file error, got: {err}" "Expected Deno script/file error, got: {err}"
@@ -854,7 +972,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("BuildKit") && err.contains("config"), err.contains("BuildKit") && err.contains("config"),
"Expected BuildKit config error, got: {err}" "Expected BuildKit config error, got: {err}"
@@ -875,7 +996,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("dockerfile"), err.contains("dockerfile"),
"Expected dockerfile error, got: {err}" "Expected dockerfile error, got: {err}"
@@ -896,7 +1020,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("context"), err.contains("context"),
"Expected context error, got: {err}" "Expected context error, got: {err}"
@@ -919,7 +1046,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("push") && err.contains("tags"), err.contains("push") && err.contains("tags"),
"Expected push/tags error, got: {err}" "Expected push/tags error, got: {err}"
@@ -970,7 +1100,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("Containerd") && err.contains("config"), err.contains("Containerd") && err.contains("config"),
"Expected Containerd config error, got: {err}" "Expected Containerd config error, got: {err}"
@@ -991,11 +1124,11 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
assert!( Err(e) => e.to_string(),
err.contains("image"), Ok(_) => panic!("expected error"),
"Expected image error, got: {err}" };
); assert!(err.contains("image"), "Expected image error, got: {err}");
} }
#[test] #[test]
@@ -1012,7 +1145,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("run") || err.contains("command"), err.contains("run") || err.contains("command"),
"Expected run/command error, got: {err}" "Expected run/command error, got: {err}"
@@ -1037,7 +1173,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("cannot have both"), err.contains("cannot have both"),
"Expected 'cannot have both' error, got: {err}" "Expected 'cannot have both' error, got: {err}"
@@ -1060,7 +1199,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("network") && err.contains("overlay"), err.contains("network") && err.contains("overlay"),
"Expected invalid network error, got: {err}" "Expected invalid network error, got: {err}"
@@ -1111,7 +1253,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("pull") && err.contains("aggressive"), err.contains("pull") && err.contains("aggressive"),
"Expected invalid pull policy error, got: {err}" "Expected invalid pull policy error, got: {err}"
@@ -1190,7 +1335,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("config"), err.contains("config"),
"Expected config error for invalid hook, got: {err}" "Expected config error for invalid hook, got: {err}"
@@ -1214,7 +1362,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("config"), err.contains("config"),
"Expected config error for invalid on_success hook, got: {err}" "Expected config error for invalid on_success hook, got: {err}"
@@ -1238,7 +1389,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("config"), err.contains("config"),
"Expected config error for invalid ensure hook, got: {err}" "Expected config error for invalid ensure hook, got: {err}"
@@ -1261,7 +1415,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("config"), err.contains("config"),
"Expected config error for deeply nested invalid step, got: {err}" "Expected config error for deeply nested invalid step, got: {err}"
@@ -1301,7 +1458,10 @@ workflows:
"#; "#;
let result = load_workflow_from_str(yaml, &HashMap::new()); let result = load_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("Circular workflow reference"), err.contains("Circular workflow reference"),
"Expected circular reference from hooks, got: {err}" "Expected circular reference from hooks, got: {err}"
@@ -1339,7 +1499,10 @@ workflows:
"#; "#;
let result = load_workflow_from_str(yaml, &HashMap::new()); let result = load_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("Circular workflow reference"), err.contains("Circular workflow reference"),
"Expected circular reference from ensure hooks, got: {err}" "Expected circular reference from ensure hooks, got: {err}"
@@ -1371,7 +1534,10 @@ workflows:
"#; "#;
let result = load_workflow_from_str(yaml, &HashMap::new()); let result = load_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("Circular workflow reference"), err.contains("Circular workflow reference"),
"Expected circular reference from parallel blocks, got: {err}" "Expected circular reference from parallel blocks, got: {err}"
@@ -1394,7 +1560,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("Unknown step type") && err.contains("terraform"), err.contains("Unknown step type") && err.contains("terraform"),
"Expected unknown step type error, got: {err}" "Expected unknown step type error, got: {err}"
@@ -1408,7 +1577,10 @@ fn load_workflow_from_nonexistent_file_returns_io_error() {
let path = std::path::Path::new("/tmp/nonexistent_wfe_test_file.yaml"); let path = std::path::Path::new("/tmp/nonexistent_wfe_test_file.yaml");
let result = wfe_yaml::load_workflow(path, &HashMap::new()); let result = wfe_yaml::load_workflow(path, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("IO error") || err.contains("No such file"), err.contains("IO error") || err.contains("No such file"),
"Expected IO error, got: {err}" "Expected IO error, got: {err}"
@@ -1435,7 +1607,11 @@ workflow:
equals: true equals: true
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_ok(), "Field path to known input should pass, got: {:?}", result.err()); assert!(
result.is_ok(),
"Field path to known input should pass, got: {:?}",
result.err()
);
} }
#[test] #[test]
@@ -1458,7 +1634,11 @@ workflow:
equals: success equals: success
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_ok(), "Field path to known output should pass, got: {:?}", result.err()); assert!(
result.is_ok(),
"Field path to known output should pass, got: {:?}",
result.err()
);
} }
#[test] #[test]
@@ -1480,7 +1660,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("unknown input field") && err.contains("nonexistent"), err.contains("unknown input field") && err.contains("nonexistent"),
"Expected unknown input field error, got: {err}" "Expected unknown input field error, got: {err}"
@@ -1508,7 +1691,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("unknown output field") && err.contains("missing"), err.contains("unknown output field") && err.contains("missing"),
"Expected unknown output field error, got: {err}" "Expected unknown output field error, got: {err}"
@@ -1534,7 +1720,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("gt/gte/lt/lte") && err.contains("number/integer"), err.contains("gt/gte/lt/lte") && err.contains("number/integer"),
"Expected type mismatch error, got: {err}" "Expected type mismatch error, got: {err}"
@@ -1559,7 +1748,11 @@ workflow:
gt: 5 gt: 5
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_ok(), "gt on number should pass, got: {:?}", result.err()); assert!(
result.is_ok(),
"gt on number should pass, got: {:?}",
result.err()
);
} }
#[test] #[test]
@@ -1581,7 +1774,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("contains") && err.contains("string/list"), err.contains("contains") && err.contains("string/list"),
"Expected type mismatch error for contains, got: {err}" "Expected type mismatch error for contains, got: {err}"
@@ -1606,7 +1802,11 @@ workflow:
contains: needle contains: needle
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_ok(), "contains on string should pass, got: {:?}", result.err()); assert!(
result.is_ok(),
"contains on string should pass, got: {:?}",
result.err()
);
} }
#[test] #[test]
@@ -1627,7 +1827,11 @@ workflow:
contains: release contains: release
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_ok(), "contains on list should pass, got: {:?}", result.err()); assert!(
result.is_ok(),
"contains on list should pass, got: {:?}",
result.err()
);
} }
#[test] #[test]
@@ -1649,7 +1853,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("is_null/is_not_null") && err.contains("optional"), err.contains("is_null/is_not_null") && err.contains("optional"),
"Expected type mismatch error for is_null, got: {err}" "Expected type mismatch error for is_null, got: {err}"
@@ -1674,7 +1881,11 @@ workflow:
is_null: true is_null: true
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_ok(), "is_null on optional should pass, got: {:?}", result.err()); assert!(
result.is_ok(),
"is_null on optional should pass, got: {:?}",
result.err()
);
} }
#[test] #[test]
@@ -1693,7 +1904,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("never produced") && err.contains("result"), err.contains("never produced") && err.contains("result"),
"Expected unused output error, got: {err}" "Expected unused output error, got: {err}"
@@ -1717,7 +1931,11 @@ workflow:
- name: result - name: result
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_ok(), "Output produced by step should pass, got: {:?}", result.err()); assert!(
result.is_ok(),
"Output produced by step should pass, got: {:?}",
result.err()
);
} }
#[test] #[test]
@@ -1734,7 +1952,11 @@ workflow:
run: echo hi run: echo hi
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_ok(), "No outputs schema should not cause error, got: {:?}", result.err()); assert!(
result.is_ok(),
"No outputs schema should not cause error, got: {:?}",
result.err()
);
} }
#[test] #[test]
@@ -1780,7 +2002,10 @@ workflow:
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("inputs") && err.contains("outputs"), err.contains("inputs") && err.contains("outputs"),
"Expected error about invalid path segment, got: {err}" "Expected error about invalid path segment, got: {err}"
@@ -1826,9 +2051,12 @@ workflow:
let main_path = dir.path().join("main.yaml"); let main_path = dir.path().join("main.yaml");
std::fs::write(&main_path, &main_yaml).unwrap(); std::fs::write(&main_path, &main_yaml).unwrap();
let result = let result = wfe_yaml::load_workflow_with_includes(&main_yaml, &main_path, &HashMap::new());
wfe_yaml::load_workflow_with_includes(&main_yaml, &main_path, &HashMap::new()); assert!(
assert!(result.is_ok(), "Include single file should work, got: {:?}", result.err()); result.is_ok(),
"Include single file should work, got: {:?}",
result.err()
);
let workflows = result.unwrap(); let workflows = result.unwrap();
assert_eq!(workflows.len(), 2); assert_eq!(workflows.len(), 2);
let ids: Vec<&str> = workflows.iter().map(|w| w.definition.id.as_str()).collect(); let ids: Vec<&str> = workflows.iter().map(|w| w.definition.id.as_str()).collect();
@@ -1887,9 +2115,12 @@ workflow:
let main_path = dir.path().join("main.yaml"); let main_path = dir.path().join("main.yaml");
std::fs::write(&main_path, main_yaml).unwrap(); std::fs::write(&main_path, main_yaml).unwrap();
let result = let result = wfe_yaml::load_workflow_with_includes(main_yaml, &main_path, &HashMap::new());
wfe_yaml::load_workflow_with_includes(main_yaml, &main_path, &HashMap::new()); assert!(
assert!(result.is_ok(), "Include multiple files should work, got: {:?}", result.err()); result.is_ok(),
"Include multiple files should work, got: {:?}",
result.err()
);
let workflows = result.unwrap(); let workflows = result.unwrap();
assert_eq!(workflows.len(), 3); assert_eq!(workflows.len(), 3);
} }
@@ -1931,9 +2162,12 @@ workflow:
let main_path = dir.path().join("main.yaml"); let main_path = dir.path().join("main.yaml");
std::fs::write(&main_path, main_yaml).unwrap(); std::fs::write(&main_path, main_yaml).unwrap();
let result = let result = wfe_yaml::load_workflow_with_includes(main_yaml, &main_path, &HashMap::new());
wfe_yaml::load_workflow_with_includes(main_yaml, &main_path, &HashMap::new()); assert!(
assert!(result.is_ok(), "Override should work, got: {:?}", result.err()); result.is_ok(),
"Override should work, got: {:?}",
result.err()
);
let workflows = result.unwrap(); let workflows = result.unwrap();
// Only 1 workflow since main takes precedence over included // Only 1 workflow since main takes precedence over included
assert_eq!(workflows.len(), 1); assert_eq!(workflows.len(), 1);
@@ -1972,10 +2206,12 @@ workflow:
let main_path = dir.path().join("main.yaml"); let main_path = dir.path().join("main.yaml");
std::fs::write(&main_path, main_yaml).unwrap(); std::fs::write(&main_path, main_yaml).unwrap();
let result = let result = wfe_yaml::load_workflow_with_includes(main_yaml, &main_path, &HashMap::new());
wfe_yaml::load_workflow_with_includes(main_yaml, &main_path, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("nonexistent") || err.contains("not found") || err.contains("No such file"), err.contains("nonexistent") || err.contains("not found") || err.contains("No such file"),
"Expected file not found error, got: {err}" "Expected file not found error, got: {err}"
@@ -2018,10 +2254,12 @@ workflow:
let a_path = dir.path().join("a.yaml"); let a_path = dir.path().join("a.yaml");
let result = let result = wfe_yaml::load_workflow_with_includes(a_yaml, &a_path, &HashMap::new());
wfe_yaml::load_workflow_with_includes(a_yaml, &a_path, &HashMap::new());
assert!(result.is_err()); assert!(result.is_err());
let err = match result { Err(e) => e.to_string(), Ok(_) => panic!("expected error") }; let err = match result {
Err(e) => e.to_string(),
Ok(_) => panic!("expected error"),
};
assert!( assert!(
err.contains("Circular include"), err.contains("Circular include"),
"Expected circular include error, got: {err}" "Expected circular include error, got: {err}"
@@ -2053,7 +2291,11 @@ workflow:
equals: true equals: true
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_ok(), "equals should work on all types, got: {:?}", result.err()); assert!(
result.is_ok(),
"equals should work on all types, got: {:?}",
result.err()
);
} }
#[test] #[test]
@@ -2074,7 +2316,11 @@ workflow:
gte: 10 gte: 10
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_ok(), "gte on integer should pass, got: {:?}", result.err()); assert!(
result.is_ok(),
"gte on integer should pass, got: {:?}",
result.err()
);
} }
#[test] #[test]
@@ -2096,5 +2342,9 @@ workflow:
gt: 5 gt: 5
"#; "#;
let result = load_single_workflow_from_str(yaml, &HashMap::new()); let result = load_single_workflow_from_str(yaml, &HashMap::new());
assert!(result.is_ok(), "any type should allow gt, got: {:?}", result.err()); assert!(
result.is_ok(),
"any type should allow gt, got: {:?}",
result.err()
);
} }

View File

@@ -43,11 +43,11 @@ use async_trait::async_trait;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use wfe::WorkflowHostBuilder;
use wfe::builder::WorkflowBuilder; use wfe::builder::WorkflowBuilder;
use wfe::models::*; use wfe::models::*;
use wfe::traits::step::{StepBody, StepExecutionContext};
use wfe::test_support::*; use wfe::test_support::*;
use wfe::WorkflowHostBuilder; use wfe::traits::step::{StepBody, StepExecutionContext};
// ============================================================================= // =============================================================================
// Workflow Data // Workflow Data
@@ -217,10 +217,7 @@ impl StepBody for AddToppings {
async fn run(&mut self, ctx: &StepExecutionContext<'_>) -> wfe::Result<ExecutionResult> { async fn run(&mut self, ctx: &StepExecutionContext<'_>) -> wfe::Result<ExecutionResult> {
if let Some(item) = ctx.item { if let Some(item) = ctx.item {
let pizza: Pizza = serde_json::from_value(item.clone()).unwrap_or_default(); let pizza: Pizza = serde_json::from_value(item.clone()).unwrap_or_default();
println!( println!("[AddToppings] Layering: {}", pizza.toppings.join(", "));
"[AddToppings] Layering: {}",
pizza.toppings.join(", ")
);
if let Some(ref instructions) = pizza.special_instructions { if let Some(ref instructions) = pizza.special_instructions {
println!("[AddToppings] Special: {}", instructions); println!("[AddToppings] Special: {}", instructions);
} }
@@ -334,7 +331,10 @@ impl StepBody for DispatchDriver {
let order: PizzaOrder = serde_json::from_value(ctx.workflow.data.clone())?; let order: PizzaOrder = serde_json::from_value(ctx.workflow.data.clone())?;
println!( println!(
"[DispatchDriver] Driver en route to {}", "[DispatchDriver] Driver en route to {}",
order.delivery_address.as_deref().unwrap_or("unknown address") order
.delivery_address
.as_deref()
.unwrap_or("unknown address")
); );
Ok(ExecutionResult::next()) Ok(ExecutionResult::next())
} }
@@ -347,10 +347,7 @@ struct RingCounterBell;
impl StepBody for RingCounterBell { impl StepBody for RingCounterBell {
async fn run(&mut self, ctx: &StepExecutionContext<'_>) -> wfe::Result<ExecutionResult> { async fn run(&mut self, ctx: &StepExecutionContext<'_>) -> wfe::Result<ExecutionResult> {
let order: PizzaOrder = serde_json::from_value(ctx.workflow.data.clone())?; let order: PizzaOrder = serde_json::from_value(ctx.workflow.data.clone())?;
println!( println!("[RingCounterBell] DING! Order for {}!", order.customer_name);
"[RingCounterBell] DING! Order for {}!",
order.customer_name
);
Ok(ExecutionResult::next()) Ok(ExecutionResult::next())
} }
} }
@@ -386,54 +383,51 @@ fn build_pizza_workflow() -> WorkflowDefinition {
WorkflowBuilder::<PizzaOrder>::new() WorkflowBuilder::<PizzaOrder>::new()
// 1. Validate the order // 1. Validate the order
.start_with::<ValidateOrder>() .start_with::<ValidateOrder>()
.name("Validate Order") .name("Validate Order")
// 2. Charge payment (saga: refund if anything fails downstream) // 2. Charge payment (saga: refund if anything fails downstream)
.then::<ChargePayment>() .then::<ChargePayment>()
.name("Charge Payment") .name("Charge Payment")
.compensate_with::<RefundPayment>() .compensate_with::<RefundPayment>()
// 3. Prep toppings in parallel // 3. Prep toppings in parallel
.parallel(|p| p .parallel(|p| {
.branch(|b| { b.add_step(std::any::type_name::<MakeSauce>()); }) p.branch(|b| {
.branch(|b| { b.add_step(std::any::type_name::<GrateCheese>()); }) b.add_step(std::any::type_name::<MakeSauce>());
.branch(|b| { b.add_step(std::any::type_name::<ChopVegetables>()); }) })
) .branch(|b| {
b.add_step(std::any::type_name::<GrateCheese>());
})
.branch(|b| {
b.add_step(std::any::type_name::<ChopVegetables>());
})
})
// 4. Assemble each pizza (with quality check that retries) // 4. Assemble each pizza (with quality check that retries)
.then::<StretchDough>() .then::<StretchDough>()
.name("Stretch Dough") .name("Stretch Dough")
.then::<AddToppings>() .then::<AddToppings>()
.name("Add Toppings") .name("Add Toppings")
.then::<QualityCheck>() .then::<QualityCheck>()
.name("Quality Check") .name("Quality Check")
.on_error(ErrorBehavior::Retry { .on_error(ErrorBehavior::Retry {
interval: Duration::from_millis(100), interval: Duration::from_millis(100),
max_retries: 3, max_retries: 3,
}) })
// 5. Fire into the oven // 5. Fire into the oven
.then::<FireOven>() .then::<FireOven>()
.name("Fire Oven") .name("Fire Oven")
// 6. Wait for oven timer (external event) // 6. Wait for oven timer (external event)
.wait_for("oven.timer", "oven-1") .wait_for("oven.timer", "oven-1")
.name("Wait for Oven Timer") .name("Wait for Oven Timer")
// 7. Let pizzas cool (delay) // 7. Let pizzas cool (delay)
.delay(Duration::from_millis(50)) .delay(Duration::from_millis(50))
.name("Cooling Rest") .name("Cooling Rest")
// 8. Delivery decision // 8. Delivery decision
.then::<CheckIfDelivery>() .then::<CheckIfDelivery>()
.name("Check Delivery") .name("Check Delivery")
.then::<DispatchDriver>() .then::<DispatchDriver>()
.name("Dispatch or Pickup") .name("Dispatch or Pickup")
// 9. Done! // 9. Done!
.then::<CompleteOrder>() .then::<CompleteOrder>()
.name("Complete Order") .name("Complete Order")
.end_workflow() .end_workflow()
.build("pizza-workflow", 1) .build("pizza-workflow", 1)
} }
@@ -542,7 +536,10 @@ async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
match instance.status { match instance.status {
WorkflowStatus::Complete | WorkflowStatus::Terminated => break instance, WorkflowStatus::Complete | WorkflowStatus::Terminated => break instance,
_ if tokio::time::Instant::now() > deadline => { _ if tokio::time::Instant::now() > deadline => {
println!("\nWorkflow still running after timeout. Status: {:?}", instance.status); println!(
"\nWorkflow still running after timeout. Status: {:?}",
instance.status
);
break instance; break instance;
} }
_ => tokio::time::sleep(Duration::from_millis(100)).await, _ => tokio::time::sleep(Duration::from_millis(100)).await,

View File

@@ -18,9 +18,9 @@ use std::time::Duration;
use serde_json::json; use serde_json::json;
use wfe::models::WorkflowStatus;
use wfe::test_support::{InMemoryLockProvider, InMemoryQueueProvider, InMemoryPersistenceProvider};
use wfe::WorkflowHostBuilder; use wfe::WorkflowHostBuilder;
use wfe::models::WorkflowStatus;
use wfe::test_support::{InMemoryLockProvider, InMemoryPersistenceProvider, InMemoryQueueProvider};
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
@@ -30,7 +30,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.with_timer(tracing_subscriber::fmt::time::uptime()) .with_timer(tracing_subscriber::fmt::time::uptime())
.with_env_filter( .with_env_filter(
std::env::var("RUST_LOG") std::env::var("RUST_LOG")
.unwrap_or_else(|_| "wfe_core=info,wfe=info,run_pipeline=info".into()) .unwrap_or_else(|_| "wfe_core=info,wfe=info,run_pipeline=info".into()),
) )
.init(); .init();
@@ -40,9 +40,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.expect("usage: run_pipeline <workflows.yaml>"); .expect("usage: run_pipeline <workflows.yaml>");
// Read config from WFE_CONFIG env var (JSON map), merged over sensible defaults. // Read config from WFE_CONFIG env var (JSON map), merged over sensible defaults.
let cwd = std::env::current_dir()? let cwd = std::env::current_dir()?.to_string_lossy().to_string();
.to_string_lossy()
.to_string();
// Defaults for every ((var)) referenced in the YAML. // Defaults for every ((var)) referenced in the YAML.
let mut config: HashMap<String, serde_json::Value> = HashMap::from([ let mut config: HashMap<String, serde_json::Value> = HashMap::from([
@@ -151,7 +149,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Print workflow data (contains outputs from all steps). // Print workflow data (contains outputs from all steps).
if let Some(obj) = final_instance.data.as_object() { if let Some(obj) = final_instance.data.as_object() {
println!("\nKey outputs:"); println!("\nKey outputs:");
for key in ["version", "all_tests_passed", "coverage", "published", "released"] { for key in [
"version",
"all_tests_passed",
"coverage",
"published",
"released",
] {
if let Some(val) = obj.get(key) { if let Some(val) = obj.get(key) {
println!(" {key}: {val}"); println!(" {key}: {val}");
} }

View File

@@ -3,12 +3,12 @@ use std::sync::Arc;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use wfe_core::WfeError;
use wfe_core::executor::{StepRegistry, WorkflowExecutor}; use wfe_core::executor::{StepRegistry, WorkflowExecutor};
use wfe_core::traits::{ use wfe_core::traits::{
DistributedLockProvider, LifecyclePublisher, PersistenceProvider, QueueProvider, SearchIndex, DistributedLockProvider, LifecyclePublisher, PersistenceProvider, QueueProvider, SearchIndex,
ServiceProvider, ServiceProvider,
}; };
use wfe_core::WfeError;
use crate::host::WorkflowHost; use crate::host::WorkflowHost;
use crate::registry::InMemoryWorkflowRegistry; use crate::registry::InMemoryWorkflowRegistry;
@@ -86,13 +86,20 @@ impl WorkflowHostBuilder {
/// Returns an error if persistence, lock_provider, or queue_provider have not been set. /// Returns an error if persistence, lock_provider, or queue_provider have not been set.
pub fn build(self) -> wfe_core::Result<WorkflowHost> { pub fn build(self) -> wfe_core::Result<WorkflowHost> {
let persistence = self.persistence.ok_or_else(|| { let persistence = self.persistence.ok_or_else(|| {
WfeError::Other("PersistenceProvider is required. Call .use_persistence() before .build().".into()) WfeError::Other(
"PersistenceProvider is required. Call .use_persistence() before .build().".into(),
)
})?; })?;
let lock_provider = self.lock_provider.ok_or_else(|| { let lock_provider = self.lock_provider.ok_or_else(|| {
WfeError::Other("DistributedLockProvider is required. Call .use_lock_provider() before .build().".into()) WfeError::Other(
"DistributedLockProvider is required. Call .use_lock_provider() before .build()."
.into(),
)
})?; })?;
let queue_provider = self.queue_provider.ok_or_else(|| { let queue_provider = self.queue_provider.ok_or_else(|| {
WfeError::Other("QueueProvider is required. Call .use_queue_provider() before .build().".into()) WfeError::Other(
"QueueProvider is required. Call .use_queue_provider() before .build().".into(),
)
})?; })?;
let mut executor = WorkflowExecutor::new( let mut executor = WorkflowExecutor::new(

View File

@@ -1,8 +1,8 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use wfe_core::Result;
use wfe_core::models::WorkflowStatus; use wfe_core::models::WorkflowStatus;
use wfe_core::traits::PersistenceProvider; use wfe_core::traits::PersistenceProvider;
use wfe_core::Result;
/// Purge workflows matching a given status that were created before `older_than`. /// Purge workflows matching a given status that were created before `older_than`.
/// ///

View File

@@ -3,17 +3,15 @@ use std::time::Duration;
use async_trait::async_trait; use async_trait::async_trait;
use wfe::WorkflowHostBuilder;
use wfe::models::{ use wfe::models::{
ErrorBehavior, ExecutionResult, PointerStatus, StepOutcome, WorkflowDefinition, ErrorBehavior, ExecutionResult, PointerStatus, StepOutcome, WorkflowDefinition, WorkflowStep,
WorkflowStep,
}; };
use wfe::traits::step::{StepBody, StepExecutionContext}; use wfe::traits::step::{StepBody, StepExecutionContext};
use wfe::WorkflowHostBuilder;
use wfe_core::test_support::{ use wfe_core::test_support::{
InMemoryLockProvider, InMemoryPersistenceProvider, InMemoryQueueProvider, InMemoryLockProvider, InMemoryPersistenceProvider, InMemoryQueueProvider,
}; };
/// Step 1: succeeds normally. /// Step 1: succeeds normally.
#[derive(Default)] #[derive(Default)]
struct SucceedingStep; struct SucceedingStep;
@@ -32,9 +30,7 @@ struct FailingStep;
#[async_trait] #[async_trait]
impl StepBody for FailingStep { impl StepBody for FailingStep {
async fn run(&mut self, _ctx: &StepExecutionContext<'_>) -> wfe_core::Result<ExecutionResult> { async fn run(&mut self, _ctx: &StepExecutionContext<'_>) -> wfe_core::Result<ExecutionResult> {
Err(wfe_core::WfeError::StepExecution( Err(wfe_core::WfeError::StepExecution("Step2 failed".into()))
"Step2 failed".into(),
))
} }
} }
@@ -86,7 +82,8 @@ async fn compensation_step_runs_on_failure() {
.use_persistence(persistence.clone() as Arc<dyn wfe_core::traits::PersistenceProvider>) .use_persistence(persistence.clone() as Arc<dyn wfe_core::traits::PersistenceProvider>)
.use_lock_provider(lock as Arc<dyn wfe_core::traits::DistributedLockProvider>) .use_lock_provider(lock as Arc<dyn wfe_core::traits::DistributedLockProvider>)
.use_queue_provider(queue as Arc<dyn wfe_core::traits::QueueProvider>) .use_queue_provider(queue as Arc<dyn wfe_core::traits::QueueProvider>)
.build().unwrap(); .build()
.unwrap();
let def = build_compensation_definition(); let def = build_compensation_definition();
host.register_step::<SucceedingStep>().await; host.register_step::<SucceedingStep>().await;

View File

@@ -3,16 +3,13 @@ use std::time::Duration;
use async_trait::async_trait; use async_trait::async_trait;
use wfe::models::{ use wfe::models::{ExecutionResult, StepOutcome, WorkflowDefinition, WorkflowStatus, WorkflowStep};
ExecutionResult, StepOutcome, WorkflowDefinition, WorkflowStatus, WorkflowStep,
};
use wfe::traits::step::{StepBody, StepExecutionContext}; use wfe::traits::step::{StepBody, StepExecutionContext};
use wfe::{WorkflowHostBuilder, run_workflow_sync}; use wfe::{WorkflowHostBuilder, run_workflow_sync};
use wfe_core::test_support::{ use wfe_core::test_support::{
InMemoryLockProvider, InMemoryPersistenceProvider, InMemoryQueueProvider, InMemoryLockProvider, InMemoryPersistenceProvider, InMemoryQueueProvider,
}; };
/// A step that sleeps for a very short duration (10ms), then proceeds. /// A step that sleeps for a very short duration (10ms), then proceeds.
/// Tracks whether it has already slept via persistence_data. /// Tracks whether it has already slept via persistence_data.
#[derive(Default)] #[derive(Default)]
@@ -74,7 +71,8 @@ async fn delay_step_completes_after_sleep() {
.use_persistence(persistence.clone() as Arc<dyn wfe_core::traits::PersistenceProvider>) .use_persistence(persistence.clone() as Arc<dyn wfe_core::traits::PersistenceProvider>)
.use_lock_provider(lock as Arc<dyn wfe_core::traits::DistributedLockProvider>) .use_lock_provider(lock as Arc<dyn wfe_core::traits::DistributedLockProvider>)
.use_queue_provider(queue as Arc<dyn wfe_core::traits::QueueProvider>) .use_queue_provider(queue as Arc<dyn wfe_core::traits::QueueProvider>)
.build().unwrap(); .build()
.unwrap();
let def = build_delay_definition(); let def = build_delay_definition();
host.register_step::<ShortDelayStep>().await; host.register_step::<ShortDelayStep>().await;
@@ -100,7 +98,10 @@ async fn delay_step_completes_after_sleep() {
.iter() .iter()
.filter(|p| p.status == wfe::models::PointerStatus::Complete) .filter(|p| p.status == wfe::models::PointerStatus::Complete)
.count(); .count();
assert_eq!(complete_count, 2, "Expected both delay step and after-delay step to complete"); assert_eq!(
complete_count, 2,
"Expected both delay step and after-delay step to complete"
);
host.stop().await; host.stop().await;
} }

View File

@@ -4,8 +4,8 @@ use std::time::Duration;
use async_trait::async_trait; use async_trait::async_trait;
use wfe::models::{ use wfe::models::{
ErrorBehavior, ExecutionResult, PointerStatus, StepOutcome, WorkflowDefinition, ErrorBehavior, ExecutionResult, PointerStatus, StepOutcome, WorkflowDefinition, WorkflowStatus,
WorkflowStatus, WorkflowStep, WorkflowStep,
}; };
use wfe::traits::step::{StepBody, StepExecutionContext}; use wfe::traits::step::{StepBody, StepExecutionContext};
use wfe::{WorkflowHostBuilder, run_workflow_sync}; use wfe::{WorkflowHostBuilder, run_workflow_sync};
@@ -13,7 +13,6 @@ use wfe_core::test_support::{
InMemoryLockProvider, InMemoryPersistenceProvider, InMemoryQueueProvider, InMemoryLockProvider, InMemoryPersistenceProvider, InMemoryQueueProvider,
}; };
/// A step that fails on the first attempt but succeeds on retry. /// A step that fails on the first attempt but succeeds on retry.
/// Uses retry_count on the execution pointer to track attempts. /// Uses retry_count on the execution pointer to track attempts.
#[derive(Default)] #[derive(Default)]

View File

@@ -4,16 +4,15 @@ use std::time::Duration;
use async_trait::async_trait; use async_trait::async_trait;
use chrono::Utc; use chrono::Utc;
use wfe::WorkflowHostBuilder;
use wfe::models::{ use wfe::models::{
ExecutionResult, PointerStatus, StepOutcome, WorkflowDefinition, WorkflowStatus, WorkflowStep, ExecutionResult, PointerStatus, StepOutcome, WorkflowDefinition, WorkflowStatus, WorkflowStep,
}; };
use wfe::traits::step::{StepBody, StepExecutionContext}; use wfe::traits::step::{StepBody, StepExecutionContext};
use wfe::WorkflowHostBuilder;
use wfe_core::test_support::{ use wfe_core::test_support::{
InMemoryLockProvider, InMemoryPersistenceProvider, InMemoryQueueProvider, InMemoryLockProvider, InMemoryPersistenceProvider, InMemoryQueueProvider,
}; };
/// A step that waits for "approval" event with key "request-1". /// A step that waits for "approval" event with key "request-1".
#[derive(Default)] #[derive(Default)]
struct WaitForApprovalStep; struct WaitForApprovalStep;
@@ -68,7 +67,8 @@ async fn event_workflow_waits_then_resumes_on_publish() {
.use_persistence(persistence.clone() as Arc<dyn wfe_core::traits::PersistenceProvider>) .use_persistence(persistence.clone() as Arc<dyn wfe_core::traits::PersistenceProvider>)
.use_lock_provider(lock as Arc<dyn wfe_core::traits::DistributedLockProvider>) .use_lock_provider(lock as Arc<dyn wfe_core::traits::DistributedLockProvider>)
.use_queue_provider(queue as Arc<dyn wfe_core::traits::QueueProvider>) .use_queue_provider(queue as Arc<dyn wfe_core::traits::QueueProvider>)
.build().unwrap(); .build()
.unwrap();
let def = build_event_definition(); let def = build_event_definition();
host.register_step::<WaitForApprovalStep>().await; host.register_step::<WaitForApprovalStep>().await;
@@ -134,7 +134,10 @@ async fn event_workflow_waits_then_resumes_on_publish() {
.execution_pointers .execution_pointers
.iter() .iter()
.find(|p| p.event_data.is_some()); .find(|p| p.event_data.is_some());
assert!(wait_pointer.is_some(), "Expected event_data to be set on the waiting pointer"); assert!(
wait_pointer.is_some(),
"Expected event_data to be set on the waiting pointer"
);
let event_data = wait_pointer.unwrap().event_data.as_ref().unwrap(); let event_data = wait_pointer.unwrap().event_data.as_ref().unwrap();
assert_eq!(event_data, &serde_json::json!({"approved": true})); assert_eq!(event_data, &serde_json::json!({"approved": true}));

View File

@@ -99,7 +99,8 @@ async fn foreach_processes_all_items() {
.use_persistence(persistence as Arc<dyn wfe_core::traits::PersistenceProvider>) .use_persistence(persistence as Arc<dyn wfe_core::traits::PersistenceProvider>)
.use_lock_provider(lock as Arc<dyn wfe_core::traits::DistributedLockProvider>) .use_lock_provider(lock as Arc<dyn wfe_core::traits::DistributedLockProvider>)
.use_queue_provider(queue as Arc<dyn wfe_core::traits::QueueProvider>) .use_queue_provider(queue as Arc<dyn wfe_core::traits::QueueProvider>)
.build().unwrap(); .build()
.unwrap();
host.register_step::<DataDrivenForEachStep>().await; host.register_step::<DataDrivenForEachStep>().await;
host.register_step::<ProcessItemStep>().await; host.register_step::<ProcessItemStep>().await;

View File

@@ -53,7 +53,8 @@ async fn linear_three_step_workflow_completes() {
.use_persistence(persistence.clone() as Arc<dyn wfe_core::traits::PersistenceProvider>) .use_persistence(persistence.clone() as Arc<dyn wfe_core::traits::PersistenceProvider>)
.use_lock_provider(lock as Arc<dyn wfe_core::traits::DistributedLockProvider>) .use_lock_provider(lock as Arc<dyn wfe_core::traits::DistributedLockProvider>)
.use_queue_provider(queue as Arc<dyn wfe_core::traits::QueueProvider>) .use_queue_provider(queue as Arc<dyn wfe_core::traits::QueueProvider>)
.build().unwrap(); .build()
.unwrap();
host.register_step::<IncrementStep>().await; host.register_step::<IncrementStep>().await;
host.register_workflow_definition(def).await; host.register_workflow_definition(def).await;

View File

@@ -13,7 +13,6 @@ use wfe_core::test_support::{
InMemoryLockProvider, InMemoryPersistenceProvider, InMemoryQueueProvider, InMemoryLockProvider, InMemoryPersistenceProvider, InMemoryQueueProvider,
}; };
/// Initial step before parallel. /// Initial step before parallel.
#[derive(Default)] #[derive(Default)]
struct StartStep; struct StartStep;
@@ -117,7 +116,8 @@ async fn parallel_branches_both_complete() {
.use_persistence(persistence.clone() as Arc<dyn wfe_core::traits::PersistenceProvider>) .use_persistence(persistence.clone() as Arc<dyn wfe_core::traits::PersistenceProvider>)
.use_lock_provider(lock as Arc<dyn wfe_core::traits::DistributedLockProvider>) .use_lock_provider(lock as Arc<dyn wfe_core::traits::DistributedLockProvider>)
.use_queue_provider(queue as Arc<dyn wfe_core::traits::QueueProvider>) .use_queue_provider(queue as Arc<dyn wfe_core::traits::QueueProvider>)
.build().unwrap(); .build()
.unwrap();
host.register_step::<StartStep>().await; host.register_step::<StartStep>().await;
host.register_step::<ParallelContainerStep>().await; host.register_step::<ParallelContainerStep>().await;
@@ -144,7 +144,10 @@ async fn parallel_branches_both_complete() {
.iter() .iter()
.filter(|p| p.status == PointerStatus::Complete && !p.scope.is_empty()) .filter(|p| p.status == PointerStatus::Complete && !p.scope.is_empty())
.count(); .count();
assert_eq!(branch_completions, 2, "Expected both parallel branches to complete"); assert_eq!(
branch_completions, 2,
"Expected both parallel branches to complete"
);
host.stop().await; host.stop().await;
} }

View File

@@ -12,7 +12,6 @@ use wfe_core::test_support::{
InMemoryLockProvider, InMemoryPersistenceProvider, InMemoryQueueProvider, InMemoryLockProvider, InMemoryPersistenceProvider, InMemoryQueueProvider,
}; };
/// Version 1 uses a single step. /// Version 1 uses a single step.
#[derive(Default)] #[derive(Default)]
struct V1Step; struct V1Step;
@@ -75,14 +74,17 @@ async fn version_1_uses_v1_definition() {
.use_persistence(persistence.clone() as Arc<dyn wfe_core::traits::PersistenceProvider>) .use_persistence(persistence.clone() as Arc<dyn wfe_core::traits::PersistenceProvider>)
.use_lock_provider(lock as Arc<dyn wfe_core::traits::DistributedLockProvider>) .use_lock_provider(lock as Arc<dyn wfe_core::traits::DistributedLockProvider>)
.use_queue_provider(queue as Arc<dyn wfe_core::traits::QueueProvider>) .use_queue_provider(queue as Arc<dyn wfe_core::traits::QueueProvider>)
.build().unwrap(); .build()
.unwrap();
host.register_step::<V1Step>().await; host.register_step::<V1Step>().await;
host.register_step::<V2StepA>().await; host.register_step::<V2StepA>().await;
host.register_step::<V2StepB>().await; host.register_step::<V2StepB>().await;
host.register_workflow_definition(build_v1_definition()).await; host.register_workflow_definition(build_v1_definition())
host.register_workflow_definition(build_v2_definition()).await; .await;
host.register_workflow_definition(build_v2_definition())
.await;
host.start().await.unwrap(); host.start().await.unwrap();
@@ -121,14 +123,17 @@ async fn version_2_uses_v2_definition() {
.use_persistence(persistence.clone() as Arc<dyn wfe_core::traits::PersistenceProvider>) .use_persistence(persistence.clone() as Arc<dyn wfe_core::traits::PersistenceProvider>)
.use_lock_provider(lock as Arc<dyn wfe_core::traits::DistributedLockProvider>) .use_lock_provider(lock as Arc<dyn wfe_core::traits::DistributedLockProvider>)
.use_queue_provider(queue as Arc<dyn wfe_core::traits::QueueProvider>) .use_queue_provider(queue as Arc<dyn wfe_core::traits::QueueProvider>)
.build().unwrap(); .build()
.unwrap();
host.register_step::<V1Step>().await; host.register_step::<V1Step>().await;
host.register_step::<V2StepA>().await; host.register_step::<V2StepA>().await;
host.register_step::<V2StepB>().await; host.register_step::<V2StepB>().await;
host.register_workflow_definition(build_v1_definition()).await; host.register_workflow_definition(build_v1_definition())
host.register_workflow_definition(build_v2_definition()).await; .await;
host.register_workflow_definition(build_v2_definition())
.await;
host.start().await.unwrap(); host.start().await.unwrap();
@@ -167,14 +172,17 @@ async fn both_versions_coexist_and_run_independently() {
.use_persistence(persistence.clone() as Arc<dyn wfe_core::traits::PersistenceProvider>) .use_persistence(persistence.clone() as Arc<dyn wfe_core::traits::PersistenceProvider>)
.use_lock_provider(lock as Arc<dyn wfe_core::traits::DistributedLockProvider>) .use_lock_provider(lock as Arc<dyn wfe_core::traits::DistributedLockProvider>)
.use_queue_provider(queue as Arc<dyn wfe_core::traits::QueueProvider>) .use_queue_provider(queue as Arc<dyn wfe_core::traits::QueueProvider>)
.build().unwrap(); .build()
.unwrap();
host.register_step::<V1Step>().await; host.register_step::<V1Step>().await;
host.register_step::<V2StepA>().await; host.register_step::<V2StepA>().await;
host.register_step::<V2StepB>().await; host.register_step::<V2StepB>().await;
host.register_workflow_definition(build_v1_definition()).await; host.register_workflow_definition(build_v1_definition())
host.register_workflow_definition(build_v2_definition()).await; .await;
host.register_workflow_definition(build_v2_definition())
.await;
host.start().await.unwrap(); host.start().await.unwrap();
@@ -197,8 +205,7 @@ async fn both_versions_coexist_and_run_independently() {
} }
let inst_v1 = host.get_workflow(&id_v1).await.unwrap(); let inst_v1 = host.get_workflow(&id_v1).await.unwrap();
let inst_v2 = host.get_workflow(&id_v2).await.unwrap(); let inst_v2 = host.get_workflow(&id_v2).await.unwrap();
if inst_v1.status == WorkflowStatus::Complete if inst_v1.status == WorkflowStatus::Complete && inst_v2.status == WorkflowStatus::Complete
&& inst_v2.status == WorkflowStatus::Complete
{ {
break; break;
} }

View File

@@ -112,7 +112,8 @@ async fn while_loop_runs_three_iterations() {
.use_persistence(persistence as Arc<dyn wfe_core::traits::PersistenceProvider>) .use_persistence(persistence as Arc<dyn wfe_core::traits::PersistenceProvider>)
.use_lock_provider(lock as Arc<dyn wfe_core::traits::DistributedLockProvider>) .use_lock_provider(lock as Arc<dyn wfe_core::traits::DistributedLockProvider>)
.use_queue_provider(queue as Arc<dyn wfe_core::traits::QueueProvider>) .use_queue_provider(queue as Arc<dyn wfe_core::traits::QueueProvider>)
.build().unwrap(); .build()
.unwrap();
host.register_step::<CountingWhileStep>().await; host.register_step::<CountingWhileStep>().await;
host.register_step::<LoopBodyStep>().await; host.register_step::<LoopBodyStep>().await;

Some files were not shown because too many files have changed in this diff Show More