fix: support arbitrary types in task dependencies
Enable tasks to accept ANY type implementing Clone + Send + Sync as dependencies, without requiring users to implement ExtractInput trait. The #[task] macro now generates task-specific extraction logic inline via a new extract_and_run() method. This leverages the macro's perfect compile-time knowledge of parameter types to handle extraction for: - Zero parameters (unit type, source tasks) - Single parameter (any Clone + Send + Sync type) - Multiple parameters (tuples, awaited concurrently) Previously, only hardcoded primitive types and specific stdlib types worked. Custom user types required manual ExtractInput implementations. Now custom types work automatically with zero API changes. Changes: - Add extract_and_run() method to Task trait (internal/hidden) - Generate inline extraction in #[task] macro based on parameter count - Remove ExtractInput bound from DagRunner::add_task() - Retain ExtractInput for internal task_fn() test helper - Add comprehensive custom type tests No breaking changes - purely internal implementation improvement.
This commit is contained in:
7 files changed
+300
-14
No files matched your search
@@ -323,6 +323,112 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
.iter()
|
.iter()
|
||||||
.any(|arg| matches!(arg, FnArg::Receiver(_)));
|
.any(|arg| matches!(arg, FnArg::Receiver(_)));
|
||||||
|
|
||||||
|
// Generate extract_and_run implementation based on parameter count
|
||||||
|
let extract_and_run_impl = match params.len() {
|
||||||
|
0 => {
|
||||||
|
// Zero parameters - no extraction needed
|
||||||
|
quote! {
|
||||||
|
fn extract_and_run(
|
||||||
|
self,
|
||||||
|
_receivers: Vec<Box<dyn std::any::Any + Send>>,
|
||||||
|
) -> impl std::future::Future<Output = Result<Self::Output, String>> + Send {
|
||||||
|
async move {
|
||||||
|
let input = ();
|
||||||
|
Ok(self.run(input).await)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
// Single parameter
|
||||||
|
let param_type = ¶ms[0].1;
|
||||||
|
quote! {
|
||||||
|
fn extract_and_run(
|
||||||
|
self,
|
||||||
|
mut receivers: Vec<Box<dyn std::any::Any + Send>>,
|
||||||
|
) -> impl std::future::Future<Output = Result<Self::Output, String>> + Send {
|
||||||
|
async move {
|
||||||
|
use futures::channel::oneshot;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
if receivers.len() != 1 {
|
||||||
|
return Err(format!("Expected 1 dependency, got {}", receivers.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let rx = *receivers.pop()
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<oneshot::Receiver<Arc<#param_type>>>()
|
||||||
|
.map_err(|_| format!("Type mismatch: expected Arc<{}>", std::any::type_name::<#param_type>()))?;
|
||||||
|
|
||||||
|
let arc_value = rx.await
|
||||||
|
.map_err(|_| "Channel closed before receiving value".to_string())?;
|
||||||
|
|
||||||
|
let input = (*arc_value).clone();
|
||||||
|
Ok(self.run(input).await)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Multiple parameters
|
||||||
|
let param_count = params.len();
|
||||||
|
let param_types: Vec<_> = params.iter().map(|(_, ty)| ty).collect();
|
||||||
|
let indices: Vec<_> = (0..param_count).collect();
|
||||||
|
|
||||||
|
// Generate unique receiver variable names
|
||||||
|
let rx_vars: Vec<_> = (0..param_count)
|
||||||
|
.map(|i| syn::Ident::new(&format!("rx_{}", i), proc_macro2::Span::call_site()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Create syn::Index for tuple field access (avoids the suffix warning)
|
||||||
|
let syn_indices: Vec<_> = (0..param_count).map(syn::Index::from).collect();
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
fn extract_and_run(
|
||||||
|
self,
|
||||||
|
receivers: Vec<Box<dyn std::any::Any + Send>>,
|
||||||
|
) -> impl std::future::Future<Output = Result<Self::Output, String>> + Send {
|
||||||
|
async move {
|
||||||
|
use futures::channel::oneshot;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
let expected_count = #param_count;
|
||||||
|
if receivers.len() != expected_count {
|
||||||
|
return Err(format!("Expected {} dependencies, got {}", expected_count, receivers.len()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut iter = receivers.into_iter();
|
||||||
|
|
||||||
|
// Extract each receiver
|
||||||
|
#(
|
||||||
|
let #rx_vars = *iter.next()
|
||||||
|
.ok_or_else(|| format!("Missing receiver at index {}", #indices))?
|
||||||
|
.downcast::<oneshot::Receiver<Arc<#param_types>>>()
|
||||||
|
.map_err(|_| format!("Type mismatch at index {}: expected Arc<{}>",
|
||||||
|
#indices, std::any::type_name::<#param_types>()))?;
|
||||||
|
)*
|
||||||
|
|
||||||
|
// Await all channels concurrently
|
||||||
|
let arc_results = futures::join!(
|
||||||
|
#(
|
||||||
|
async move {
|
||||||
|
#rx_vars.await.map_err(|_| format!("Channel {} closed", #indices))
|
||||||
|
}
|
||||||
|
),*
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clone inner values and build tuple
|
||||||
|
let input = (#(
|
||||||
|
(*arc_results.#syn_indices?).clone()
|
||||||
|
),*);
|
||||||
|
|
||||||
|
Ok(self.run(input).await)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Generate the Task trait implementation based on whether we have self and async/sync
|
// Generate the Task trait implementation based on whether we have self and async/sync
|
||||||
let expanded = if has_self_receiver {
|
let expanded = if has_self_receiver {
|
||||||
// Stateful task - consumes self but delegates to a method that borrows
|
// Stateful task - consumes self but delegates to a method that borrows
|
||||||
@@ -337,6 +443,8 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
let #param_destructure = input;
|
let #param_destructure = input;
|
||||||
self.run_impl(#param_refs).await
|
self.run_impl(#param_refs).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#extract_and_run_impl
|
||||||
}
|
}
|
||||||
|
|
||||||
impl #struct_name {
|
impl #struct_name {
|
||||||
@@ -354,6 +462,8 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
let #param_destructure = input;
|
let #param_destructure = input;
|
||||||
self.run_impl(#param_refs)
|
self.run_impl(#param_refs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#extract_and_run_impl
|
||||||
}
|
}
|
||||||
|
|
||||||
impl #struct_name {
|
impl #struct_name {
|
||||||
@@ -374,6 +484,8 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
let #param_destructure = input;
|
let #param_destructure = input;
|
||||||
Self::run_impl(#param_refs).await
|
Self::run_impl(#param_refs).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#extract_and_run_impl
|
||||||
}
|
}
|
||||||
|
|
||||||
impl #struct_name {
|
impl #struct_name {
|
||||||
@@ -392,6 +504,8 @@ pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
|||||||
let #param_destructure = input;
|
let #param_destructure = input;
|
||||||
Self::run_impl(#param_refs)
|
Self::run_impl(#param_refs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#extract_and_run_impl
|
||||||
}
|
}
|
||||||
|
|
||||||
impl #struct_name {
|
impl #struct_name {
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ use futures::channel::oneshot;
|
|||||||
///
|
///
|
||||||
/// Type erasure occurs only at the ExecutableNode trait boundary - by the time
|
/// Type erasure occurs only at the ExecutableNode trait boundary - by the time
|
||||||
/// we're in ExtractInput, we know the concrete type and can safely downcast.
|
/// we're in ExtractInput, we know the concrete type and can safely downcast.
|
||||||
|
///
|
||||||
|
/// NOTE: This trait is now primarily used by the internal `task_fn` test helper.
|
||||||
|
/// The `#[task]` macro generates inline extraction logic via `extract_and_run()`,
|
||||||
|
/// which allows ANY type to work without requiring ExtractInput implementations.
|
||||||
|
#[allow(dead_code)]
|
||||||
pub(crate) trait ExtractInput: Sized + Clone {
|
pub(crate) trait ExtractInput: Sized + Clone {
|
||||||
fn extract_from_channels(
|
fn extract_from_channels(
|
||||||
receivers: Vec<Box<dyn Any + Send>>,
|
receivers: Vec<Box<dyn Any + Send>>,
|
||||||
|
|||||||
+7
-10
@@ -14,7 +14,6 @@ use std::sync::Arc;
|
|||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
|
|
||||||
use crate::error::DagError;
|
use crate::error::DagError;
|
||||||
use crate::extract::ExtractInput;
|
|
||||||
use crate::task::Task;
|
use crate::task::Task;
|
||||||
use crate::types::NodeId;
|
use crate::types::NodeId;
|
||||||
|
|
||||||
@@ -84,7 +83,7 @@ impl<T: Task + 'static> TypedNode<T> {
|
|||||||
|
|
||||||
impl<T: Task + 'static> ExecutableNode for TypedNode<T>
|
impl<T: Task + 'static> ExecutableNode for TypedNode<T>
|
||||||
where
|
where
|
||||||
T::Input: 'static + Clone + ExtractInput,
|
T::Input: 'static + Clone,
|
||||||
T::Output: 'static + Clone,
|
T::Output: 'static + Clone,
|
||||||
{
|
{
|
||||||
fn create_output_channels(&self, num_dependents: usize) -> ChannelPair {
|
fn create_output_channels(&self, num_dependents: usize) -> ChannelPair {
|
||||||
@@ -107,24 +106,22 @@ where
|
|||||||
senders: ErasedChannels,
|
senders: ErasedChannels,
|
||||||
) -> ExecuteFuture {
|
) -> ExecuteFuture {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
// Extract typed input from type-erased receivers
|
// Use the task's extract_and_run method which has inline extraction logic
|
||||||
// TypedNode<T> knows T::Input, so it can safely downcast
|
// generated by the #[task] macro. This allows ANY type to work without
|
||||||
let input = match T::Input::extract_from_channels(receivers).await {
|
// requiring ExtractInput trait implementations.
|
||||||
Ok(input) => input,
|
let output = match self.task.extract_and_run(receivers).await {
|
||||||
|
Ok(output) => output,
|
||||||
Err(_msg) => {
|
Err(_msg) => {
|
||||||
// Failed to extract input - likely channel closed or type mismatch
|
// Failed to extract input - likely channel closed or type mismatch
|
||||||
return Err(DagError::InvalidDependency { task_id: self.id.0 });
|
return Err(DagError::InvalidDependency { task_id: self.id.0 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Execute task (consumes self.task)
|
|
||||||
let output = self.task.run(input).await;
|
|
||||||
|
|
||||||
// Arc Wrapping Strategy:
|
// Arc Wrapping Strategy:
|
||||||
// Wrap output in Arc ONCE for efficient fan-out sharing.
|
// Wrap output in Arc ONCE for efficient fan-out sharing.
|
||||||
// For 1:N dependencies, this enables O(1) sharing (Arc clone = atomic increment)
|
// For 1:N dependencies, this enables O(1) sharing (Arc clone = atomic increment)
|
||||||
// instead of O(N) cloning (N deep copies of output data).
|
// instead of O(N) cloning (N deep copies of output data).
|
||||||
// Downstream tasks receive &T after ExtractInput extracts from Arc.
|
// Downstream tasks receive &T after extraction from Arc.
|
||||||
let arc_output = Arc::new(output);
|
let arc_output = Arc::new(output);
|
||||||
|
|
||||||
// Send Arc to all dependents via their channels
|
// Send Arc to all dependents via their channels
|
||||||
|
|||||||
+1
-2
@@ -22,7 +22,6 @@ use tracing::{debug, error, info, trace};
|
|||||||
|
|
||||||
use crate::builder::TaskBuilder;
|
use crate::builder::TaskBuilder;
|
||||||
use crate::error::{DagError, DagResult};
|
use crate::error::{DagError, DagResult};
|
||||||
use crate::extract::ExtractInput;
|
|
||||||
use crate::node::{ExecutableNode, TypedNode};
|
use crate::node::{ExecutableNode, TypedNode};
|
||||||
use crate::task::Task;
|
use crate::task::Task;
|
||||||
use crate::types::{NodeId, Pending, TaskHandle};
|
use crate::types::{NodeId, Pending, TaskHandle};
|
||||||
@@ -196,7 +195,7 @@ impl DagRunner {
|
|||||||
pub fn add_task<Tk>(&self, task: Tk) -> TaskBuilder<'_, Tk, Pending>
|
pub fn add_task<Tk>(&self, task: Tk) -> TaskBuilder<'_, Tk, Pending>
|
||||||
where
|
where
|
||||||
Tk: Task + 'static,
|
Tk: Task + 'static,
|
||||||
Tk::Input: 'static + Clone + ExtractInput,
|
Tk::Input: 'static + Clone,
|
||||||
Tk::Output: 'static + Clone,
|
Tk::Output: 'static + Clone,
|
||||||
{
|
{
|
||||||
let id = self.alloc_id();
|
let id = self.alloc_id();
|
||||||
|
|||||||
+25
-2
@@ -93,6 +93,17 @@ pub trait Task: Send {
|
|||||||
type Output: Send + Sync; // Sync required for Arc-wrapping and cross-thread sharing
|
type Output: Send + Sync; // Sync required for Arc-wrapping and cross-thread sharing
|
||||||
|
|
||||||
fn run(self, input: Self::Input) -> impl Future<Output = Self::Output> + Send;
|
fn run(self, input: Self::Input) -> impl Future<Output = Self::Output> + Send;
|
||||||
|
|
||||||
|
/// Internal method: Extract input from type-erased channels and execute the task.
|
||||||
|
///
|
||||||
|
/// This method is implemented automatically by the #[task] macro with inline
|
||||||
|
/// extraction logic based on the task's parameter signature. This allows ANY
|
||||||
|
/// type to work without requiring trait implementations.
|
||||||
|
#[doc(hidden)]
|
||||||
|
fn extract_and_run(
|
||||||
|
self,
|
||||||
|
receivers: Vec<Box<dyn std::any::Any + Send>>,
|
||||||
|
) -> impl Future<Output = Result<Self::Output, String>> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience function to create a task from a closure.
|
/// Convenience function to create a task from a closure.
|
||||||
@@ -104,7 +115,7 @@ pub fn task_fn<I, O, Fut, F>(f: F) -> impl Task<Input = I, Output = O>
|
|||||||
where
|
where
|
||||||
F: FnMut(I) -> Fut + Send + 'static,
|
F: FnMut(I) -> Fut + Send + 'static,
|
||||||
Fut: Future<Output = O> + Send + 'static,
|
Fut: Future<Output = O> + Send + 'static,
|
||||||
I: Send + 'static,
|
I: Send + crate::extract::ExtractInput + 'static,
|
||||||
O: Send + Sync + 'static,
|
O: Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
struct TaskFn<I, O, Fut, F>
|
struct TaskFn<I, O, Fut, F>
|
||||||
@@ -122,7 +133,7 @@ where
|
|||||||
where
|
where
|
||||||
F: FnMut(I) -> Fut + Send,
|
F: FnMut(I) -> Fut + Send,
|
||||||
Fut: Future<Output = O> + Send,
|
Fut: Future<Output = O> + Send,
|
||||||
I: Send,
|
I: Send + crate::extract::ExtractInput,
|
||||||
O: Send + Sync,
|
O: Send + Sync,
|
||||||
{
|
{
|
||||||
type Input = I;
|
type Input = I;
|
||||||
@@ -131,6 +142,18 @@ where
|
|||||||
async fn run(mut self, input: I) -> O {
|
async fn run(mut self, input: I) -> O {
|
||||||
(self.f)(input).await
|
(self.f)(input).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn extract_and_run(
|
||||||
|
self,
|
||||||
|
receivers: Vec<Box<dyn std::any::Any + Send>>,
|
||||||
|
) -> Result<Self::Output, String> {
|
||||||
|
// For task_fn, we use ExtractInput since we don't have macro-generated extraction
|
||||||
|
let input = I::extract_from_channels(receivers)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to extract input: {}", e))?;
|
||||||
|
|
||||||
|
Ok(self.run(input).await)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TaskFn {
|
TaskFn {
|
||||||
|
|||||||
@@ -83,6 +83,17 @@ impl Task for CustomTask {
|
|||||||
self.state
|
self.state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(refining_impl_trait)]
|
||||||
|
fn extract_and_run(
|
||||||
|
self,
|
||||||
|
_receivers: Vec<Box<dyn std::any::Any + Send>>,
|
||||||
|
) -> Pin<Box<dyn Future<Output = Result<Self::Output, String>> + Send>> {
|
||||||
|
Box::pin(async move {
|
||||||
|
// This is a test helper - just return an error
|
||||||
|
Err("extract_and_run not implemented for CustomTask (test helper)".to_string())
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
// Test that custom types work without any trait implementations
|
||||||
|
use dagx::{task, DagRunner, Task};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
struct Person {
|
||||||
|
name: String,
|
||||||
|
age: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
struct Company {
|
||||||
|
name: String,
|
||||||
|
employees: Vec<Person>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source task returning custom type
|
||||||
|
struct FetchPerson;
|
||||||
|
|
||||||
|
#[task]
|
||||||
|
impl FetchPerson {
|
||||||
|
async fn run(&self) -> Person {
|
||||||
|
Person {
|
||||||
|
name: "Alice".to_string(),
|
||||||
|
age: 30,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task consuming custom type
|
||||||
|
struct ProcessPerson;
|
||||||
|
|
||||||
|
#[task]
|
||||||
|
impl ProcessPerson {
|
||||||
|
async fn run(person: &Person) -> String {
|
||||||
|
format!("{} is {} years old", person.name, person.age)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task transforming custom type to another custom type
|
||||||
|
struct PersonToCompany;
|
||||||
|
|
||||||
|
#[task]
|
||||||
|
impl PersonToCompany {
|
||||||
|
async fn run(person: &Person) -> Company {
|
||||||
|
Company {
|
||||||
|
name: format!("{}'s Company", person.name),
|
||||||
|
employees: vec![person.clone()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_custom_type_single_dependency() {
|
||||||
|
let dag = DagRunner::new();
|
||||||
|
|
||||||
|
let fetch = dag.add_task(FetchPerson);
|
||||||
|
let process = dag.add_task(ProcessPerson).depends_on(&fetch);
|
||||||
|
|
||||||
|
dag.run(|fut| {
|
||||||
|
tokio::spawn(fut);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let result = dag.get(process).unwrap();
|
||||||
|
assert_eq!(result, "Alice is 30 years old");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_custom_type_passthrough() {
|
||||||
|
let dag = DagRunner::new();
|
||||||
|
|
||||||
|
let fetch = dag.add_task(FetchPerson);
|
||||||
|
let transform = dag.add_task(PersonToCompany).depends_on(&fetch);
|
||||||
|
let process = dag.add_task(ProcessPerson).depends_on(&fetch);
|
||||||
|
|
||||||
|
dag.run(|fut| {
|
||||||
|
tokio::spawn(fut);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let person_result = dag.get(process).unwrap();
|
||||||
|
let company_result = dag.get(transform).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(person_result, "Alice is 30 years old");
|
||||||
|
assert_eq!(company_result.name, "Alice's Company");
|
||||||
|
assert_eq!(company_result.employees.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_custom_type_diamond_pattern() {
|
||||||
|
let dag = DagRunner::new();
|
||||||
|
|
||||||
|
// Source
|
||||||
|
let fetch = dag.add_task(FetchPerson);
|
||||||
|
|
||||||
|
// Two paths from source
|
||||||
|
let path_a = dag.add_task(ProcessPerson).depends_on(&fetch);
|
||||||
|
let path_b = dag.add_task(PersonToCompany).depends_on(&fetch);
|
||||||
|
|
||||||
|
dag.run(|fut| {
|
||||||
|
tokio::spawn(fut);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let result_a = dag.get(path_a).unwrap();
|
||||||
|
let result_b = dag.get(path_b).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result_a, "Alice is 30 years old");
|
||||||
|
assert_eq!(result_b.name, "Alice's Company");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_custom_type_fan_out() {
|
||||||
|
let dag = DagRunner::new();
|
||||||
|
|
||||||
|
let fetch = dag.add_task(FetchPerson);
|
||||||
|
let process1 = dag.add_task(ProcessPerson).depends_on(&fetch);
|
||||||
|
let process2 = dag.add_task(ProcessPerson).depends_on(&fetch);
|
||||||
|
let process3 = dag.add_task(ProcessPerson).depends_on(&fetch);
|
||||||
|
|
||||||
|
dag.run(|fut| {
|
||||||
|
tokio::spawn(fut);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let result1 = dag.get(process1).unwrap();
|
||||||
|
let result2 = dag.get(process2).unwrap();
|
||||||
|
let result3 = dag.get(process3).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result1, "Alice is 30 years old");
|
||||||
|
assert_eq!(result2, "Alice is 30 years old");
|
||||||
|
assert_eq!(result3, "Alice is 30 years old");
|
||||||
|
}
|
||||||
Reference in new issue
Block a user