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:
@@ -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};
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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, ®istry, None).await.unwrap();
|
executor
|
||||||
|
.execute(&instance.id, &def, ®istry, 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, ®istry, None).await.unwrap();
|
executor
|
||||||
|
.execute(&instance.id, &def, ®istry, 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, ®istry, None).await.unwrap();
|
executor
|
||||||
|
.execute(&instance.id, &def, ®istry, 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, ®istry, None).await.unwrap();
|
executor
|
||||||
|
.execute(&instance.id, &def, ®istry, 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, ®istry, None).await.unwrap();
|
executor
|
||||||
|
.execute(&instance.id, &def, ®istry, 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, ®istry, None).await.unwrap();
|
executor
|
||||||
|
.execute(&instance.id, &def, ®istry, 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, ®istry, None).await.unwrap();
|
executor
|
||||||
|
.execute(&instance.id, &def, ®istry, 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, ®istry, None).await.unwrap();
|
executor
|
||||||
|
.execute(&instance.id, &def, ®istry, 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, ®istry, None).await.unwrap();
|
executor
|
||||||
|
.execute(&instance.id, &def, ®istry, 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, ®istry, None).await.unwrap();
|
executor
|
||||||
|
.execute(&instance.id, &def, ®istry, 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, ®istry, None).await.unwrap();
|
executor
|
||||||
|
.execute(&instance.id, &def, ®istry, 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, ®istry, None).await.unwrap();
|
executor
|
||||||
|
.execute(&instance.id, &def, ®istry, 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, ®istry, None).await.unwrap();
|
executor
|
||||||
|
.execute(&instance.id, &def, ®istry, 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, ®istry, None).await.unwrap();
|
executor
|
||||||
|
.execute(&instance.id, &def, ®istry, 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, ®istry, None).await.unwrap();
|
executor
|
||||||
|
.execute(&instance.id, &def, ®istry, 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, ®istry, None).await.unwrap();
|
executor
|
||||||
|
.execute(&instance.id, &def, ®istry, 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, ®istry, None).await.unwrap();
|
executor
|
||||||
|
.execute(&instance.id, &def, ®istry, 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, ®istry, None).await.unwrap();
|
executor
|
||||||
|
.execute(&instance.id, &def, ®istry, 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, ®istry, None).await.unwrap();
|
executor
|
||||||
|
.execute(&instance.id, &def, ®istry, 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, ®istry, None).await.unwrap();
|
executor
|
||||||
|
.execute(&instance.id, &def, ®istry, 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, ®istry, None).await.unwrap();
|
executor
|
||||||
|
.execute(&instance.id, &def, ®istry, 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, ®istry, None).await.unwrap();
|
executor
|
||||||
|
.execute(&instance.id, &def, ®istry, 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, ®istry, None).await.unwrap();
|
executor
|
||||||
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap();
|
.execute(&instance.id, &def, ®istry, 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, ®istry, None).await.unwrap();
|
executor
|
||||||
let updated = persistence.get_workflow_instance(&instance.id).await.unwrap();
|
.execute(&instance.id, &def, ®istry, 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, ®istry, None).await.unwrap();
|
executor
|
||||||
|
.execute(&instance.id, &def, ®istry, 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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}))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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}))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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}))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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, ¶ms).await.map_err(|e| {
|
let stream = pods.log_stream(pod_name, ¶ms).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();
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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![],
|
||||||
|
|||||||
@@ -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)) => {
|
||||||
|
|||||||
@@ -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())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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"
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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('.') {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|
||||||
|
|||||||
@@ -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"],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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(", ")
|
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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",
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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`.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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}));
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user