diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2bc01d..bfd5403 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,20 +126,20 @@ jobs: - name: Run all examples run: ./scripts/run_examples.sh - # coverage: - # name: Code Coverage - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v4 + coverage: + name: Code Coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 - # - name: Install Rust - # uses: dtolnay/rust-toolchain@stable + - name: Install Rust + uses: dtolnay/rust-toolchain@stable - # - name: Install cargo-tarpaulin - # - uses: taiki-e/install-action@cargo-tarpaulin + - name: Install cargo-tarpaulin + - uses: taiki-e/install-action@cargo-tarpaulin - # - name: Generate code coverage - # run: cargo tarpaulin --lib --all-features --out Xml --out Lcov --output-dir coverage --fail-under 80 + - name: Generate code coverage + run: cargo tarpaulin --out Xml --out Lcov --output-dir coverage --fail-under 80 security: name: Security Audit diff --git a/Cargo.lock b/Cargo.lock index 046eee9..fc6ce91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,27 +38,6 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" -[[package]] -name = "async-attributes" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "async-channel" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" -dependencies = [ - "concurrent-queue", - "event-listener 2.5.3", - "futures-core", -] - [[package]] name = "async-channel" version = "2.5.0" @@ -96,21 +75,6 @@ dependencies = [ "futures-lite", ] -[[package]] -name = "async-global-executor" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" -dependencies = [ - "async-channel 2.5.0", - "async-executor", - "async-io", - "async-lock", - "blocking", - "futures-lite", - "once_cell", -] - [[package]] name = "async-io" version = "2.6.0" @@ -135,7 +99,7 @@ version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" dependencies = [ - "event-listener 5.4.1", + "event-listener", "event-listener-strategy", "pin-project-lite", ] @@ -157,14 +121,14 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ - "async-channel 2.5.0", + "async-channel", "async-io", "async-lock", "async-signal", "async-task", "blocking", "cfg-if", - "event-listener 5.4.1", + "event-listener", "futures-lite", "rustix", ] @@ -187,33 +151,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "async-std" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" -dependencies = [ - "async-attributes", - "async-channel 1.9.0", - "async-global-executor", - "async-io", - "async-lock", - "crossbeam-utils", - "futures-channel", - "futures-core", - "futures-io", - "futures-lite", - "gloo-timers", - "kv-log-macro", - "log", - "memchr", - "once_cell", - "pin-project-lite", - "pin-utils", - "slab", - "wasm-bindgen-futures", -] - [[package]] name = "async-task" version = "4.7.1" @@ -228,7 +165,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -270,7 +207,7 @@ version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ - "async-channel 2.5.0", + "async-channel", "async-task", "futures-io", "futures-lite", @@ -437,16 +374,17 @@ name = "dagx" version = "0.3.1" dependencies = [ "async-executor", - "async-std", "async-trait", "criterion", "dagrs", "dagx-macros", + "dagx-test", "futures-executor", "futures-util", "parking_lot", "pollster", "smol", + "test-case", "tokio", "tracing", "tracing-subscriber", @@ -458,7 +396,14 @@ version = "0.3.1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", +] + +[[package]] +name = "dagx-test" +version = "0.3.1" +dependencies = [ + "dagx", ] [[package]] @@ -477,12 +422,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - [[package]] name = "event-listener" version = "5.4.1" @@ -500,7 +439,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener 5.4.1", + "event-listener", "pin-project-lite", ] @@ -580,7 +519,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -619,18 +558,6 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - [[package]] name = "half" version = "2.6.0" @@ -683,15 +610,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "kv-log-macro" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" -dependencies = [ - "log", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -724,9 +642,6 @@ name = "log" version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" -dependencies = [ - "value-bag", -] [[package]] name = "matchers" @@ -1061,7 +976,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -1113,7 +1028,7 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" dependencies = [ - "async-channel 2.5.0", + "async-channel", "async-executor", "async-fs", "async-io", @@ -1124,17 +1039,6 @@ dependencies = [ "futures-lite", ] -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.106" @@ -1146,6 +1050,39 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "test-case" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" +dependencies = [ + "test-case-macros", +] + +[[package]] +name = "test-case-core" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "test-case-macros" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "test-case-core", +] + [[package]] name = "thread_local" version = "1.1.9" @@ -1188,7 +1125,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -1210,7 +1147,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -1264,12 +1201,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "value-bag" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" - [[package]] name = "walkdir" version = "2.5.0" @@ -1309,23 +1240,10 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.106", + "syn", "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "wasm-bindgen-macro" version = "0.2.104" @@ -1344,7 +1262,7 @@ checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index 7fb6d41..c53da27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["dagx-macros"] +members = ["dagx-macros", "dagx-test"] [package] name = "dagx" @@ -21,12 +21,15 @@ exclude = [".claude/", ".jj/", "dagx-spec.md"] default = [] tracing = ["dep:tracing"] -[lints.rust] +[workspace.lints.rust] unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(tarpaulin)', 'cfg(tarpaulin_include)', ] } +[lints] +workspace = true + [dependencies] futures-util = { version = "0.3", default-features = false, features = ["std"] } parking_lot = "0.12" @@ -34,13 +37,13 @@ dagx-macros = { version = "0.3.1", path = "./dagx-macros" } tracing = { version = "0.1", optional = true } [dev-dependencies] +dagx-test = { path = "./dagx-test" } tokio = { version = "1", features = [ "macros", "rt-multi-thread", "time", "sync", ] } -async-std = { version = "1", features = ["attributes"] } smol = "2" criterion = "0.7" # Quirky async runtimes for edge-case testing @@ -52,6 +55,7 @@ dagrs = { version = "0.5", default-features = false } async-trait = "0.1" # Tracing support for tests and examples tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } +test-case = "3.3.1" [[bench]] name = "dagx_bench" diff --git a/README.md b/README.md index c7fe0d3..d902944 100644 --- a/README.md +++ b/README.md @@ -214,8 +214,6 @@ impl ProcessUser { **No trait implementations needed!** The `#[task]` macro generates type-specific extraction logic automatically. This includes nested structs, collections, enums, and any other type you define. -See [`custom_types.rs`](examples/custom_types.rs) for a complete example with nested custom types. - ### Runtime Agnostic dagx works with any async runtime. Provide a spawner function to `run()`: @@ -261,17 +259,8 @@ cargo run --example 04_parallel_computation Real-world patterns: - [`circuit_breaker.rs`](examples/circuit_breaker.rs) - Circuit breaker pattern for resilient systems -- [`complex_dag.rs`](examples/complex_dag.rs) - Multi-layer workflows with complex dependencies -- [`conditional_workflow.rs`](examples/conditional_workflow.rs) - Conditional execution with Result types -- [`custom_types.rs`](examples/custom_types.rs) - Using your own custom types (no trait implementations needed!) - [`data_pipeline.rs`](examples/data_pipeline.rs) - ETL data processing pipeline -- [`debug_tracing.rs`](examples/debug_tracing.rs) - Debug metadata and task naming - [`error_handling.rs`](examples/error_handling.rs) - Error propagation and recovery -- [`large_dag.rs`](examples/large_dag.rs) - Performance at 10,000+ nodes -- [`parallelism_proof.rs`](examples/parallelism_proof.rs) - Proof of true parallel execution -- [`retry_strategies.rs`](examples/retry_strategies.rs) - Retry patterns for transient failures -- [`timeout.rs`](examples/timeout.rs) - Managing task timeouts -- [`tracing_example.rs`](examples/tracing_example.rs) - Observability with tracing support Run any example: `cargo run --example custom_types` diff --git a/benches/basic/execution.rs b/benches/basic/execution.rs deleted file mode 100644 index c044bfc..0000000 --- a/benches/basic/execution.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! Basic DAG execution benchmarks - -use criterion::Criterion; -use dagx::{task_fn, DagRunner}; - -pub fn bench_dag_execution(c: &mut Criterion) { - let rt = tokio::runtime::Runtime::new().unwrap(); - - c.bench_function("execute_10_independent_tasks", |b| { - b.iter(|| { - rt.block_on(async { - let dag = DagRunner::new(); - for i in 0..10 { - dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); - } - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - }) - }); - }); - - c.bench_function("execute_100_independent_tasks", |b| { - b.iter(|| { - rt.block_on(async { - let dag = DagRunner::new(); - for i in 0..100 { - dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); - } - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - }) - }); - }); - - c.bench_function("execute_deep_chain_20", |b| { - b.iter(|| { - rt.block_on(async { - let dag = DagRunner::new(); - - // Build a manual chain (API doesn't support loop-based chaining due to type system) - struct Value(i32); - #[dagx::task] - impl Value { - async fn run(&self) -> i32 { - self.0 - } - } - - let t0 = dag.add_task(Value(0)); - let t1 = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(t0); - let t2 = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(t1); - let t3 = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(t2); - let t4 = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(t3); - let t5 = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(t4); - let t6 = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(t5); - let t7 = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(t6); - let t8 = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(t7); - let t9 = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(t8); - let t10 = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(t9); - let t11 = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(t10); - let t12 = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(t11); - let t13 = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(t12); - let t14 = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(t13); - let t15 = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(t14); - let t16 = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(t15); - let t17 = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(t16); - let t18 = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(t17); - let _t19 = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(t18); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - }) - }); - }); -} diff --git a/benches/basic/mod.rs b/benches/basic/mod.rs index a2cd062..41931c4 100644 --- a/benches/basic/mod.rs +++ b/benches/basic/mod.rs @@ -1,5 +1,4 @@ //! Basic DAG benchmarks - creation, execution, scaling pub mod creation; -pub mod execution; pub mod scaling; diff --git a/benches/basic/scaling.rs b/benches/basic/scaling.rs index c4d6a17..1be3a1c 100644 --- a/benches/basic/scaling.rs +++ b/benches/basic/scaling.rs @@ -1,7 +1,8 @@ //! DAG scaling benchmarks - how performance scales with task count use criterion::{BenchmarkId, Criterion}; -use dagx::{task_fn, DagRunner}; +use dagx::DagRunner; +use dagx_test::task_fn; pub fn bench_dag_scaling(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); diff --git a/benches/comparisons/deep_chain.rs b/benches/comparisons/deep_chain.rs index c93084b..932601d 100644 --- a/benches/comparisons/deep_chain.rs +++ b/benches/comparisons/deep_chain.rs @@ -13,7 +13,8 @@ pub fn bench_deep_chain(c: &mut Criterion) { group.bench_function("dagx", |b| { b.iter(|| { rt.block_on(async { - use dagx::{task_fn, DagRunner}; + use dagx::DagRunner; + use dagx_test::task_fn; let dag = DagRunner::new(); diff --git a/benches/comparisons/diamond_pattern.rs b/benches/comparisons/diamond_pattern.rs index c0f15bd..5aae415 100644 --- a/benches/comparisons/diamond_pattern.rs +++ b/benches/comparisons/diamond_pattern.rs @@ -13,7 +13,8 @@ pub fn bench_diamond_pattern(c: &mut Criterion) { group.bench_function("dagx", |b| { b.iter(|| { rt.block_on(async { - use dagx::{task_fn, DagRunner}; + use dagx::DagRunner; + use dagx_test::task_fn; let dag = DagRunner::new(); diff --git a/benches/comparisons/etl_pipeline.rs b/benches/comparisons/etl_pipeline.rs index ef9f70c..1363546 100644 --- a/benches/comparisons/etl_pipeline.rs +++ b/benches/comparisons/etl_pipeline.rs @@ -12,7 +12,8 @@ pub fn bench_etl_pipeline(c: &mut Criterion) { group.bench_function("dagx", |b| { b.iter(|| { rt.block_on(async { - use dagx::{task_fn, DagRunner}; + use dagx::DagRunner; + use dagx_test::task_fn; let dag = DagRunner::new(); diff --git a/benches/comparisons/large_scale.rs b/benches/comparisons/large_scale.rs index 53c2bb4..b23a562 100644 --- a/benches/comparisons/large_scale.rs +++ b/benches/comparisons/large_scale.rs @@ -14,7 +14,8 @@ pub fn bench_large_scale(c: &mut Criterion) { group.bench_function("dagx", |b| { b.iter(|| { rt.block_on(async { - use dagx::{task_fn, DagRunner}; + use dagx::DagRunner; + use dagx_test::task_fn; let dag = DagRunner::new(); for i in 0..10_000 { diff --git a/benches/comparisons/linear_pipeline.rs b/benches/comparisons/linear_pipeline.rs index 63d243c..6e787c0 100644 --- a/benches/comparisons/linear_pipeline.rs +++ b/benches/comparisons/linear_pipeline.rs @@ -12,7 +12,8 @@ pub fn bench_linear_pipeline(c: &mut Criterion) { group.bench_function("dagx", |b| { b.iter(|| { rt.block_on(async { - use dagx::{task_fn, DagRunner}; + use dagx::DagRunner; + use dagx_test::task_fn; let dag = DagRunner::new(); diff --git a/benches/comparisons/wide_fanout.rs b/benches/comparisons/wide_fanout.rs index 960956d..fa5149c 100644 --- a/benches/comparisons/wide_fanout.rs +++ b/benches/comparisons/wide_fanout.rs @@ -13,7 +13,8 @@ pub fn bench_wide_fanout(c: &mut Criterion) { group.bench_function("dagx", |b| { b.iter(|| { rt.block_on(async { - use dagx::{task_fn, DagRunner}; + use dagx::DagRunner; + use dagx_test::task_fn; let dag = DagRunner::new(); let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into(); diff --git a/benches/dagx_bench.rs b/benches/dagx_bench.rs index c9d6a32..4c94817 100644 --- a/benches/dagx_bench.rs +++ b/benches/dagx_bench.rs @@ -28,12 +28,6 @@ criterion_group! { targets = basic::creation::bench_dag_creation } -criterion_group! { - name = execution_benches; - config = configure_criterion(); - targets = basic::execution::bench_dag_execution -} - criterion_group! { name = scaling_benches; config = configure_criterion(); @@ -54,7 +48,6 @@ criterion_group! { config = configure_criterion(); targets = realistic::etl::bench_etl_pipeline, - realistic::config_broadcast::bench_config_broadcast, realistic::memory_efficiency::bench_memory_efficiency, realistic::mixed::bench_mixed_patterns } @@ -73,7 +66,6 @@ criterion_group! { criterion_main!( creation_benches, - execution_benches, scaling_benches, pattern_benches, realistic_benches, diff --git a/benches/patterns/breakdown.rs b/benches/patterns/breakdown.rs index e58d77e..196d857 100644 --- a/benches/patterns/breakdown.rs +++ b/benches/patterns/breakdown.rs @@ -1,7 +1,8 @@ //! 10,000 task breakdown benchmarks - construction vs execution -use criterion::Criterion; -use dagx::{task_fn, DagRunner}; +use criterion::{BatchSize, Criterion}; +use dagx::DagRunner; +use dagx_test::task_fn; pub fn bench_10k_breakdown(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); @@ -35,19 +36,23 @@ pub fn bench_10k_breakdown(c: &mut Criterion) { // c) Execution only (pre-built DAG, just run) group.bench_function("execution_only", |b| { - // Pre-build the DAG outside the measurement - let dag = DagRunner::new(); - for i in 0..10_000 { - dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); - } - - b.iter(|| { - rt.block_on(async { - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - }) - }); + b.iter_batched( + || { + let dag = DagRunner::new(); + for i in 0..10_000 { + dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); + } + dag + }, + |dag| { + rt.block_on(async { + dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + .await + .unwrap(); + }) + }, + BatchSize::LargeInput, + ); }); group.finish(); diff --git a/benches/patterns/diamond.rs b/benches/patterns/diamond.rs index 5806bb9..75a2263 100644 --- a/benches/patterns/diamond.rs +++ b/benches/patterns/diamond.rs @@ -1,8 +1,8 @@ //! Diamond pattern benchmarks (fan-out then fan-in) use criterion::Criterion; -use dagx::{task_fn, DagRunner, TaskHandle}; - +use dagx::{DagRunner, TaskHandle}; +use dagx_test::task_fn; pub fn bench_diamond(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); diff --git a/benches/patterns/fanout.rs b/benches/patterns/fanout.rs index 9e29a85..0b49035 100644 --- a/benches/patterns/fanout.rs +++ b/benches/patterns/fanout.rs @@ -1,8 +1,8 @@ //! Fan-out pattern benchmarks (1 → N dependencies) use criterion::Criterion; -use dagx::{task_fn, DagRunner, TaskHandle}; - +use dagx::{DagRunner, TaskHandle}; +use dagx_test::task_fn; pub fn bench_fanout(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); @@ -51,30 +51,4 @@ pub fn bench_fanout(c: &mut Criterion) { }) }); }); - - // Arc overhead benchmark - shows cost on Copy types - // For Copy types like usize, Arc adds overhead (atomic refcounting + indirection) - // For heap types like Vec, Arc overhead is negligible compared to clone cost - c.bench_function("arc_overhead_copy_type", |b| { - b.iter(|| { - rt.block_on(async { - let dag = DagRunner::new(); - - // Source produces a simple Copy type (usize) - // Framework wraps in Arc internally - let source: TaskHandle<_> = - dag.add_task(task_fn::<(), _, _>(|_: ()| 42usize)).into(); - - // 100 dependents - Arc overhead visible on Copy types - for i in 0..100 { - dag.add_task(task_fn::(move |x: &usize| x + i)) - .depends_on(source); - } - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - }) - }); - }); } diff --git a/benches/realistic/config_broadcast.rs b/benches/realistic/config_broadcast.rs deleted file mode 100644 index c011b7f..0000000 --- a/benches/realistic/config_broadcast.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! Configuration broadcast pattern benchmark - one config shared across many workers - -use criterion::{BenchmarkId, Criterion}; -use dagx::{task_fn, DagRunner, TaskHandle}; - -use std::collections::HashMap; -use std::hint::black_box; - -pub fn bench_config_broadcast(c: &mut Criterion) { - let rt = tokio::runtime::Runtime::new().unwrap(); - let mut group = c.benchmark_group("config_broadcast"); - - for worker_count in [10, 50, 100, 200].iter() { - group.bench_with_input( - BenchmarkId::new("config", worker_count), - worker_count, - |b, &workers| { - b.iter(|| { - rt.block_on(async { - let dag = DagRunner::new(); - - // Configuration source - framework wraps HashMap in Arc automatically - let config: TaskHandle<_> = dag - .add_task(task_fn::<(), _, _>(|_: ()| { - HashMap::from([ - ("max_connections".to_string(), "100".to_string()), - ("timeout_ms".to_string(), "5000".to_string()), - ("batch_size".to_string(), "50".to_string()), - ("retry_count".to_string(), "3".to_string()), - ("worker_threads".to_string(), "4".to_string()), - ("buffer_size".to_string(), "8192".to_string()), - ("compression".to_string(), "true".to_string()), - ("log_level".to_string(), "info".to_string()), - ]) - })) - .into(); - - // Workers receive and process based on shared config - for worker_id in 0..workers { - dag.add_task(task_fn::, _, _>( - move |cfg: &HashMap| { - // Simulate work based on configuration - let batch_size: usize = cfg - .get("batch_size") - .and_then(|s| s.parse().ok()) - .unwrap_or(10); - let timeout: usize = cfg - .get("timeout_ms") - .and_then(|s| s.parse().ok()) - .unwrap_or(1000); - - // Simulate some computation - black_box(worker_id * batch_size + timeout / 100) - }, - )) - .depends_on(config); - } - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - }) - }); - }, - ); - } - - group.finish(); -} diff --git a/benches/realistic/etl.rs b/benches/realistic/etl.rs index 06c0e80..2be6428 100644 --- a/benches/realistic/etl.rs +++ b/benches/realistic/etl.rs @@ -1,8 +1,8 @@ //! ETL (Extract, Transform, Load) pipeline benchmark use criterion::{BenchmarkId, Criterion}; -use dagx::{task_fn, DagRunner, TaskHandle}; - +use dagx::{DagRunner, TaskHandle}; +use dagx_test::task_fn; pub fn bench_etl_pipeline(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); let mut group = c.benchmark_group("etl_scaling"); diff --git a/benches/realistic/memory_efficiency.rs b/benches/realistic/memory_efficiency.rs index 64b5e69..61ae2c8 100644 --- a/benches/realistic/memory_efficiency.rs +++ b/benches/realistic/memory_efficiency.rs @@ -1,8 +1,8 @@ //! Memory efficiency benchmark with large data structures use criterion::{BenchmarkId, Criterion}; -use dagx::{task_fn, DagRunner, TaskHandle}; - +use dagx::{DagRunner, TaskHandle}; +use dagx_test::task_fn; use std::hint::black_box; pub fn bench_memory_efficiency(c: &mut Criterion) { diff --git a/benches/realistic/mixed.rs b/benches/realistic/mixed.rs index 9b706d1..5f34d8d 100644 --- a/benches/realistic/mixed.rs +++ b/benches/realistic/mixed.rs @@ -1,8 +1,8 @@ //! Mixed realistic patterns - fan-out, fan-in, and processing use criterion::Criterion; -use dagx::{task_fn, DagRunner, TaskHandle}; - +use dagx::{DagRunner, TaskHandle}; +use dagx_test::task_fn; pub fn bench_mixed_patterns(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); diff --git a/benches/realistic/mod.rs b/benches/realistic/mod.rs index e1230f4..d7ca64d 100644 --- a/benches/realistic/mod.rs +++ b/benches/realistic/mod.rs @@ -1,6 +1,5 @@ //! Realistic workload benchmarks -pub mod config_broadcast; pub mod etl; pub mod memory_efficiency; pub mod mixed; diff --git a/dagx-macros/src/lib.rs b/dagx-macros/src/lib.rs index 891e03b..e4a3d26 100644 --- a/dagx-macros/src/lib.rs +++ b/dagx-macros/src/lib.rs @@ -98,7 +98,7 @@ use syn::{parse_macro_input, FnArg, Ident, ImplItem, ItemImpl, Pat, PatType, Ret /// self.value /// } /// } -/// ```ignore +/// ``` /// /// ## Single Input /// @@ -113,7 +113,7 @@ use syn::{parse_macro_input, FnArg, Ident, ImplItem, ItemImpl, Pat, PatType, Ret /// input * 2 /// } /// } -/// ```ignore +/// ``` /// /// ## Multiple Inputs (up to 8) /// @@ -128,7 +128,7 @@ use syn::{parse_macro_input, FnArg, Ident, ImplItem, ItemImpl, Pat, PatType, Ret /// format!("{}: {} ({})", b, a, c) /// } /// } -/// ```ignore +/// ``` /// /// # Requirements /// @@ -168,7 +168,7 @@ use syn::{parse_macro_input, FnArg, Ident, ImplItem, ItemImpl, Pat, PatType, Ret /// a + b /// } /// } -/// ```ignore +/// ``` /// /// For stateful tasks (with &mut self): /// @@ -199,7 +199,7 @@ use syn::{parse_macro_input, FnArg, Ident, ImplItem, ItemImpl, Pat, PatType, Ret /// self.count /// } /// } -/// ```ignore +/// ``` #[proc_macro_attribute] pub fn task(_attr: TokenStream, item: TokenStream) -> TokenStream { let impl_block = parse_macro_input!(item as ItemImpl); diff --git a/dagx-test/Cargo.toml b/dagx-test/Cargo.toml new file mode 100644 index 0000000..3b4ec4a --- /dev/null +++ b/dagx-test/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "dagx-test" +version = "0.3.1" +edition = "2021" +rust-version = "1.81" +authors = ["TechnoPorg"] +license = "MIT" +description = "Utilities for dagx's internal tests" +repository = "https://github.com/swaits/dagx" +homepage = "https://github.com/swaits/dagx" +documentation = "https://docs.rs/dagx-test" + +[lints] +workspace = true + +[dependencies] +dagx = { version = "0.3.1", path = "../" } diff --git a/src/extract.rs b/dagx-test/src/extract.rs similarity index 64% rename from src/extract.rs rename to dagx-test/src/extract.rs index f11c262..d3c0dcf 100644 --- a/src/extract.rs +++ b/dagx-test/src/extract.rs @@ -1,46 +1,10 @@ -//! Input extraction for task dependencies via a list of dependencies. -//! -//! # Legacy Trait for Internal Use Only -//! -//! **NOTE**: This module is now primarily for internal use. The `ExtractInput` trait is only -//! used by the internal `task_fn` test helper function. -//! -//! **For regular tasks**: The `#[task]` macro generates inline `run()` methods with -//! type-specific extraction logic. This means **ANY type** implementing `Send + Sync` -//! works automatically without needing `ExtractInput` implementations! -//! -//! # How it works -//! -//! When a task executes, it needs to extract its inputs from type-erased oneshot dependencies. -//! Outputs are wrapped in Arc for efficient fanout (cheap Arc clones instead of data clones). -//! -//! **Modern approach (via `#[task]` macro)**: -//! - The macro generates custom extraction logic inline in `run()` -//! - Works with ANY type (just needs `Send + Sync + 'static`) -//! - No trait implementations needed! -//! -//! **Legacy approach (via `ExtractInput` trait)**: -//! - Used only by the internal `task_fn` test helper -//! - Provides implementations for primitives, standard types, and tuples -//! - Requires explicit trait implementations for custom types (not recommended) -//! -//! # Why macros? -//! -//! Rust lacks variadic generics, so we use macros to generate implementations for: -//! - Common primitive/standard types (one impl per type) -//! - Tuples of different sizes (one impl per size, up to 8 elements) -//! -//! This is standard practice in Rust for working with tuples of different arities. +//! Input extraction for tasks from a list of dependencies. use std::{collections::HashMap, hash::Hash, sync::Arc}; use crate::TaskInput; /// Helper trait for input extraction from list of dependencies. -/// -/// 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. -#[allow(dead_code)] pub trait ExtractInput: Sized + Clone { type Input: Send; type Retv<'input>; @@ -158,18 +122,6 @@ where } } -/// Macro to implement ExtractInput for tuple types. -/// -/// We need separate implementations for each tuple size (2-8) because Rust doesn't support -/// variadic generics. This is standard practice when working with tuples in Rust. -/// -/// # Compile-time Safety -/// -/// The number of dependencies is guaranteed to match the tuple size at compile time through: -/// 1. The `DepsTuple` trait bound on `depends_on()` ensures type/arity matching -/// 2. The `IsUnitType` constraint on `From` prevents using tasks without calling `depends_on()` -/// -/// Therefore, no runtime validation is needed - if it compiles, the dependency count is correct. macro_rules! impl_extract_tuple { ($($T:ident),+) => { impl<$($T: 'static + Clone + Send + Sync),+> ExtractInput for ($($T,)+) { diff --git a/dagx-test/src/lib.rs b/dagx-test/src/lib.rs new file mode 100644 index 0000000..226b891 --- /dev/null +++ b/dagx-test/src/lib.rs @@ -0,0 +1,50 @@ +//! DAG builder helpers for internal use in dagx tests and benchmarks. +//! +//! This crate is not meant for public use and offers no stability guarantees. +//! The ergonomics are also such that you don't want to use it anyways. **Use the `#[task]` macro instead.** + +#![cfg(not(tarpaulin_include))] + +use crate::extract::ExtractInput; + +use dagx::{Task, TaskInput}; + +mod extract; + +pub struct TaskFn +where + for<'input> F: FnMut(I::Retv<'input>) -> O + Send, + I: ExtractInput + Send + Sync + 'static, + O: Send, +{ + f: F, + _phantom: std::marker::PhantomData O>, +} + +impl Task for TaskFn +where + for<'input> F: FnMut(I::Retv<'input>) -> O + Send, + I: ExtractInput + Send + Sync + 'static, + O: Send + Sync, +{ + type Input = I::Input; + type Output = O; + + async fn run(mut self, input: TaskInput<'_, I::Input>) -> O { + (self.f)(I::extract_from_task_input(input).unwrap()) + } +} + +/// Convenience function to create a task from a closure. +pub fn task_fn(f: F) -> TaskFn +where + for<'input> F: FnMut(I::Retv<'input>) -> O + Send, + I: ExtractInput + Send + Sync + 'static, + I::Input: Send + Sync + 'static, + O: Send + Sync + 'static, +{ + TaskFn { + f, + _phantom: std::marker::PhantomData, + } +} diff --git a/docs/TRACING.md b/docs/TRACING.md index c47edf9..e419b42 100644 --- a/docs/TRACING.md +++ b/docs/TRACING.md @@ -41,8 +41,6 @@ RUST_LOG=dagx=debug cargo run # Task and layer details RUST_LOG=dagx=trace cargo run # All execution details ``` -See [`examples/tracing_example.rs`](../examples/tracing_example.rs) for a complete working example. - ## Zero-Cost Guarantee When the `tracing` feature is disabled (the default), there is **literally 0ns overhead**: diff --git a/examples/complex_dag.rs b/examples/complex_dag.rs deleted file mode 100644 index 4ac8aad..0000000 --- a/examples/complex_dag.rs +++ /dev/null @@ -1,215 +0,0 @@ -//! # Building Complex Workflows: Multi-Layer DAG -//! -//! This example demonstrates how to build complex, real-world DAGs by combining -//! multiple dependency patterns into a cohesive workflow. -//! -//! ## What You'll Learn -//! - How to combine fan-out, fan-in, and diamond patterns in one DAG -//! - How to build multi-layer workflows with intermediate results -//! - How to structure an ETL (Extract-Transform-Load) pipeline -//! - How to pass results through multiple transformation stages -//! -//! ## Workflow Diagram -//! ```text -//! EXTRACT TRANSFORM COMBINE AGGREGATE -//! ┌──────────┐ ┌──────────┐ -//! │ LoadData │────►│Transform │────┐ -//! │ A │ │ (×2) │ │ -//! └──────────┘ └──────────┘ │ -//! ├──►┌─────────┐───►┌─────────┐───►┌─────────┐ -//! │ │ Combine │ │ Average │ │ Report │ -//! └──►└─────────┘ └─────────┘ └─────────┘ -//! ┌──────────┐ ┌──────────┐ ┌──► │ │ -//! │ LoadData │────►│Transform │────┘ │ │ -//! │ B │ │ (×3) │ └──────────────┘ -//! └──────────┘ └──────────┘ -//! ``` -//! -//! ## Key Concepts -//! - **Multi-layer execution**: Tasks organized into logical stages (Extract → Transform → Combine) -//! - **Many-to-many dependencies**: The Combine task depends on both Transform tasks -//! - **Result reuse**: The combined result is used by both Average and Report tasks -//! - **Diamond pattern**: Multiple paths from sources converge and then diverge again -//! -//! ## Use Cases -//! - ETL pipelines with multiple data sources -//! - Multi-stage data processing workflows -//! - Complex business logic with branching and merging -//! - Data aggregation and reporting systems -//! -//! ## Running This Example -//! ```bash -//! cargo run --example complex_dag -//! ``` -//! -//! ## Expected Output -//! ```text -//! Building ETL pipeline... -//! -//! Executing pipeline... -//! -//! Loading data from source A -//! Loading data from source B -//! Transforming: 6 * 2 = 12 -//! Transforming: 15 * 3 = 45 -//! Combining datasets: 12 + 45 = 57 -//! Computing average: 57 / 6 = 9.50 -//! -//! ETL Pipeline Results: -//! Items: 6 -//! Total: 57 -//! Average: 9.50 -//! -//! Calculation: -//! Source A: sum=6, transformed (x2) = 12 -//! Source B: sum=15, transformed (x3) = 45 -//! Combined: 6 items, sum=57, avg=9.50 -//! ``` - -use dagx::{task, DagResult, DagRunner}; - -// Extract: Load data from different sources -struct LoadData { - source: &'static str, -} - -impl LoadData { - fn new(source: &'static str) -> Self { - Self { source } - } -} - -#[task] -impl LoadData { - async fn run(&mut self) -> i32 { - // Simulate loading and summing data - println!("Loading data from source {}", self.source); - match self.source { - "A" => 6, // sum of [1, 2, 3] - "B" => 15, // sum of [4, 5, 6] - _ => 0, - } - } -} - -// Transform: Process data with a multiplier -struct Transform { - multiplier: i32, -} - -impl Transform { - fn new(multiplier: i32) -> Self { - Self { multiplier } - } -} - -#[task] -impl Transform { - async fn run(&mut self, sum: &i32) -> i32 { - let result = sum * self.multiplier; - println!("Transforming: {} * {} = {}", sum, self.multiplier, result); - result - } -} - -// Combine: Merge two datasets -struct Combine; - -#[task] -impl Combine { - async fn run(sum_a: &i32, sum_b: &i32) -> i32 { - println!( - "Combining datasets: {} + {} = {}", - sum_a, - sum_b, - sum_a + sum_b - ); - sum_a + sum_b - } -} - -// Compute average (using a fixed count for simplicity) -struct ComputeAverage { - total_count: i32, -} - -impl ComputeAverage { - fn new(total_count: i32) -> Self { - Self { total_count } - } -} - -#[task] -impl ComputeAverage { - async fn run(&mut self, sum: &i32) -> f64 { - let avg = *sum as f64 / self.total_count as f64; - println!( - "Computing average: {} / {} = {:.2}", - sum, self.total_count, avg - ); - avg - } -} - -// Format report -struct FormatReport { - count: i32, -} - -impl FormatReport { - fn new(count: i32) -> Self { - Self { count } - } -} - -#[task] -impl FormatReport { - async fn run(&mut self, sum: &i32, avg: &f64) -> String { - format!( - "ETL Pipeline Results:\n Items: {}\n Total: {}\n Average: {:.2}", - self.count, sum, avg - ) - } -} - -#[tokio::main] -async fn main() -> DagResult<()> { - let dag = DagRunner::new(); - - println!("Building ETL pipeline...\n"); - - // Extract: Load from two sources (parallel) - let source_a = dag.add_task(LoadData::new("A")); - let source_b = dag.add_task(LoadData::new("B")); - - // Transform: Process each source (parallel) - let transform_a = dag.add_task(Transform::new(2)).depends_on(source_a); - let transform_b = dag.add_task(Transform::new(3)).depends_on(source_b); - - // Combine: Merge the transformed datasets - let combined = dag - .add_task(Combine) - .depends_on((&transform_a, &transform_b)); - - // Compute average on combined data (we know we have 6 items total) - let avg = dag.add_task(ComputeAverage::new(6)).depends_on(combined); - - // Format final report (needs both sum and average) - let report = dag - .add_task(FormatReport::new(6)) - .depends_on((&combined, &avg)); - - // Execute the entire ETL pipeline - println!("Executing pipeline...\n"); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Display final report - println!("\n{}", dag.get(report)?); - println!("\nCalculation:"); - println!(" Source A: sum=6, transformed (x2) = 12"); - println!(" Source B: sum=15, transformed (x3) = 45"); - println!(" Combined: 6 items, sum=57, avg=9.50"); - - Ok(()) -} diff --git a/examples/conditional_workflow.rs b/examples/conditional_workflow.rs deleted file mode 100644 index 74b98ea..0000000 --- a/examples/conditional_workflow.rs +++ /dev/null @@ -1,148 +0,0 @@ -//! # Pattern: Conditional Execution with Data Validation -//! -//! This example demonstrates how to implement conditional logic in DAGs by using -//! simple data transformations rather than traditional Result types. -//! -//! ## What You'll Learn -//! - How to implement validation logic in parallel tasks -//! - How to use return values for conditional behavior (e.g., 0 for invalid) -//! - How to categorize and branch based on computed values -//! - How to aggregate results from multiple conditional branches -//! -//! ## Workflow Diagram -//! ```text -//! VALIDATE CATEGORIZE SUMMARIZE -//! ┌─────────────┐ ┌──────────────┐ -//! │ Validate(10)│────20──►│Categorize(25)│────"low"────┐ -//! └─────────────┘ └──────────────┘ │ -//! │ -//! ┌─────────────┐ ┌──────────────┐ │ -//! │ Validate(-5)│────0───►│Categorize(25)│──"invalid"──┼──►┌────────────┐ -//! └─────────────┘ └──────────────┘ │ │ Summarize │ -//! │ └────────────┘ -//! ┌─────────────┐ ┌──────────────┐ │ -//! │ Validate(20)│────40──►│Categorize(25)│────"high"───┘ -//! └─────────────┘ └──────────────┘ -//! ``` -//! -//! ## Key Concepts -//! - **Validation as transformation**: Invalid inputs become sentinel values (0) -//! - **Parallel validation**: All inputs validated simultaneously -//! - **Data-driven branching**: Categorization based on computed values -//! - **Type-safe aggregation**: All branches produce the same type (String) -//! -//! ## Use Cases -//! - Input validation pipelines -//! - Multi-stage data cleaning -//! - Classification and categorization workflows -//! - Quality control systems -//! - Data preprocessing for ML pipelines -//! -//! ## Running This Example -//! ```bash -//! cargo run --example conditional_workflow -//! ``` -//! -//! ## Expected Output -//! ```text -//! Processing and categorizing inputs in parallel... -//! -//! Valid input 10: doubling to 20 -//! Invalid input -5: returning 0 -//! Valid input 20: doubling to 40 -//! -//! Final summary: Categories: [low, invalid, high] -//! Expected: Categories: [low, invalid, high] -//! ``` - -use dagx::{task, DagResult, DagRunner}; - -// Task that validates and processes input -struct ValidateAndDouble { - value: i32, -} - -impl ValidateAndDouble { - fn new(value: i32) -> Self { - Self { value } - } -} - -#[task] -impl ValidateAndDouble { - async fn run(&mut self) -> i32 { - if self.value > 0 { - println!("Valid input {}: doubling to {}", self.value, self.value * 2); - self.value * 2 - } else { - println!("Invalid input {}: returning 0", self.value); - 0 // Return 0 for invalid inputs - } - } -} - -// Task that categorizes a number -struct Categorize { - threshold: i32, -} - -impl Categorize { - fn new(threshold: i32) -> Self { - Self { threshold } - } -} - -#[task] -impl Categorize { - async fn run(&mut self, value: &i32) -> String { - if *value == 0 { - "invalid".to_string() - } else if *value >= self.threshold { - "high".to_string() - } else { - "low".to_string() - } - } -} - -// Task that combines categorization results -struct SummarizeResults; - -#[task] -impl SummarizeResults { - async fn run(cat1: &String, cat2: &String, cat3: &String) -> String { - format!("Categories: [{}, {}, {}]", cat1, cat2, cat3) - } -} - -#[tokio::main] -async fn main() -> DagResult<()> { - let dag = DagRunner::new(); - - println!("Processing and categorizing inputs in parallel...\n"); - - // Validate and process multiple inputs in parallel - let processed1 = dag.add_task(ValidateAndDouble::new(10)); // Valid: 20 - let processed2 = dag.add_task(ValidateAndDouble::new(-5)); // Invalid: 0 - let processed3 = dag.add_task(ValidateAndDouble::new(20)); // Valid: 40 - - // Categorize each result (also parallel, threshold = 25) - let cat1 = dag.add_task(Categorize::new(25)).depends_on(processed1); // 20 < 25 = "low" - let cat2 = dag.add_task(Categorize::new(25)).depends_on(processed2); // 0 = "invalid" - let cat3 = dag.add_task(Categorize::new(25)).depends_on(processed3); // 40 >= 25 = "high" - - // Combine results - let summary = dag - .add_task(SummarizeResults) - .depends_on((&cat1, &cat2, &cat3)); - - // Execute - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Display results - println!("\nFinal summary: {}", dag.get(summary)?); - println!("Expected: Categories: [low, invalid, high]"); - - Ok(()) -} diff --git a/examples/custom_types.rs b/examples/custom_types.rs deleted file mode 100644 index 4c3d8ed..0000000 --- a/examples/custom_types.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! # Custom Types: Using Your Own Structs -//! -//! This example demonstrates that **ANY custom type** works in dagx as long as it implements -//! ` Send + Sync + 'static`. You don't need to implement any special traits! -//! -//! ## What You'll Learn -//! - How to use custom structs as task outputs -//! - How custom types flow through the DAG automatically -//! - How one task's output can be consumed by multiple downstream tasks -//! -//! ## Key Insight -//! -//! **dagx works with ANY type automatically!** The `#[task]` macro generates type-specific -//! extraction logic, so you don't need to implement any special traits. Just make sure your -//! type implements: -//! - `Send + Sync` (required for async tasks) -//! - `'static` (no lifetime constraints) -//! -//! That's it! No ExtractInput, no special annotations, no trait boilerplate. -//! -//! ## Running This Example -//! ```bash -//! cargo run --example custom_types -//! ``` -//! -//! ## Expected Output -//! ```text -//! Creating custom types example... -//! -//! Creating user: Alice (age: 30) -//! Processing user Alice for greeting -//! Processing user Alice for validation -//! Greeting: Hello, Alice! -//! Validation: Alice is valid (age 30 >= 18) -//! ``` - -use dagx::{task, DagRunner, TaskHandle}; - -#[derive(Debug)] -struct User { - name: String, - age: u32, -} - -// Task that produces a custom type -struct CreateUser { - name: String, - age: u32, -} - -#[task] -impl CreateUser { - async fn run(&self) -> User { - println!("Creating user: {} (age: {})", self.name, self.age); - User { - name: self.name.clone(), - age: self.age, - } - } -} - -// Task that consumes the custom type and produces a String -struct GreetUser; - -#[task] -impl GreetUser { - async fn run(user: &User) -> String { - println!("Processing user {} for greeting", user.name); - format!("Hello, {}!", user.name) - } -} - -// Another task that also consumes the same custom type -// This demonstrates fan-out: one output consumed by multiple tasks -struct ValidateUser; - -#[task] -impl ValidateUser { - async fn run(user: &User) -> String { - println!("Processing user {} for validation", user.name); - if user.age >= 18 { - format!("{} is valid (age {} >= 18)", user.name, user.age) - } else { - format!("{} is too young (age {} < 18)", user.name, user.age) - } - } -} - -#[tokio::main] -async fn main() { - println!("Creating custom types example...\n"); - - let dag = DagRunner::new(); - - // Create a task that produces a custom User type - let user_task: TaskHandle<_> = dag - .add_task(CreateUser { - name: "Alice".to_string(), - age: 30, - }) - .into(); - - // Two different tasks consume the same custom User type - // dagx automatically handles Arc wrapping, so both tasks get &User - let greeting = dag.add_task(GreetUser).depends_on(user_task); - let validation = dag.add_task(ValidateUser).depends_on(user_task); - - // Execute the DAG - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // Print results - println!("Greeting: {}", dag.get(greeting).unwrap()); - println!("Validation: {}", dag.get(validation).unwrap()); -} diff --git a/examples/debug_tracing.rs b/examples/debug_tracing.rs deleted file mode 100644 index 9dc2d2c..0000000 --- a/examples/debug_tracing.rs +++ /dev/null @@ -1,461 +0,0 @@ -//! # Debug Tracing and Task Naming -//! -//! This example demonstrates how to add debug metadata and tracing information to tasks -//! for better observability in complex DAG workflows. -//! -//! ## What You'll Learn -//! - How to add task names and IDs for debugging -//! - How to implement correlation IDs for distributed tracing -//! - How to track task execution timing and lifecycle events -//! - How to build traceable task wrappers -//! -//! ## Debugging Patterns -//! -//! **1. Named Tasks** - Add human-readable names to tasks -//! ```rust -//! struct TaskMetadata { -//! id: String, -//! name: String, -//! start_time: Option, -//! } -//! ``` -//! -//! **2. Correlation IDs** - Track requests across task boundaries -//! ```rust -//! struct TraceContext { -//! correlation_id: String, -//! parent_task: Option, -//! } -//! ``` -//! -//! **3. Execution Metrics** - Measure task performance -//! ```rust -//! struct TaskTrace { -//! name: String, -//! duration_ms: u64, -//! status: &'static str, -//! } -//! ``` -//! -//! ## Workflow Diagram -//! ```text -//! TRACE: order-12345 -//! ┌──────────────────┐ -//! │ ValidateOrder │ [order-12345] Start: 0ms -//! │ ID: task-001 │──────► Status: Success -//! └──────────────────┘ Duration: 10ms -//! │ -//! ├──────────────────┬──────────────────┐ -//! ▼ ▼ ▼ -//! ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ -//! │ CheckInventory │ │ ValidatePayment │ │ CalculateShipping│ -//! │ ID: task-002 │ │ ID: task-003 │ │ ID: task-004 │ -//! └──────────────────┘ └──────────────────┘ └──────────────────┘ -//! [order-12345] [order-12345] [order-12345] -//! Duration: 45ms Duration: 120ms Duration: 15ms -//! │ │ │ -//! └──────────────────┴──────────────────┘ -//! ▼ -//! ┌──────────────────┐ -//! │ FulfillOrder │ [order-12345] -//! │ ID: task-005 │ Duration: 30ms -//! └──────────────────┘ -//! ``` -//! -//! ## Key Concepts -//! - **Observability**: Make task execution visible for debugging -//! - **Correlation IDs**: Track a request through multiple tasks -//! - **Structured logging**: Consistent task metadata across logs -//! - **Performance metrics**: Measure task execution time -//! - **Task lifecycle**: Track start, completion, and errors -//! -//! ## Use Cases -//! - Debugging complex workflows in production -//! - Performance analysis and bottleneck identification -//! - Distributed tracing in microservices -//! - SLA monitoring and alerting -//! - Audit logging for compliance -//! -//! ## Running This Example -//! ```bash -//! cargo run --example debug_tracing -//! ``` -//! -//! ## Expected Output -//! ```text -//! === Debug Tracing Example === -//! -//! Starting order processing for order-12345 -//! -//! [order-12345] [task-001] ValidateOrder: Starting -//! [order-12345] [task-001] ValidateOrder: Completed in 10ms - Success -//! -//! [order-12345] [task-002] CheckInventory: Starting -//! [order-12345] [task-003] ValidatePayment: Starting -//! [order-12345] [task-004] CalculateShipping: Starting -//! [order-12345] [task-002] CheckInventory: Completed in 45ms - Success -//! [order-12345] [task-004] CalculateShipping: Completed in 15ms - Success -//! [order-12345] [task-003] ValidatePayment: Completed in 120ms - Success -//! -//! [order-12345] [task-005] FulfillOrder: Starting -//! [order-12345] [task-005] FulfillOrder: Completed in 30ms - Success -//! -//! === Execution Trace Summary === -//! Correlation ID: order-12345 -//! Total Tasks: 5 -//! Total Duration: 220ms -//! Success: 5, Failed: 0 -//! -//! Task Breakdown: -//! ValidateOrder: 10ms -//! CheckInventory: 45ms -//! ValidatePayment: 120ms (slowest) -//! CalculateShipping: 15ms -//! FulfillOrder: 30ms -//! ``` - -use dagx::{task, DagRunner}; - -use std::time::Instant; -use tokio::time::{sleep, Duration}; - -// Stateful task that tracks execution metadata for debugging -struct TracedTask { - task_id: String, - task_name: String, - correlation_id: String, - work_duration_ms: u64, - start_time: Option, -} - -impl TracedTask { - fn new( - task_id: impl Into, - task_name: impl Into, - correlation_id: impl Into, - work_duration_ms: u64, - ) -> Self { - Self { - task_id: task_id.into(), - task_name: task_name.into(), - correlation_id: correlation_id.into(), - work_duration_ms, - start_time: None, - } - } -} - -#[task] -impl TracedTask { - async fn run(&mut self) -> Result { - // Record start - let start = Instant::now(); - self.start_time = Some(start); - - println!( - "[{}] [{}] {}: Starting", - self.correlation_id, self.task_id, self.task_name - ); - - // Simulate work - sleep(Duration::from_millis(self.work_duration_ms)).await; - - // Record completion - let duration_ms = start.elapsed().as_millis() as u64; - - println!( - "[{}] [{}] {}: Completed in {}ms - Success", - self.correlation_id, self.task_id, self.task_name, duration_ms - ); - - Ok(TaskTrace { - task_id: self.task_id.clone(), - task_name: self.task_name.clone(), - correlation_id: self.correlation_id.clone(), - duration_ms, - status: "Success", - }) - } -} - -// Task that can fail, demonstrating error tracing -struct TracedTaskWithError { - task_id: String, - task_name: String, - correlation_id: String, - should_fail: bool, -} - -impl TracedTaskWithError { - fn new( - task_id: impl Into, - task_name: impl Into, - correlation_id: impl Into, - should_fail: bool, - ) -> Self { - Self { - task_id: task_id.into(), - task_name: task_name.into(), - correlation_id: correlation_id.into(), - should_fail, - } - } -} - -#[task] -impl TracedTaskWithError { - async fn run(&mut self) -> Result { - let start = Instant::now(); - - println!( - "[{}] [{}] {}: Starting", - self.correlation_id, self.task_id, self.task_name - ); - - sleep(Duration::from_millis(50)).await; - - let duration_ms = start.elapsed().as_millis() as u64; - - if self.should_fail { - println!( - "[{}] [{}] {}: Failed in {}ms - Validation Error", - self.correlation_id, self.task_id, self.task_name, duration_ms - ); - - Err(TaskTrace { - task_id: self.task_id.clone(), - task_name: self.task_name.clone(), - correlation_id: self.correlation_id.clone(), - duration_ms, - status: "Failed", - }) - } else { - println!( - "[{}] [{}] {}: Completed in {}ms - Success", - self.correlation_id, self.task_id, self.task_name, duration_ms - ); - - Ok(TaskTrace { - task_id: self.task_id.clone(), - task_name: self.task_name.clone(), - correlation_id: self.correlation_id.clone(), - duration_ms, - status: "Success", - }) - } - } -} - -// Task trace data structure -#[derive(Clone, Debug)] -#[allow(dead_code)] -struct TaskTrace { - task_id: String, - task_name: String, - correlation_id: String, - duration_ms: u64, - status: &'static str, -} - -// Task that aggregates traces from multiple upstream tasks -struct TraceAggregator { - correlation_id: String, -} - -impl TraceAggregator { - fn new(correlation_id: impl Into) -> Self { - Self { - correlation_id: correlation_id.into(), - } - } -} - -#[task] -impl TraceAggregator { - async fn run( - &mut self, - t1: &Result, - t2: &Result, - t3: &Result, - ) -> TraceSummary { - let traces = vec![t1, t2, t3]; - - let mut summary = TraceSummary { - correlation_id: self.correlation_id.clone(), - task_count: traces.len(), - total_duration_ms: 0, - success_count: 0, - failed_count: 0, - task_details: Vec::new(), - }; - - for trace_result in traces { - match trace_result { - Ok(trace) => { - summary.success_count += 1; - summary.total_duration_ms += trace.duration_ms; - summary.task_details.push(trace.clone()); - } - Err(e) => { - summary.failed_count += 1; - eprintln!("[{}] Task failed: {}", self.correlation_id, e); - } - } - } - - summary - } -} - -// Summary of traced execution -#[derive(Clone, Debug)] -struct TraceSummary { - correlation_id: String, - task_count: usize, - total_duration_ms: u64, - success_count: usize, - failed_count: usize, - task_details: Vec, -} - -impl TraceSummary { - fn print(&self) { - println!("\n=== Execution Trace Summary ==="); - println!("Correlation ID: {}", self.correlation_id); - println!("Total Tasks: {}", self.task_count); - println!("Total Duration: {}ms", self.total_duration_ms); - println!( - "Success: {}, Failed: {}", - self.success_count, self.failed_count - ); - - if !self.task_details.is_empty() { - println!("\nTask Breakdown:"); - let max_duration = self - .task_details - .iter() - .map(|t| t.duration_ms) - .max() - .unwrap_or(0); - - for trace in &self.task_details { - let slowest_tag = if trace.duration_ms == max_duration { - " (slowest)" - } else { - "" - }; - println!( - " {}: {}ms{}", - trace.task_name, trace.duration_ms, slowest_tag - ); - } - } - } -} - -#[tokio::main] -async fn main() { - println!("=== Debug Tracing Example ===\n"); - - // Example 1: Successful order processing with full tracing - println!("Example 1: Successful order processing\n"); - { - let correlation_id = "order-12345"; - println!("Starting order processing for {}\n", correlation_id); - - let dag = DagRunner::new(); - - // Root task: Validate order - let _validate = dag.add_task(TracedTask::new( - "task-001", - "ValidateOrder", - correlation_id, - 10, - )); - - // Parallel validation tasks (independent, no dependencies) - let inventory = dag.add_task(TracedTask::new( - "task-002", - "CheckInventory", - correlation_id, - 45, - )); - - let payment = dag.add_task(TracedTask::new( - "task-003", - "ValidatePayment", - correlation_id, - 120, - )); - - let shipping = dag.add_task(TracedTask::new( - "task-004", - "CalculateShipping", - correlation_id, - 15, - )); - - // Aggregate traces - let summary = dag - .add_task(TraceAggregator::new(correlation_id)) - .depends_on((inventory, payment, shipping)); - - // Run the DAG - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // Print summary - dag.get(summary).unwrap().print(); - } - - println!("\n"); - - // Example 2: Order with failed payment - println!("Example 2: Order with failed payment\n"); - { - let correlation_id = "order-67890"; - println!("Starting order processing for {}\n", correlation_id); - - let dag = DagRunner::new(); - - let _validate = dag.add_task(TracedTaskWithError::new( - "task-101", - "ValidateOrder", - correlation_id, - false, - )); - - let _inventory = dag.add_task(TracedTaskWithError::new( - "task-102", - "CheckInventory", - correlation_id, - false, - )); - - // This task will fail - let payment = dag.add_task(TracedTaskWithError::new( - "task-103", - "ValidatePayment", - correlation_id, - true, // will fail - )); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // Check results - match dag.get(payment).unwrap().as_ref() { - Ok(_) => println!("\nPayment validation succeeded"), - Err(trace) => { - println!("\n[{}] Payment validation failed:", correlation_id); - println!(" Task ID: {}", trace.task_id); - println!(" Duration: {}ms", trace.duration_ms); - println!(" Status: {}", trace.status); - } - } - } - - println!("\n=== Debug Tracing Example Complete ==="); -} diff --git a/examples/large_dag.rs b/examples/large_dag.rs deleted file mode 100644 index 11aac09..0000000 --- a/examples/large_dag.rs +++ /dev/null @@ -1,382 +0,0 @@ -//! # Working with Large DAGs: Performance and Scalability -//! -//! This example demonstrates dagx's performance characteristics when working -//! with large-scale DAGs containing hundreds or thousands of tasks. -//! -//! ## What You'll Learn -//! - How dagx scales with increasing task counts -//! - Performance characteristics of different DAG topologies -//! - How to benchmark DAG construction and execution -//! - Memory efficiency considerations -//! -//! ## DAG Topologies Tested -//! -//! **1. Wide DAG** - Maximum parallelism -//! ```text -//! ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ -//! │Task 1│ │Task 2│ │Task 3│ ... │Task N│ -//! └──────┘ └──────┘ └──────┘ └──────┘ -//! (1000 independent parallel tasks) -//! ``` -//! -//! **2. Deep DAG** - Sequential chain -//! ```text -//! ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ -//! │Task 1│───►│Task 2│───►│Task 3│───► ... ──►│Task N│ -//! └──────┘ └──────┘ └──────┘ └──────┘ -//! (100-level dependency chain) -//! ``` -//! -//! **3. Binary Tree** - Balanced aggregation -//! ```text -//! ┌────┐ ┌────┐ ┌────┐ ┌────┐ -//! │ T1 │ │ T2 │ │ T3 │ │ T4 │ -//! └─┬──┘ └─┬──┘ └─┬──┘ └─┬──┘ -//! └───┬───┘ └───┬───┘ -//! │ │ -//! ┌──▼──┐ ┌──▼──┐ -//! │ C1 │ │ C2 │ -//! └──┬──┘ └──┬──┘ -//! └───────┬──────┘ -//! │ -//! ┌──▼──┐ -//! │Root │ -//! └─────┘ -//! ``` -//! -//! **4. Diamond Pattern** - Many-to-many -//! ```text -//! Layer 1: 50 sources -//! Layer 2: 50 tasks (each depends on 2 sources) -//! Layer 3: 4 aggregates -//! Layer 4: 1 final result -//! ``` -//! -//! ## Performance Metrics -//! - **Build time**: Time to construct the DAG structure -//! - **Execution time**: Time to run all tasks -//! - **Tasks/second**: Throughput metric -//! - **Memory overhead**: Approximate per-task memory usage -//! -//! ## Key Observations -//! - DAG construction is very fast (< 1ms for 1000s of tasks) -//! - Parallel execution scales well with available CPU cores -//! - Deep chains execute sequentially as expected -//! - Memory overhead is minimal (~200 bytes per task) -//! -//! ## Running This Example -//! ```bash -//! cargo run --example large_dag --release # Use --release for accurate benchmarks -//! ``` -//! -//! ## Expected Output -//! ```text -//! === Large DAG Performance Example === -//! -//! Example 1: Wide DAG (1000 parallel tasks) -//! Build time: 347µs -//! Execution time: 12.5ms -//! Tasks/second: 80000 -//! Sample sum (first 10): ... -//! -//! Example 2: Deep DAG (100-level chain) -//! Build time: 156µs -//! Execution time: 8.2ms -//! Final result: ... -//! -//! Example 3: Binary Tree DAG (depth 10, 1023 tasks) -//! Build time: 423µs -//! Tree depth: 9 -//! Execution time: 15.3ms -//! Root result: ... -//! -//! Example 4: Diamond Pattern (50x50 = 2500 edges) -//! Build time: 234µs -//! Execution time: 11.7ms -//! Total tasks: 105 -//! Final result: ... -//! -//! Example 5: Memory Efficiency (100 tasks, measure overhead) -//! i32 sum: ... -//! String total length: ... -//! -//! === Large DAG Performance Example Complete === -//! -//! Key Observations: -//! - DAG construction is very fast (< 1ms for 1000s of tasks) -//! - Parallel execution scales well with available CPU cores -//! - Deep chains execute sequentially as expected -//! - Memory overhead is minimal (~ 200 bytes per task) -//! ``` - -use dagx::{task, DagRunner}; - -use std::time::Instant; - -// Simple computation task -struct Compute { - value: i32, -} - -#[task] -impl Compute { - async fn run(&mut self) -> i32 { - // Simulate some work - let mut result = self.value; - for _ in 0..100 { - result = result.wrapping_mul(31).wrapping_add(17); - } - result - } -} - -// Task that combines two values -struct Combine; - -#[task] -impl Combine { - async fn run(a: &i32, b: &i32) -> i32 { - a.wrapping_add(*b) - } -} - -// Task that aggregates multiple values -struct Aggregate; - -#[task] -impl Aggregate { - async fn run(a: &i32, b: &i32, c: &i32, d: &i32) -> i32 { - a.wrapping_add(*b).wrapping_add(*c).wrapping_add(*d) - } -} - -// Task that formats a number as a string -struct FormatNumber { - id: i32, -} - -#[task] -impl FormatNumber { - async fn run(&self) -> String { - format!("Result {}", self.id) - } -} - -#[tokio::main] -async fn main() { - println!("=== Large DAG Performance Example ===\n"); - - // Example 1: Wide DAG (many parallel tasks) - println!("Example 1: Wide DAG (1000 parallel tasks)"); - { - let start = Instant::now(); - - let dag = DagRunner::new(); - - // Create 1000 independent tasks - let tasks: Vec<_> = (0..1000) - .map(|i| dag.add_task(Compute { value: i })) - .collect(); - - let build_time = start.elapsed(); - println!(" Build time: {:?}", build_time); - - let exec_start = Instant::now(); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - let exec_time = exec_start.elapsed(); - - println!(" Execution time: {:?}", exec_time); - println!(" Tasks/second: {:.0}", 1000.0 / exec_time.as_secs_f64()); - - // Verify a few results - let sum: i32 = tasks - .into_iter() - .take(10) - .map(|t| dag.get(t).unwrap()) - .fold(0i32, |acc, x| acc.wrapping_add(x)); - println!(" Sample sum (first 10): {}\n", sum); - } - - // Example 2: Deep DAG (long dependency chain) - println!("Example 2: Deep DAG (100-level chain)"); - { - let start = Instant::now(); - - let dag = DagRunner::new(); - - // Create a deep chain by combining adjacent task pairs - let tasks: Vec<_> = (0..100) - .map(|i| dag.add_task(Compute { value: i }).into()) - .collect(); - let mut chain_tasks = vec![]; - for i in 0..99 { - let combined = dag.add_task(Combine).depends_on((&tasks[i], &tasks[i + 1])); - chain_tasks.push(combined); - } - - let prev = chain_tasks.last().unwrap(); - - let build_time = start.elapsed(); - println!(" Build time: {:?}", build_time); - - let exec_start = Instant::now(); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - let exec_time = exec_start.elapsed(); - - println!(" Execution time: {:?}", exec_time); - println!(" Final result: {}\n", dag.get(prev).unwrap()); - } - - // Example 3: Binary tree DAG - println!("Example 3: Binary Tree DAG (depth 10, 1023 tasks)"); - { - let start = Instant::now(); - - let dag = DagRunner::new(); - - // Build binary tree: each level combines pairs from previous level - let current_level: Vec<_> = (0..512) - .map(|i| dag.add_task(Compute { value: i })) - .collect(); - - // Process all levels reducing by combining pairs - let mut level = 0; - let mut layer: Vec> = - current_level.into_iter().map(|b| b.into()).collect(); - - while layer.len() > 1 { - level += 1; - let mut next_layer = Vec::new(); - - for i in (0..layer.len()).step_by(2) { - if i + 1 < layer.len() { - // We have a pair - combine them - let combined = dag.add_task(Combine).depends_on((&layer[i], &layer[i + 1])); - next_layer.push(combined); - } else { - // Odd one out - just pass it through - next_layer.push(layer[i]); - } - } - - layer = next_layer; - } - - let root = &layer[0]; - - let build_time = start.elapsed(); - println!(" Build time: {:?}", build_time); - println!(" Tree depth: {}", level); - - let exec_start = Instant::now(); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - let exec_time = exec_start.elapsed(); - - println!(" Execution time: {:?}", exec_time); - println!(" Root result: {}\n", dag.get(root).unwrap()); - } - - // Example 4: Diamond pattern (many-to-many) - println!("Example 4: Diamond Pattern (50x50 = 2500 edges)"); - { - let start = Instant::now(); - - let dag = DagRunner::new(); - - // Layer 1: 50 source tasks - let sources: Vec<_> = (0..50) - .map(|i| dag.add_task(Compute { value: i }).into()) - .collect(); - - // Layer 2: 50 tasks, each depends on all sources - let middle: Vec<_> = (0..50) - .map(|i| { - let combine = dag.add_task(Combine); - // Each middle task depends on two sources - combine.depends_on(( - &sources[i % sources.len()], - &sources[(i + 1) % sources.len()], - )) - }) - .collect(); - - // Layer 3: Aggregate to 4 groups - let aggregates: Vec<_> = (0..4) - .map(|i| { - let start_idx = i * 12; - dag.add_task(Aggregate).depends_on(( - &middle[start_idx % middle.len()], - &middle[(start_idx + 1) % middle.len()], - &middle[(start_idx + 2) % middle.len()], - &middle[(start_idx + 3) % middle.len()], - )) - }) - .collect(); - - // Layer 4: Final aggregate - let final_result = dag.add_task(Aggregate).depends_on(( - &aggregates[0], - &aggregates[1], - &aggregates[2], - &aggregates[3], - )); - - let build_time = start.elapsed(); - println!(" Build time: {:?}", build_time); - - let exec_start = Instant::now(); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - let exec_time = exec_start.elapsed(); - - println!(" Execution time: {:?}", exec_time); - println!(" Total tasks: {}", 50 + 50 + 4 + 1); - println!(" Final result: {}\n", dag.get(final_result).unwrap()); - } - - // Example 5: Memory efficiency test - println!("Example 5: Memory Efficiency (100 tasks, measure overhead)"); - { - let dag = DagRunner::new(); - - // Create tasks with various output sizes - let small_tasks: Vec<_> = (0..50) - .map(|i| dag.add_task(Compute { value: i })) - .collect(); - - let string_tasks: Vec<_> = (0..50) - .map(|i| dag.add_task(FormatNumber { id: i })) - .collect(); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // Verify results are accessible - let i32_sum: i32 = small_tasks - .into_iter() - .map(|t| dag.get(t).unwrap()) - .fold(0i32, |acc, x| acc.wrapping_add(x)); - let string_count = string_tasks - .into_iter() - .map(|t| dag.get(t).unwrap().len()) - .sum::(); - - println!(" i32 sum: {}", i32_sum); - println!(" String total length: {}", string_count); - } - - println!("\n=== Large DAG Performance Example Complete ==="); - println!("\nKey Observations:"); - println!(" - DAG construction is very fast (< 1ms for 1000s of tasks)"); - println!(" - Parallel execution scales well with available CPU cores"); - println!(" - Deep chains execute sequentially as expected"); - println!(" - Memory overhead is minimal (~ 200 bytes per task)"); -} diff --git a/examples/retry_strategies.rs b/examples/retry_strategies.rs deleted file mode 100644 index e04140f..0000000 --- a/examples/retry_strategies.rs +++ /dev/null @@ -1,461 +0,0 @@ -//! # Retry Strategies for Resilient Workflows -//! -//! This example demonstrates various retry patterns for handling transient failures -//! in DAG workflows, such as network errors, rate limits, or temporary service unavailability. -//! -//! ## What You'll Learn -//! - How to implement immediate, linear, and exponential backoff retry strategies -//! - How to distinguish retriable vs non-retriable errors -//! - How to track retry attempts and provide diagnostics -//! - How to implement jitter to prevent thundering herd problems -//! -//! ## Retry Patterns -//! -//! **1. Immediate Retry** - Retry without delay (for quick transient failures) -//! ```text -//! Attempt 1: ✗ Error -//! Attempt 2: ✗ Error (no delay) -//! Attempt 3: ✓ Success -//! ``` -//! -//! **2. Linear Backoff** - Fixed delay between retries -//! ```text -//! Attempt 1: ✗ Error -//! Wait 100ms -//! Attempt 2: ✗ Error -//! Wait 100ms -//! Attempt 3: ✓ Success -//! ``` -//! -//! **3. Exponential Backoff** - Increasing delays between retries -//! ```text -//! Attempt 1: ✗ Error -//! Wait 100ms -//! Attempt 2: ✗ Error -//! Wait 200ms -//! Attempt 3: ✗ Error -//! Wait 400ms -//! Attempt 4: ✓ Success -//! ``` -//! -//! **4. Exponential Backoff with Jitter** - Randomized delays to prevent coordinated retries -//! ```text -//! Attempt 1: ✗ Error -//! Wait 87ms (50-150ms range) -//! Attempt 2: ✗ Error -//! Wait 234ms (100-300ms range) -//! Attempt 3: ✓ Success -//! ``` -//! -//! ## Workflow Diagram -//! ```text -//! ┌──────────────────────┐ -//! │ ApiCallWithRetry │ -//! │ Max attempts: 3 │ -//! │ Strategy: Exp │ -//! └──────────────────────┘ -//! │ -//! ├─► Attempt 1: ✗ (Connection timeout) -//! │ Wait 100ms -//! │ -//! ├─► Attempt 2: ✗ (Connection timeout) -//! │ Wait 200ms -//! │ -//! └─► Attempt 3: ✓ Success -//! -//! ┌──────────────────────┐ -//! │ ApiCallWithRetry │ -//! │ Max attempts: 3 │ -//! │ Strategy: Linear │ -//! └──────────────────────┘ -//! │ -//! ├─► Attempt 1: ✗ (Rate limit) -//! │ Wait 500ms -//! │ -//! ├─► Attempt 2: ✗ (Rate limit) -//! │ Wait 500ms -//! │ -//! ├─► Attempt 3: ✗ (Rate limit) -//! │ -//! └─► Final: ✗ Retry exhausted -//! ``` -//! -//! ## Key Concepts -//! - **Idempotency**: Retried operations should be safe to repeat -//! - **Retriable errors**: Network timeouts, rate limits, temporary service errors -//! - **Non-retriable errors**: Authentication failures, invalid input, logical errors -//! - **Backoff strategies**: Balance between recovery speed and system load -//! - **Jitter**: Randomization to avoid thundering herd when many clients retry simultaneously -//! - **Retry budgets**: Limit total retries to prevent cascading failures -//! -//! ## Use Cases -//! - External API calls with transient network failures -//! - Database operations with temporary connection issues -//! - Distributed systems with eventual consistency -//! - Rate-limited API integrations -//! - Cloud service calls with intermittent availability -//! -//! ## Running This Example -//! ```bash -//! cargo run --example retry_strategies -//! ``` -//! -//! ## Expected Output -//! ```text -//! === Retry Strategies Example === -//! -//! Example 1: Immediate retry strategy -//! [ApiCall-1] Attempt 1/3: Calling external API... -//! [ApiCall-1] ✗ Transient error: Connection timeout -//! [ApiCall-1] Retrying immediately (attempt 2/3) -//! [ApiCall-1] Attempt 2/3: Calling external API... -//! [ApiCall-1] ✗ Transient error: Connection timeout -//! [ApiCall-1] Retrying immediately (attempt 3/3) -//! [ApiCall-1] Attempt 3/3: Calling external API... -//! [ApiCall-1] ✓ Success: API response data -//! Result: Ok(RetryResult { attempts: 3, success: true }) -//! -//! Example 2: Linear backoff (500ms delay) -//! [ApiCall-2] Attempt 1/4: Calling external API... -//! [ApiCall-2] ✗ Transient error: Rate limit exceeded -//! [ApiCall-2] Waiting 500ms before retry (attempt 2/4) -//! [ApiCall-2] Attempt 2/4: Calling external API... -//! [ApiCall-2] ✓ Success: API response data -//! Result: Ok(RetryResult { attempts: 2, success: true }) -//! -//! Example 3: Exponential backoff -//! [ApiCall-3] Attempt 1/5: Calling external API... -//! [ApiCall-3] ✗ Transient error: Service unavailable -//! [ApiCall-3] Waiting 100ms before retry (attempt 2/5) -//! [ApiCall-3] Attempt 2/5: Calling external API... -//! [ApiCall-3] ✗ Transient error: Service unavailable -//! [ApiCall-3] Waiting 200ms before retry (attempt 3/5) -//! [ApiCall-3] Attempt 3/5: Calling external API... -//! [ApiCall-3] ✓ Success: API response data -//! Result: Ok(RetryResult { attempts: 3, success: true }) -//! -//! Example 4: Non-retriable error (fails immediately) -//! [ApiCall-4] Attempt 1/3: Calling external API... -//! [ApiCall-4] ✗ Non-retriable error: Authentication failed -//! Result: Err("Authentication failed - not retrying") -//! -//! Example 5: Retry exhausted -//! [ApiCall-5] Attempt 1/3: Calling external API... -//! [ApiCall-5] ✗ Transient error: Connection timeout -//! [ApiCall-5] Waiting 100ms before retry (attempt 2/3) -//! [ApiCall-5] Attempt 2/3: Calling external API... -//! [ApiCall-5] ✗ Transient error: Connection timeout -//! [ApiCall-5] Waiting 200ms before retry (attempt 3/3) -//! [ApiCall-5] Attempt 3/3: Calling external API... -//! [ApiCall-5] ✗ Transient error: Connection timeout -//! [ApiCall-5] All retry attempts exhausted -//! Result: Err("Retry exhausted after 3 attempts: Connection timeout") -//! ``` - -use dagx::{task, DagRunner}; - -use tokio::time::{sleep, Duration}; - -// Retry strategy configuration -#[derive(Clone, Copy, Debug)] -enum RetryStrategy { - Immediate, - Linear { delay_ms: u64 }, - Exponential { base_ms: u64, factor: f64 }, -} - -impl RetryStrategy { - fn calculate_delay(&self, attempt: u32) -> Option { - match self { - RetryStrategy::Immediate => None, - RetryStrategy::Linear { delay_ms } => Some(Duration::from_millis(*delay_ms)), - RetryStrategy::Exponential { base_ms, factor } => { - let delay = *base_ms as f64 * factor.powi(attempt as i32 - 1); - Some(Duration::from_millis(delay as u64)) - } - } - } -} - -// Simulated error types -#[derive(Clone, Debug)] -enum ApiError { - // Retriable errors - ConnectionTimeout, - ServiceUnavailable, - RateLimitExceeded, - - // Non-retriable errors - AuthenticationFailed, - #[allow(dead_code)] - InvalidInput, -} - -impl ApiError { - fn is_retriable(&self) -> bool { - matches!( - self, - ApiError::ConnectionTimeout - | ApiError::ServiceUnavailable - | ApiError::RateLimitExceeded - ) - } - - fn message(&self) -> &'static str { - match self { - ApiError::ConnectionTimeout => "Connection timeout", - ApiError::ServiceUnavailable => "Service unavailable", - ApiError::RateLimitExceeded => "Rate limit exceeded", - ApiError::AuthenticationFailed => "Authentication failed", - ApiError::InvalidInput => "Invalid input", - } - } -} - -// Result of retry operation -#[derive(Clone, Debug)] -#[allow(dead_code)] -struct RetryResult { - attempts: u32, - success: bool, - final_error: Option, -} - -// Task that simulates an API call with configurable failure pattern -struct ApiCallWithRetry { - task_id: String, - max_attempts: u32, - strategy: RetryStrategy, - // Simulated failure pattern: errors to return before success - failure_sequence: Vec, - current_attempt: u32, -} - -impl ApiCallWithRetry { - fn new( - task_id: impl Into, - max_attempts: u32, - strategy: RetryStrategy, - failure_sequence: Vec, - ) -> Self { - Self { - task_id: task_id.into(), - max_attempts, - strategy, - failure_sequence, - current_attempt: 0, - } - } - - async fn attempt_api_call(&mut self) -> Result { - println!( - "[{}] Attempt {}/{}: Calling external API...", - self.task_id, - self.current_attempt + 1, - self.max_attempts - ); - - // Simulate network delay - sleep(Duration::from_millis(50)).await; - - // Check if we should fail this attempt - if (self.current_attempt as usize) < self.failure_sequence.len() { - let error = self.failure_sequence[self.current_attempt as usize].clone(); - Err(error) - } else { - Ok("API response data".to_string()) - } - } -} - -#[task] -impl ApiCallWithRetry { - async fn run(&mut self) -> Result { - loop { - self.current_attempt += 1; - - match self.attempt_api_call().await { - Ok(data) => { - println!("[{}] ✓ Success: {}", self.task_id, data); - return Ok(RetryResult { - attempts: self.current_attempt, - success: true, - final_error: None, - }); - } - Err(error) => { - println!( - "[{}] ✗ {} error: {}", - self.task_id, - if error.is_retriable() { - "Transient" - } else { - "Non-retriable" - }, - error.message() - ); - - // Check if error is retriable - if !error.is_retriable() { - return Err(format!("{} - not retrying", error.message())); - } - - // Check if we have attempts remaining - if self.current_attempt >= self.max_attempts { - println!("[{}] All retry attempts exhausted", self.task_id); - return Err(format!( - "Retry exhausted after {} attempts: {}", - self.current_attempt, - error.message() - )); - } - - // Calculate and apply backoff delay - if let Some(delay) = self.strategy.calculate_delay(self.current_attempt) { - println!( - "[{}] Waiting {}ms before retry (attempt {}/{})", - self.task_id, - delay.as_millis(), - self.current_attempt + 1, - self.max_attempts - ); - sleep(delay).await; - } else { - println!( - "[{}] Retrying immediately (attempt {}/{})", - self.task_id, - self.current_attempt + 1, - self.max_attempts - ); - } - - // Continue loop to retry - } - } - } - } -} - -#[tokio::main] -async fn main() { - println!("=== Retry Strategies Example ===\n"); - - // Example 1: Immediate retry - succeeds on 3rd attempt - println!("Example 1: Immediate retry strategy"); - { - let dag = DagRunner::new(); - - let api_call = dag.add_task(ApiCallWithRetry::new( - "ApiCall-1", - 3, - RetryStrategy::Immediate, - vec![ - ApiError::ConnectionTimeout, - ApiError::ConnectionTimeout, - // 3rd attempt will succeed - ], - )); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - println!("Result: {:?}\n", dag.get(api_call).unwrap()); - } - - // Example 2: Linear backoff - succeeds on 2nd attempt - println!("Example 2: Linear backoff (500ms delay)"); - { - let dag = DagRunner::new(); - - let api_call = dag.add_task(ApiCallWithRetry::new( - "ApiCall-2", - 4, - RetryStrategy::Linear { delay_ms: 500 }, - vec![ - ApiError::RateLimitExceeded, - // 2nd attempt will succeed - ], - )); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - println!("Result: {:?}\n", dag.get(api_call).unwrap()); - } - - // Example 3: Exponential backoff - succeeds on 3rd attempt - println!("Example 3: Exponential backoff"); - { - let dag = DagRunner::new(); - - let api_call = dag.add_task(ApiCallWithRetry::new( - "ApiCall-3", - 5, - RetryStrategy::Exponential { - base_ms: 100, - factor: 2.0, - }, - vec![ - ApiError::ServiceUnavailable, - ApiError::ServiceUnavailable, - // 3rd attempt will succeed - ], - )); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - println!("Result: {:?}\n", dag.get(api_call).unwrap()); - } - - // Example 4: Non-retriable error - fails immediately - println!("Example 4: Non-retriable error (fails immediately)"); - { - let dag = DagRunner::new(); - - let api_call = dag.add_task(ApiCallWithRetry::new( - "ApiCall-4", - 3, - RetryStrategy::Immediate, - vec![ApiError::AuthenticationFailed], - )); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - println!("Result: {:?}\n", dag.get(api_call).unwrap()); - } - - // Example 5: Retry exhausted - all attempts fail - println!("Example 5: Retry exhausted"); - { - let dag = DagRunner::new(); - - let api_call = dag.add_task(ApiCallWithRetry::new( - "ApiCall-5", - 3, - RetryStrategy::Exponential { - base_ms: 100, - factor: 2.0, - }, - vec![ - ApiError::ConnectionTimeout, - ApiError::ConnectionTimeout, - ApiError::ConnectionTimeout, - // All attempts will fail - ], - )); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - println!("Result: {:?}\n", dag.get(api_call).unwrap()); - } - - println!("=== Retry Strategies Example Complete ==="); -} diff --git a/examples/timeout.rs b/examples/timeout.rs deleted file mode 100644 index 68dbb2b..0000000 --- a/examples/timeout.rs +++ /dev/null @@ -1,296 +0,0 @@ -//! # Managing Timeouts in Tasks -//! -//! This example demonstrates how to implement timeout patterns in DAG tasks using -//! tokio's timeout utilities for time-bounded execution. -//! -//! ## What You'll Learn -//! - How to implement timeouts using `tokio::time::timeout` -//! - How to build tasks with built-in timeout logic -//! - How to handle timeout errors as Result types -//! - How to apply timeouts at both task-level and DAG-level -//! -//! ## Timeout Patterns -//! -//! **1. Task-Level Timeout** - Individual task with timeout -//! ```rust -//! async fn run(&mut self) -> Result { -//! let work = async { /* ... */ }; -//! match timeout(Duration::from_millis(self.timeout_ms), work).await { -//! Ok(result) => Ok(result), -//! Err(_) => Err("Task timed out".to_string()), -//! } -//! } -//! ``` -//! -//! **2. DAG-Level Timeout** - Entire workflow with timeout -//! ```rust -//! match timeout(dag_timeout, dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await { -//! Ok(result) => { /* DAG completed */ }, -//! Err(_) => { /* DAG timed out */ }, -//! } -//! ``` -//! -//! ## Workflow Diagram -//! ```text -//! ┌─────────────┐ -//! │ FastTask │ (completes in 100ms) -//! │ (timeout │ ───► Ok("Fast task completed") -//! │ 500ms) │ -//! └─────────────┘ -//! -//! ┌─────────────┐ -//! │ SlowTask │ (takes 1000ms) -//! │ (timeout │ ───► Err("Task timed out after 200ms") -//! │ 200ms) │ -//! └─────────────┘ -//! │ -//! └──────►┌──────────────┐ -//! │ProcessResult │ ───► "✗ Task timed out..." -//! └──────────────┘ -//! ``` -//! -//! ## Key Concepts -//! - **Timeout as error**: Timeouts produce `Err` results that propagate through the DAG -//! - **Error as data**: Use Result to represent timeout states -//! - **Graceful degradation**: Downstream tasks can handle timeout errors -//! - **Time-bounded execution**: Enforce SLAs and prevent runaway tasks -//! -//! ## Use Cases -//! - API calls with timeout limits -//! - SLA enforcement in workflows -//! - Preventing resource exhaustion from slow tasks -//! - Circuit breaker patterns -//! - External service integration with timeouts -//! -//! ## Running This Example -//! ```bash -//! cargo run --example timeout -//! ``` -//! -//! ## Expected Output -//! ```text -//! === Timeout Example === -//! -//! Example 1: All tasks complete successfully -//! Fast task: Ok("Fast task completed") -//! Timed task: Ok("Work completed in 100ms") -//! -//! Example 2: Task exceeds timeout -//! Result: ✗ Task timed out after 200ms (work would take 1000ms) -//! -//! Example 3: Parallel tasks with different speeds -//! Fast task 1: Ok("Fast task completed") -//! Fast task 2: Ok("Fast task completed") -//! Slow task: Ok("Slow task completed after 500ms") -//! Timed task 1: Ok("Work completed in 100ms") -//! Timed task 2: Err("Task timed out after 200ms...") -//! -//! Example 4: Downstream tasks handling timeout errors -//! Processed 1: ✗ Task timed out after 100ms (work would take 500ms) -//! Processed 2: ✗ Task timed out after 100ms (work would take 500ms) -//! -//! Example 5: Whole-DAG timeout -//! DAG completed within timeout -//! Task 1: Ok("Slow task completed after 200ms") -//! Task 2: Ok("Slow task completed after 200ms") -//! Task 3: Ok("Slow task completed after 200ms") -//! -//! === Timeout Example Complete === -//! ``` - -use dagx::{task, DagRunner, TaskHandle}; - -use tokio::time::{sleep, timeout, Duration}; - -// Fast task that completes quickly -struct FastTask; - -#[task] -impl FastTask { - async fn run() -> Result { - sleep(Duration::from_millis(100)).await; - Ok("Fast task completed".to_string()) - } -} - -// Slow task that takes a long time -struct SlowTask { - duration_ms: u64, -} - -#[task] -impl SlowTask { - async fn run(&mut self) -> Result { - sleep(Duration::from_millis(self.duration_ms)).await; - Ok(format!("Slow task completed after {}ms", self.duration_ms)) - } -} - -// Task with built-in timeout -struct TaskWithTimeout { - work_duration_ms: u64, - timeout_ms: u64, -} - -#[task] -impl TaskWithTimeout { - async fn run(&mut self) -> Result { - let work = async { - sleep(Duration::from_millis(self.work_duration_ms)).await; - format!("Work completed in {}ms", self.work_duration_ms) - }; - - match timeout(Duration::from_millis(self.timeout_ms), work).await { - Ok(result) => Ok(result), - Err(_) => Err(format!( - "Task timed out after {}ms (work would take {}ms)", - self.timeout_ms, self.work_duration_ms - )), - } - } -} - -// Task that processes results (including timeouts) -struct ProcessResult; - -#[task] -impl ProcessResult { - async fn run(input: &Result) -> String { - match input { - Ok(msg) => format!("✓ {}", msg), - Err(err) => format!("✗ {}", err), - } - } -} - -#[tokio::main] -async fn main() { - println!("=== Timeout Example ===\n"); - - // Example 1: All tasks complete within timeout - println!("Example 1: All tasks complete successfully"); - { - let dag = DagRunner::new(); - - let fast = dag.add_task(FastTask); - let timed = dag.add_task(TaskWithTimeout { - work_duration_ms: 100, - timeout_ms: 500, - }); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - println!("Fast task: {:?}", dag.get(fast).unwrap()); - println!("Timed task: {:?}\n", dag.get(timed).unwrap()); - } - - // Example 2: Task exceeds timeout - println!("Example 2: Task exceeds timeout"); - { - let dag = DagRunner::new(); - - let timed = dag.add_task(TaskWithTimeout { - work_duration_ms: 1000, - timeout_ms: 200, - }); - - let processed = dag.add_task(ProcessResult).depends_on(timed); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - println!("Result: {}\n", dag.get(processed).unwrap()); - } - - // Example 3: Mixed fast and slow tasks - println!("Example 3: Parallel tasks with different speeds"); - { - let dag = DagRunner::new(); - - let fast1 = dag.add_task(FastTask); - let fast2 = dag.add_task(FastTask); - - let slow = dag.add_task(SlowTask { duration_ms: 500 }); - - let timed1 = dag.add_task(TaskWithTimeout { - work_duration_ms: 100, - timeout_ms: 200, - }); - - let timed2 = dag.add_task(TaskWithTimeout { - work_duration_ms: 300, - timeout_ms: 200, - }); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - println!("Fast task 1: {:?}", dag.get(fast1).unwrap()); - println!("Fast task 2: {:?}", dag.get(fast2).unwrap()); - println!("Slow task: {:?}", dag.get(slow).unwrap()); - println!("Timed task 1: {:?}", dag.get(timed1).unwrap()); - println!("Timed task 2: {:?}\n", dag.get(timed2).unwrap()); - } - - // Example 4: Cascading timeout errors - println!("Example 4: Downstream tasks handling timeout errors"); - { - let dag = DagRunner::new(); - - let source: TaskHandle<_> = dag - .add_task(TaskWithTimeout { - work_duration_ms: 500, - timeout_ms: 100, - }) - .into(); - - let processed1 = dag.add_task(ProcessResult).depends_on(source); - let processed2 = dag.add_task(ProcessResult).depends_on(source); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - println!("Processed 1: {}", dag.get(processed1).unwrap()); - println!("Processed 2: {}\n", dag.get(processed2).unwrap()); - } - - // Example 5: Implementing whole-DAG timeout - println!("Example 5: Whole-DAG timeout"); - { - let dag = DagRunner::new(); - - let t1 = dag.add_task(SlowTask { duration_ms: 200 }); - let t2 = dag.add_task(SlowTask { duration_ms: 200 }); - let t3 = dag.add_task(SlowTask { duration_ms: 200 }); - - // Run the entire DAG with a timeout - let dag_timeout = Duration::from_millis(500); - match timeout( - dag_timeout, - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }), - ) - .await - { - Ok(result) => { - result.unwrap(); - println!("DAG completed within timeout"); - println!("Task 1: {:?}", dag.get(t1).unwrap()); - println!("Task 2: {:?}", dag.get(t2).unwrap()); - println!("Task 3: {:?}", dag.get(t3).unwrap()); - } - Err(_) => { - eprintln!( - "DAG execution timed out after {}ms", - dag_timeout.as_millis() - ); - } - } - } - - println!("\n=== Timeout Example Complete ==="); -} diff --git a/examples/tracing_example.rs b/examples/tracing_example.rs deleted file mode 100644 index 2fb5025..0000000 --- a/examples/tracing_example.rs +++ /dev/null @@ -1,162 +0,0 @@ -//! # Tracing Example -//! -//! This example demonstrates how to use the optional tracing feature to gain -//! visibility into DAG execution for debugging and performance analysis. -//! -//! ## Running This Example -//! -//! ```bash -//! # With default log level (info) -//! cargo run --example tracing_example --features tracing -//! -//! # With debug level (shows more details) -//! RUST_LOG=dagx=debug cargo run --example tracing_example --features tracing -//! -//! # With trace level (shows all execution details) -//! RUST_LOG=dagx=trace cargo run --example tracing_example --features tracing -//! ``` -//! -//! ## What You'll See -//! -//! - Task additions to the DAG -//! - Dependency wiring between tasks -//! - Topological layer computation -//! - Layer-by-layer execution -//! - Inline vs spawned task execution -//! - Completion status -//! -//! ## Log Levels -//! -//! - **INFO**: High-level DAG execution start/completion -//! - **DEBUG**: Task additions, dependency wiring, layer computation -//! - **TRACE**: Individual task execution details (inline vs spawned) -//! - **ERROR**: Errors, panics, cycle detection -//! - -use dagx::{task, DagRunner}; - -use tracing_subscriber::{fmt, EnvFilter}; - -// Simple source task -struct Value(i32); - -#[task] -impl Value { - async fn run(&self) -> i32 { - self.0 - } -} - -// Computation task -struct Add; - -#[task] -impl Add { - async fn run(a: &i32, b: &i32) -> i32 { - a + b - } -} - -// Another computation task -struct Multiply; - -#[task] -impl Multiply { - async fn run(a: &i32, b: &i32) -> i32 { - a * b - } -} - -#[tokio::main] -async fn main() { - // Initialize tracing subscriber with environment filter - // Use RUST_LOG environment variable to control log level - fmt() - .with_env_filter( - EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("dagx=info")), - ) - .init(); - - println!("=== DAG Execution with Tracing ===\n"); - println!("Run with RUST_LOG=dagx=debug or RUST_LOG=dagx=trace for more details\n"); - - // Example 1: Simple diamond pattern - println!("Example 1: Diamond Pattern"); - println!("---------------------------"); - { - let dag = DagRunner::new(); - - // Layer 0: Source - let source = dag.add_task(Value(10)).into(); - - // Layer 1: Two parallel computations - let left = dag.add_task(Add).depends_on((&source, &source)); - let right = dag.add_task(Multiply).depends_on((&source, &source)); - - // Layer 2: Combine results - let result = dag.add_task(Add).depends_on((&left, &right)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - println!("\nResult: {}", dag.get(result).unwrap()); - println!("(Expected: 10+10 = 20, 10*10 = 100, 20+100 = 120)\n"); - } - - // Example 2: Linear chain (demonstrates inline execution) - println!("\nExample 2: Linear Chain (Inline Execution)"); - println!("-------------------------------------------"); - { - let dag = DagRunner::new(); - - // Create a linear chain - each layer has only one task - // This should trigger inline execution optimization - let a = dag.add_task(Value(1)).into(); - let b = dag.add_task(Add).depends_on((&a, &a)); - let c = dag.add_task(Multiply).depends_on((&b, &b)); - let d = dag.add_task(Add).depends_on((&c, &c)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - println!("\nResult: {}", dag.get(d).unwrap()); - println!("(Expected: 1+1 = 2, 2*2 = 4, 4+4 = 8)\n"); - } - - // Example 3: Wide pattern (demonstrates spawned execution) - println!("\nExample 3: Wide Pattern (Spawned Execution)"); - println!("--------------------------------------------"); - { - let dag = DagRunner::new(); - - // Layer 0: Multiple sources - let v1 = dag.add_task(Value(1)).into(); - let v2 = dag.add_task(Value(2)).into(); - let v3 = dag.add_task(Value(3)).into(); - let v4 = dag.add_task(Value(4)).into(); - - // Layer 1: Multiple parallel computations - let sum12 = dag.add_task(Add).depends_on((&v1, &v2)); - let sum23 = dag.add_task(Add).depends_on((&v2, &v3)); - let sum34 = dag.add_task(Add).depends_on((&v3, &v4)); - - // Layer 2: Final combination - let sum_all = dag.add_task(Add).depends_on((&sum12, &sum23)); - let final_result = dag.add_task(Add).depends_on((&sum_all, &sum34)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - println!("\nResult: {}", dag.get(final_result).unwrap()); - println!("(Expected: (1+2)+(2+3) = 8, 8+(3+4) = 15)\n"); - } - - println!("\n=== Tracing Example Complete ==="); - println!("\nTry running with different log levels:"); - println!(" RUST_LOG=dagx=info - High-level execution info"); - println!(" RUST_LOG=dagx=debug - Task additions and layer details"); - println!(" RUST_LOG=dagx=trace - All execution details including inline/spawned"); -} diff --git a/scripts/release_check.sh b/scripts/release_check.sh index 7b4b49d..bc5725f 100755 --- a/scripts/release_check.sh +++ b/scripts/release_check.sh @@ -22,23 +22,23 @@ BLUE='\033[0;34m' NC='\033[0m' # No Color check_pass() { - echo -e "${GREEN}✓${NC} $1" + echo -e "${GREEN}✓${NC} $1" } check_fail() { - echo -e "${RED}✗${NC} $1" - exit 1 + echo -e "${RED}✗${NC} $1" + exit 1 } check_warn() { - echo -e "${YELLOW}⚠${NC} $1" + echo -e "${YELLOW}⚠${NC} $1" } section() { - echo "" - echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo -e "${BLUE}$1${NC}" - echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo "" + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${BLUE}$1${NC}" + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" } # ============================================================================= @@ -57,18 +57,18 @@ check_pass "Rust version compatible" echo "" echo "→ Checking for uncommitted changes..." if jj status 2>&1 | grep -q "Working copy changes:"; then - check_warn "Uncommitted changes found - you'll need to commit before publishing" + check_warn "Uncommitted changes found - you'll need to commit before publishing" else - check_pass "No uncommitted changes" + check_pass "No uncommitted changes" fi echo "" echo "→ Checking for old 'dagrunner' references..." OLD_REFS=$(grep -ri "dagrunner" --exclude-dir=target --exclude-dir=.git --exclude-dir=.jj --exclude="*.lock" --exclude="release_check.sh" . 2>/dev/null | grep -v "DagRunner" | grep -v "dag-runner" | wc -l) if [ "$OLD_REFS" -gt 0 ]; then - check_fail "Found $OLD_REFS old 'dagrunner' references (excluding DagRunner struct)" + check_fail "Found $OLD_REFS old 'dagrunner' references (excluding DagRunner struct)" else - check_pass "No old 'dagrunner' references found" + check_pass "No old 'dagrunner' references found" fi # ============================================================================= @@ -90,9 +90,9 @@ echo "" echo "→ Checking for TODO/FIXME/XXX/HACK in source..." TODO_COUNT=$(grep -r "TODO\|FIXME\|XXX\|HACK" src/ dagx-macros/src/ 2>/dev/null | wc -l) if [ "$TODO_COUNT" -gt 0 ]; then - check_warn "Found $TODO_COUNT TODO/FIXME/HACK comments in source" + check_warn "Found $TODO_COUNT TODO/FIXME/HACK comments in source" else - check_pass "No TODO/FIXME/HACK comments in source" + check_pass "No TODO/FIXME/HACK comments in source" fi # ============================================================================= @@ -218,16 +218,16 @@ check_pass "LICENSE files present in both crates" section "COVERAGE CHECK" echo "→ Running coverage analysis..." -COVERAGE=$(cargo tarpaulin --lib --all-features --out Stdout 2>&1 | grep -oP '^\d+\.\d+%' | head -1 | sed 's/%//') +COVERAGE=$(cargo tarpaulin --out Stdout 2>&1 | grep -oP '^\d+\.\d+%' | head -1 | sed 's/%//') if [ -z "$COVERAGE" ]; then - check_warn "Could not determine coverage percentage" + check_warn "Could not determine coverage percentage" else - echo " Current coverage: ${COVERAGE}%" - if (( $(echo "$COVERAGE < 80" | bc -l) )); then - check_warn "Coverage is ${COVERAGE}% (below 80% target)" - else - check_pass "Coverage is ${COVERAGE}% (meets 80% target)" - fi + echo " Current coverage: ${COVERAGE}%" + if (($(echo "$COVERAGE < 80" | bc -l))); then + check_warn "Coverage is ${COVERAGE}% (below 80% target)" + else + check_pass "Coverage is ${COVERAGE}% (meets 80% target)" + fi fi # ============================================================================= @@ -248,10 +248,10 @@ check_pass "All examples build successfully" echo "" echo "→ Checking examples run without panics..." if [ -f ./scripts/run_examples.sh ]; then - ./scripts/run_examples.sh || check_fail "Some examples failed to run" - check_pass "All examples run successfully" + ./scripts/run_examples.sh || check_fail "Some examples failed to run" + check_pass "All examples run successfully" else - check_warn "run_examples.sh not found, skipping runtime check" + check_warn "run_examples.sh not found, skipping runtime check" fi echo "" @@ -297,9 +297,9 @@ echo "→ Verifying version consistency..." DAGX_VERSION=$(grep '^version = ' Cargo.toml | head -1 | cut -d'"' -f2) MACROS_VERSION=$(grep '^version = ' dagx-macros/Cargo.toml | head -1 | cut -d'"' -f2) if [ "$DAGX_VERSION" != "$MACROS_VERSION" ]; then - check_fail "Version mismatch: dagx=$DAGX_VERSION, dagx-macros=$MACROS_VERSION" + check_fail "Version mismatch: dagx=$DAGX_VERSION, dagx-macros=$MACROS_VERSION" else - check_pass "Versions match: both are $DAGX_VERSION" + check_pass "Versions match: both are $DAGX_VERSION" fi # ============================================================================= @@ -316,10 +316,10 @@ echo "" echo "→ Testing dagx package (dry-run with --allow-dirty)..." echo " Note: This will fail until dagx-macros is published to crates.io" if cargo publish --dry-run --allow-dirty 2>&1 | grep -q "failed to select a version for the requirement.*dagx-macros"; then - check_warn "dagx package check shows dependency on unpublished dagx-macros v$VERSION (expected)" + check_warn "dagx package check shows dependency on unpublished dagx-macros v$VERSION (expected)" else - cargo publish --dry-run --allow-dirty || check_fail "dagx package check failed unexpectedly" - check_pass "dagx package is ready (dagx-macros must be published first)" + cargo publish --dry-run --allow-dirty || check_fail "dagx package check failed unexpectedly" + check_pass "dagx package is ready (dagx-macros must be published first)" fi echo "" @@ -332,9 +332,9 @@ echo "→ Checking package size..." PACKAGE_SIZE=$(cargo package --list --allow-dirty 2>/dev/null | wc -l) echo " Files to be packaged: $PACKAGE_SIZE" if [ "$PACKAGE_SIZE" -lt 5 ]; then - check_fail "Suspiciously few files to package ($PACKAGE_SIZE)" + check_fail "Suspiciously few files to package ($PACKAGE_SIZE)" else - check_pass "Package contains $PACKAGE_SIZE files" + check_pass "Package contains $PACKAGE_SIZE files" fi # ============================================================================= @@ -345,16 +345,16 @@ section "SECURITY CHECKS" echo "→ Checking for sensitive data..." if grep -r "api[_-]key\|secret\|password\|token" src/ dagx-macros/src/ 2>/dev/null | grep -v "^Binary" | grep -v "token for a node" | grep -q .; then - check_fail "Potential sensitive data found in source" + check_fail "Potential sensitive data found in source" else - check_pass "No sensitive data found" + check_pass "No sensitive data found" fi echo "" echo "→ Verifying .gitignore..." [ -f .gitignore ] || check_fail ".gitignore missing" if ! grep -qE "^/?target(/)?$" .gitignore; then - check_fail ".gitignore doesn't exclude target/" + check_fail ".gitignore doesn't exclude target/" fi check_pass ".gitignore looks good" diff --git a/src/builder.rs b/src/builder.rs index 1129442..5708782 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -121,6 +121,7 @@ impl<'a, Tk: Task, Deps> TaskBuilder<'a, Tk, Deps> { /// dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }).await.unwrap(); /// # }; /// ``` + #[allow(private_bounds)] pub fn depends_on(self, deps: D) -> TaskHandle where D: DepsTuple, diff --git a/src/builder/tests.rs b/src/builder/tests.rs index 087250b..e94cfe4 100644 --- a/src/builder/tests.rs +++ b/src/builder/tests.rs @@ -57,18 +57,6 @@ fn test_task_builder_from_conversion() { assert_eq!(handle.id.0, 0); // First task should have ID 0 } -#[test] -fn test_task_builder_depends_on_returns_handle() { - init_tracing(); - let dag = DagRunner::new(); - - let source = dag.add_task(TestTask { value: 10 }); - let dependent = dag.add_task(TestTaskWithInput).depends_on(source); - - // depends_on should return a TaskHandle - let _handle: TaskHandle<_> = dependent; -} - #[test] fn test_task_builder_chain() { let dag = DagRunner::new(); diff --git a/src/cycle_prevention.rs b/src/cycle_prevention.rs deleted file mode 100644 index 6fd2a5f..0000000 --- a/src/cycle_prevention.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! # Compile-Time Cycle Prevention -//! -//! This module documents how the type system prevents cycles in the DAG. -//! **Cycles are impossible via the public API** due to the type-state pattern. -//! -//! ## Why Cycles Can't Happen -//! -//! 1. [`crate::builder::TaskBuilder::depends_on`] **consumes** the builder (takes `self` by value) -//! 2. It returns a [`crate::types::TaskHandle`] - an immutable, opaque reference -//! 3. [`crate::types::TaskHandle`] has **no methods** to add or modify dependencies -//! 4. You can only wire dependencies **once** per task -//! -//! ## The Catch-22 -//! -//! To create a cycle A→B→A, you would need: -//! - Task A to depend on Task B's handle (requires B finalized) -//! - Task B to depend on Task A's handle (requires A finalized) -//! -//! But you can't finalize both before wiring them together! -//! -//! ## Proof by Compilation Failure -//! -//! The following examples demonstrate that cycles fail to compile: -//! -//! ### Proof 1: Self-Loop Prevention via Move Semantics -//! -//! ```compile_fail -//! use dagx::*; -//! -//! struct TaskA; -//! #[dagx::task] -//! impl TaskA { -//! async fn run(&self, input: &i32) -> i32 { -//! input + 1 -//! } -//! } -//! -//! async fn self_loop() { -//! let dag = DagRunner::new(); -//! let a_builder = dag.add_task(TaskA); -//! -//! // ERROR: a_builder moved in the call to depends_on -//! // Can't use 'a_builder' as both the builder and the dependency -//! let a = a_builder.depends_on(&a_builder); -//! } -//! ``` -//! -//! ### Proof 2: TaskHandle is Immutable -//! -//! ```compile_fail,E0599 -//! use dagx::*; -//! -//! struct NoInput; -//! #[dagx::task] -//! impl NoInput { -//! async fn run(&self) -> i32 { 42 } -//! } -//! -//! async fn unit_cycle() { -//! let dag = DagRunner::new(); -//! let a_builder = dag.add_task(NoInput); -//! let b_builder = dag.add_task(NoInput); -//! -//! // We can convert to handles (unit input = no dependencies required) -//! let a_handle: TaskHandle = a_builder.into(); -//! let b_handle: TaskHandle = b_builder.into(); -//! -//! // But TaskHandle has no methods to add dependencies! -//! // ERROR: no method named `depends_on` found for type `TaskHandle` -//! let _ = a_handle.depends_on(&b_handle); -//! } -//! ``` -//! -//! ## Implications -//! -//! Because cycles are impossible at compile-time: -//! - No runtime cycle detection is needed -//! - Topological sort will always succeed -//! - The DAG structure is guaranteed by the type system -//! -//! This is an example of **zero-cost abstraction** - the type system -//! provides safety guarantees without runtime overhead. diff --git a/src/deps/tests.rs b/src/deps/tests.rs index 5ff8006..203c850 100644 --- a/src/deps/tests.rs +++ b/src/deps/tests.rs @@ -17,17 +17,6 @@ impl TestTask { } } -struct TestTaskWithInput; - -#[crate::task] -impl TestTaskWithInput { - #[allow(dead_code)] - #[cfg(not(tarpaulin_include))] - async fn run(input: &i32) -> i32 { - input * 2 - } -} - #[test] fn test_deps_tuple_unit() { // Test lines 20-22 in deps.rs - unit type implementation diff --git a/src/error.rs b/src/error.rs index 53fb983..4f98c55 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,6 +4,7 @@ /// Errors that can occur during DAG construction and execution #[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] pub enum DagError { /// Invalid dependency: task does not exist InvalidDependency { task_id: u32 }, diff --git a/src/error/tests.rs b/src/error/tests.rs index dc2130f..c4ef7e0 100644 --- a/src/error/tests.rs +++ b/src/error/tests.rs @@ -1,6 +1,6 @@ //! Unit tests for error module -use crate::error::{DagError, DagResult}; +use crate::error::DagError; #[test] fn test_dag_error_display_invalid_dependency() { @@ -74,52 +74,3 @@ fn test_dag_error_std_error_impl() { let _ = err_ref.to_string(); assert!(err_ref.source().is_none()); // DagError doesn't chain errors } - -#[test] -fn test_dag_error_equality() { - let err1 = DagError::InvalidDependency { task_id: 42 }; - let err2 = DagError::InvalidDependency { task_id: 42 }; - let err3 = DagError::InvalidDependency { task_id: 99 }; - - assert_eq!(err1, err2); - assert_ne!(err1, err3); -} - -#[test] -fn test_dag_error_clone() { - let err = DagError::TaskPanicked { - task_id: 10, - panic_message: "test panic".to_string(), - }; - - let cloned = err.clone(); - assert_eq!(err, cloned); -} - -#[test] -fn test_dag_error_debug() { - let err = DagError::TypeMismatch { - expected: "i32", - found: "bool", - }; - - let debug_str = format!("{:?}", err); - assert!(debug_str.contains("TypeMismatch")); - assert!(debug_str.contains("expected")); - assert!(debug_str.contains("found")); -} - -#[test] -fn test_dag_result_type_alias() { - // Test that DagResult works as expected - fn returns_dag_result() -> DagResult { - Ok(42) - } - - fn returns_dag_error() -> DagResult { - Err(DagError::ResultNotFound { task_id: 1 }) - } - - assert!(returns_dag_result().is_ok()); - assert!(returns_dag_error().is_err()); -} diff --git a/src/lib.rs b/src/lib.rs index 9730e5c..443e15e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,7 @@ //! # Features //! //! - **Compile-time cycle prevention**: The type system makes cycles **impossible**—no runtime -//! cycle detection needed! See [`cycle_prevention`] module for detailed explanation and proof. +//! cycle detection needed! See [the repository documentation](https://github.com/swaits/dagx/blob/main/docs/CYCLE_PREVENTION.md) for a detailed explanation. //! - **Compile-time type safety**: Dependencies are validated at compile time through the type //! system. The public API is fully type-safe with no runtime type errors. Internal execution //! uses type erasure for heterogeneous task storage, but this is never exposed to users. @@ -482,8 +482,7 @@ //! ``` //! //! **No trait implementations needed!** Works with nested structs, collections, enums, and any -//! other custom type. See [`examples/custom_types.rs`](https://github.com/swaits/dagx/blob/main/examples/custom_types.rs) -//! for a complete example with complex nested types. +//! other custom type. //! //! # Runtime Agnostic //! @@ -764,21 +763,13 @@ //! //! This follows the same zero-cost pattern used by Tokio, Hyper, and other performance-critical //! Rust async libraries. -//! -//! See [`examples/tracing_example.rs`](https://github.com/swaits/dagx/blob/main/examples/tracing_example.rs) -//! for a complete working example. -//! - -#![allow(private_bounds, private_interfaces)] extern crate self as dagx; // Module declarations mod builder; -pub mod cycle_prevention; mod deps; mod error; -mod extract; mod node; mod runner; mod task; @@ -793,7 +784,3 @@ pub use types::{Pending, TaskHandle}; // Re-export the procedural macro pub use dagx_macros::task; - -// Internal testing support - not part of the public API -#[doc(hidden)] -pub use task::task_fn; diff --git a/src/node/tests.rs b/src/node/tests.rs index 9e946d5..f3bc5ae 100644 --- a/src/node/tests.rs +++ b/src/node/tests.rs @@ -68,7 +68,7 @@ async fn test_execute_with_deps_sink_node() { #[tokio::test] async fn test_execute_with_deps_non_sink_node() { - // Test that non-sink nodes send output via channels + // Test that non-sink nodes return their output let node = TypedNode::new(TestTask { value: 42 }); // Execute with senders (non-sink node) diff --git a/src/runner.rs b/src/runner.rs index b70f37b..335de5c 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -10,12 +10,12 @@ use std::collections::{HashMap, HashSet, VecDeque}; use std::future::Future; use std::hash::{BuildHasher, Hasher}; use std::panic::AssertUnwindSafe; -use std::pin::Pin; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, }; +use futures_util::future::BoxFuture; use futures_util::{stream::FuturesUnordered, FutureExt, StreamExt, TryFutureExt}; use parking_lot::Mutex; @@ -28,8 +28,6 @@ use crate::node::{ExecutableNode, TypedNode}; use crate::task::Task; use crate::types::{NodeId, Pending, TaskHandle}; -type BoxFuture<'a, T> = Pin + Send + 'a>>; - // Guard to ensure run_lock is released even on early return or panic struct RunGuard<'a> { lock: &'a AtomicBool, @@ -279,11 +277,6 @@ impl DagRunner { /// - Returns `DagError::ConcurrentExecution` if the DAG is already executing /// - Returns `DagError::TaskPanicked` if any task panics during execution /// - /// # Note on Cycles - /// - /// Cycles are impossible via the public API due to compile-time prevention. - /// See the [`crate::cycle_prevention`] module for details. - /// /// # Examples /// /// ``` @@ -319,8 +312,8 @@ impl DagRunner { #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, spawner)))] pub async fn run(&self, spawner: S) -> DagResult<()> where - S: Fn(BoxFuture<'static, DagResult<(NodeId, Arc)>>) -> F, - F: Future)>>, + S: Fn(BoxFuture<'static, DagResult>>) -> F, + F: Future>>, { #[cfg(feature = "tracing")] info!("starting DAG execution"); @@ -474,14 +467,15 @@ impl DagRunner { .cloned() .collect(); + let inner_future = spawner(node.execute_with_deps(dependencies)); // Spawn the task using the provided spawner let inner_future = async move { - let result = node.execute_with_deps(dependencies).await?; + let result = inner_future.await?; Ok((node_id, result)) }; Some( - AssertUnwindSafe(spawner(Box::pin(inner_future))) + AssertUnwindSafe(inner_future) .catch_unwind() .unwrap_or_else(move |panic_payload| { // Convert panic to error @@ -675,13 +669,11 @@ impl DagRunner { } if !current_layer.is_empty() { - current_layer.sort(); // Deterministic ordering layers.push(current_layer); } } // Cycle detection removed: cycles are impossible via the public API. - // See src/cycle_prevention.rs for proof that the type system prevents cycles. // // The type-state pattern enforces acyclic structure at compile time through: // 1. TaskBuilder::depends_on() consumes the builder (move semantics) diff --git a/src/runner/tests.rs b/src/runner/tests.rs index 950a416..b6b2c4f 100644 --- a/src/runner/tests.rs +++ b/src/runner/tests.rs @@ -1,7 +1,10 @@ //! Unit tests for runner module +use std::collections::HashMap; +use std::hash::Hasher; + use crate::error::DagError; -use crate::runner::DagRunner; +use crate::runner::{DagRunner, PassThroughHasher}; use crate::types::TaskHandle; // Initialize tracing subscriber for tests (idempotent) @@ -168,23 +171,6 @@ async fn test_concurrent_run_protection() { } } -#[test] -fn test_add_task_increments_id() { - let dag = DagRunner::new(); - - let handle1 = dag.add_task(TestTask { value: 1 }); - let handle2 = dag.add_task(TestTask { value: 2 }); - let handle3 = dag.add_task(TestTask { value: 3 }); - - // Node IDs should be sequential - assert_eq!(handle1.id.0, 0); - assert_eq!(handle2.id.0, 1); - assert_eq!(handle3.id.0, 2); - - // Check that nodes were actually added - assert_eq!(dag.nodes.lock().len(), 3); -} - #[tokio::test] async fn test_invalid_node_id_in_get() { init_tracing(); @@ -255,7 +241,18 @@ async fn test_task_panic_in_multi_task_layer() { .await; // The run should fail due to the panic - // Note: Tokio catches panics in spawned tasks, so the behavior depends on the runtime - // This test mainly ensures we don't crash - assert!(result.is_ok() || result.is_err()); + assert!(result.is_err()); +} + +#[test] +fn test_passthrough_hasher() { + let mut passthrough_hashmap = HashMap::with_hasher(PassThroughHasher::default()); + passthrough_hashmap.insert(64u32, "test string"); +} + +#[test] +#[should_panic] +fn test_passthrough_hasher_only_u32() { + let mut passthrough_hasher = PassThroughHasher::default(); + passthrough_hasher.write(&[0; 4]); } diff --git a/src/task.rs b/src/task.rs index 604c2aa..0b40a18 100644 --- a/src/task.rs +++ b/src/task.rs @@ -5,8 +5,6 @@ use std::{any::Any, future::Future, marker::PhantomData, slice::Iter, sync::Arc}; -use crate::extract::ExtractInput; - /// A unit of async work with typed inputs and outputs. /// /// Use the [`task`](crate::task) macro to implement this trait. The macro automatically @@ -149,50 +147,5 @@ impl TaskInput<'static, ()> { } } -/// **INTERNAL USE ONLY**: This struct is for testing purposes only and should -/// not be used in production code. Use the `#[task]` macro instead. -#[doc(hidden)] -pub struct TaskFn -where - for<'input> F: FnMut(I::Retv<'input>) -> O + Send, - I: ExtractInput + Send + Sync + 'static, - O: Send, -{ - f: F, - _phantom: std::marker::PhantomData O>, -} - -impl Task for TaskFn -where - for<'input> F: FnMut(I::Retv<'input>) -> O + Send, - I: ExtractInput + Send + Sync + 'static, - O: Send + Sync, -{ - type Input = I::Input; - type Output = O; - - async fn run(mut self, input: TaskInput<'_, I::Input>) -> O { - (self.f)(I::extract_from_task_input(input).unwrap()) - } -} - -/// Convenience function to create a task from a closure. -/// -/// **INTERNAL USE ONLY**: This function is for testing purposes only and should -/// not be used in production code. Use the `#[task]` macro instead. -#[doc(hidden)] -pub fn task_fn(f: F) -> TaskFn -where - for<'input> F: FnMut(I::Retv<'input>) -> O + Send, - I: ExtractInput + Send + Sync + 'static, - I::Input: Send + Sync + 'static, - O: Send + Sync + 'static, -{ - TaskFn { - f, - _phantom: std::marker::PhantomData, - } -} - #[cfg(test)] mod tests; diff --git a/src/task/tests.rs b/src/task/tests.rs index 99dbc38..1cdb494 100644 --- a/src/task/tests.rs +++ b/src/task/tests.rs @@ -1,99 +1,9 @@ //! Unit tests for task module -use crate::task::{task_fn, Task, TaskInput}; +use crate::task::{Task, TaskInput}; use std::any::Any; -use std::future::Future; -use std::pin::Pin; use std::sync::Arc; -// Test the task_fn function and TaskFn struct -#[tokio::test] -async fn test_task_fn_creation_and_execution() { - // Test line 111 in task.rs - Task implementation for TaskFn - - // Create a simple task function - let task = task_fn::(|input: &i32| input * 2); - - // Test the Task trait implementation - let result = task - .run(TaskInput::new( - [Arc::new(42) as Arc].iter(), - )) - .await; - assert_eq!(result, 84); -} - -#[tokio::test] -async fn test_task_fn_with_unit_input() { - // Test task_fn with unit input - let task = task_fn::<(), _, _>(|_: ()| 100); - - let result = task - .run(TaskInput::new( - [Arc::new(()) as Arc].iter(), - )) - .await; - assert_eq!(result, 100); -} - -#[tokio::test] -async fn test_task_fn_with_tuple_input() { - // Test task_fn with tuple input - let task = task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b); - - let result = task - .run(TaskInput::new( - [ - Arc::new(10) as Arc, - Arc::new(20) as Arc, - ] - .iter(), - )) - .await; - assert_eq!(result, 30); -} - -#[tokio::test] -async fn test_task_fn_with_string_input() { - // Test task_fn with String input - let task = task_fn::(|input: &String| input.len()); - - let test_string = String::from("hello"); - let result = task - .run(TaskInput::new( - [Arc::new(test_string) as Arc].iter(), - )) - .await; - assert_eq!(result, 5); -} - -#[tokio::test] -async fn test_task_fn_with_complex_output() { - // Test task_fn with complex output type - let task = task_fn::(|&input: &i32| vec![input, input * 2, input * 3]); - - let result = task - .run(TaskInput::new( - [Arc::new(5) as Arc].iter(), - )) - .await; - assert_eq!(result, vec![5, 10, 15]); -} - -#[tokio::test] -async fn test_task_fn_stateful_closure() { - // Test that task_fn can capture state - let multiplier = 3; - let task = task_fn::(move |input: &i32| input * multiplier); - - let result = task - .run(TaskInput::new( - [Arc::new(7) as Arc].iter(), - )) - .await; - assert_eq!(result, 21); -} - // Test custom Task implementation struct CustomTask { state: i32, @@ -103,15 +13,9 @@ impl Task for CustomTask { type Input = (i32,); type Output = i32; - #[allow(refining_impl_trait)] - fn run( - mut self, - input: TaskInput<'_, Self::Input>, - ) -> Pin + Send + '_>> { - Box::pin(async move { - self.state += input.next().0; - self.state - }) + async fn run(mut self, input: TaskInput<'_, Self::Input>) -> i32 { + self.state += input.next().0; + self.state } } @@ -130,85 +34,8 @@ async fn test_custom_task_implementation() { // This demonstrates the ownership model } -// Test that Task trait bounds are correct -fn assert_task_bounds() { - // This function just checks that T implements Task with the right bounds - fn assert_send() {} - - // These should compile if bounds are correct - assert_send::(); - assert_send::(); - assert_send::(); -} - #[test] -fn test_task_trait_bounds() { - // Verify that our test tasks meet the Task trait bounds - assert_task_bounds::(); - - // We can't directly test TaskFn because it's a private type and needs a concrete closure type, - // but the fact that task_fn works in the tests above proves it's correct -} - -#[tokio::test] -async fn test_task_fn_single_call() { - // Test that task_fn works (can only call once due to ownership) - let task = task_fn::(|x: &i32| x + 1); - - assert_eq!( - task.run(TaskInput::new( - [Arc::new(1) as Arc].iter() - )) - .await, - 2 - ); -} - -#[tokio::test] -async fn test_task_fn_with_result_type() { - // Test task_fn returning Result - let task = task_fn::(|&x: &i32| { - if x > 0 { - Ok(x * 2) - } else { - Err("negative input") - } - }); - - assert_eq!( - task.run(TaskInput::new( - [Arc::new(5) as Arc].iter() - )) - .await, - Ok(10) - ); -} - -#[tokio::test] -async fn test_task_fn_with_option_type() { - // Test task_fn returning Option - let task = task_fn::(|&x: &i32| if x > 0 { Some(x * 2) } else { None }); - - assert_eq!( - task.run(TaskInput::new( - [Arc::new(5) as Arc].iter() - )) - .await, - Some(10) - ); -} - -#[tokio::test] -async fn test_task_fn_explicit_trait_call() { - // Test explicit Task trait method call to cover line 121 - - let task = task_fn::(|x: &i32| x * 3); - - // Explicitly call the Task trait's run method - let result = Task::run( - task, - TaskInput::new([Arc::new(10) as Arc].iter()), - ) - .await; - assert_eq!(result, 30); +fn test_empty_task_input() { + let empty = TaskInput::empty(); + assert_eq!(empty.inputs.len(), 0); } diff --git a/src/types.rs b/src/types.rs index 2ebf2ae..63f45ad 100644 --- a/src/types.rs +++ b/src/types.rs @@ -7,7 +7,7 @@ use crate::builder::TaskBuilder; use crate::task::Task; /// Opaque node identifier -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub struct NodeId(pub u32); /// Opaque, typed token for a node's output. diff --git a/src/types/coverage_tests.rs b/src/types/coverage_tests.rs index 1095fa1..f81ee93 100644 --- a/src/types/coverage_tests.rs +++ b/src/types/coverage_tests.rs @@ -4,90 +4,6 @@ use crate::{task, DagRunner, TaskHandle}; -#[tokio::test] -async fn test_unit_type_taskbuilder_to_handle_conversion() { - // Test that explicitly uses the From conversion for unit-type tasks - // This covers line 71 in types.rs - - // Unit struct (no fields) - its Input type is () - struct UnitTask; - - #[task] - impl UnitTask { - async fn run(&self) -> i32 { - 42 - } - } - - let dag = DagRunner::new(); - - // This creates a TaskBuilder - // Since UnitTask has Input = (), it can be converted to TaskHandle - let builder = dag.add_task(UnitTask); - - // Explicitly convert to TaskHandle using From implementation - // This triggers the From implementation at line 69-74, - // specifically covering line 71 - let handle: TaskHandle<_> = builder.into(); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // Verify the task ran and we can get its output - assert_eq!(dag.get(handle).unwrap(), 42); -} - -#[tokio::test] -async fn test_unit_type_conversion_with_multiple_tasks() { - // Test conversion with multiple unit-type tasks - - struct FirstTask; - #[task] - impl FirstTask { - async fn run(&self) -> String { - "first".to_string() - } - } - - struct SecondTask; - #[task] - impl SecondTask { - async fn run(&self) -> i32 { - 100 - } - } - - struct ThirdTask; - #[task] - impl ThirdTask { - async fn run(&self) -> bool { - true - } - } - - let dag = DagRunner::new(); - - // Create builders for all tasks - let first_builder = dag.add_task(FirstTask); - let second_builder = dag.add_task(SecondTask); - let third_builder = dag.add_task(ThirdTask); - - // Convert each builder to handle explicitly - let first_handle: TaskHandle = first_builder.into(); - let second_handle: TaskHandle<_> = second_builder.into(); - let third_handle: TaskHandle = third_builder.into(); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // Verify all outputs - assert_eq!(dag.get(first_handle).unwrap(), "first"); - assert_eq!(dag.get(second_handle).unwrap(), 100); - assert!(dag.get(third_handle).unwrap()); -} - #[tokio::test] async fn test_unit_struct_with_state() { // Even structs with fields can have unit input type @@ -124,75 +40,9 @@ async fn test_unit_struct_with_state() { assert_eq!(dag.get(handle).unwrap(), 42); } -#[tokio::test] -async fn test_taskbuilder_conversion_assignment() { - // Test the conversion through assignment (different from .into()) - - struct SimpleTask; - #[task] - impl SimpleTask { - async fn run(&self) -> Vec { - vec![1, 2, 3] - } - } - - let dag = DagRunner::new(); - let builder = dag.add_task(SimpleTask); - - // Conversion through type annotation and assignment - // This also uses From implementation - let handle: TaskHandle> = builder.into(); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - let result = dag.get(handle).unwrap(); - assert_eq!(result, vec![1, 2, 3]); -} - -#[tokio::test] -async fn test_mixed_unit_and_dependent_tasks() { - // Test that unit-type tasks work alongside tasks with dependencies - - struct Source; - #[task] - impl Source { - async fn run(&self) -> i32 { - 10 - } - } - - struct Doubler; - #[task] - impl Doubler { - async fn run(&self, input: &i32) -> i32 { - input * 2 - } - } - - let dag = DagRunner::new(); - - // Source is unit-type, can be converted - let source_builder = dag.add_task(Source); - let source_handle: TaskHandle<_> = source_builder.into(); // Covers line 71 - - // Doubler needs dependencies, so we use it differently - let doubler = dag.add_task(Doubler).depends_on(source_handle); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(source_handle).unwrap(), 10); - assert_eq!(dag.get(doubler).unwrap(), 20); -} - #[test] fn test_taskbuilder_conversion_without_running() { // Test that conversion works even without running the DAG - // This ensures the conversion is purely a type-level operation - struct TestTask; #[task] impl TestTask { @@ -211,54 +61,3 @@ fn test_taskbuilder_conversion_without_running() { // Verify the ID was transferred correctly assert_eq!(handle.id, builder_id); } - -#[test] -fn test_multiple_conversions_preserve_ids() { - // Ensure that multiple conversions maintain correct IDs - - struct Task1; - #[task] - impl Task1 { - async fn run(&self) -> i32 { - 1 - } - } - - struct Task2; - #[task] - impl Task2 { - async fn run(&self) -> i32 { - 2 - } - } - - struct Task3; - #[task] - impl Task3 { - async fn run(&self) -> i32 { - 3 - } - } - - let dag = DagRunner::new(); - - let builder1 = dag.add_task(Task1); - let builder2 = dag.add_task(Task2); - let builder3 = dag.add_task(Task3); - - let id1 = builder1.id; - let id2 = builder2.id; - let id3 = builder3.id; - - // Convert all builders - let handle1: TaskHandle<_> = builder1.into(); - let handle2: TaskHandle<_> = builder2.into(); - let handle3: TaskHandle<_> = builder3.into(); - - // Verify IDs are preserved and sequential - assert_eq!(handle1.id, id1); - assert_eq!(handle2.id, id2); - assert_eq!(handle3.id, id3); - assert_eq!(handle1.id.0 + 1, handle2.id.0); - assert_eq!(handle2.id.0 + 1, handle3.id.0); -} diff --git a/src/types/tests.rs b/src/types/tests.rs index f1ac142..a8717ba 100644 --- a/src/types/tests.rs +++ b/src/types/tests.rs @@ -4,54 +4,19 @@ use crate::types::{NodeId, TaskHandle}; use std::marker::PhantomData; #[test] -fn test_node_id_operations() { - let id1 = NodeId(1); - let id2 = NodeId(2); - let id3 = NodeId(1); - - // Test equality - assert_eq!(id1, id3); - assert_ne!(id1, id2); - - // Test ordering - assert!(id1 < id2); - assert!(id2 > id1); - - // Test Debug - let debug_str = format!("{:?}", id1); - assert!(debug_str.contains("NodeId")); - assert!(debug_str.contains("1")); - - // Test Clone - let cloned = id1; - assert_eq!(id1, cloned); - - // Test Copy - let copied = id1; - assert_eq!(id1, copied); - - // Test Hash (compile-time check that it implements Hash) - use std::collections::HashSet; - let mut set = HashSet::new(); - set.insert(id1); - set.insert(id2); - assert_eq!(set.len(), 2); -} - -#[test] +#[allow(clippy::clone_on_copy)] fn test_task_handle_clone() { let handle: TaskHandle<(i32,)> = TaskHandle { id: NodeId(42), _phantom: PhantomData, }; - // Test Clone implementation (lines 44-46 in types.rs) - let cloned = handle; + // Test Clone implementation + let cloned = handle.clone(); assert_eq!(handle.id.0, cloned.id.0); - // Test that clone is actually a copy (same memory semantics) - let handle2 = handle; - let handle3 = handle2; + let handle2 = handle.clone(); + let handle3 = handle2.clone(); assert_eq!(handle3.id.0, 42); } @@ -69,70 +34,6 @@ fn test_task_handle_copy() { assert_eq!(copied2.id.0, 99); } -#[test] -fn test_task_handle_with_different_types() { - // Test that TaskHandle works with various types - let int_handle: TaskHandle = TaskHandle { - id: NodeId(1), - _phantom: PhantomData, - }; - - let string_handle: TaskHandle = TaskHandle { - id: NodeId(2), - _phantom: PhantomData, - }; - - let vec_handle: TaskHandle> = TaskHandle { - id: NodeId(3), - _phantom: PhantomData, - }; - - // Clone and copy should work for all - let _ = int_handle; - let _ = string_handle; - let _ = vec_handle; - - let _ = int_handle; - let _ = string_handle; - let _ = vec_handle; -} - -#[test] -fn test_task_handle_explicit_clone() { - // Test explicit .clone() call to cover line 47-48 - let handle: TaskHandle = TaskHandle { - id: NodeId(42), - _phantom: PhantomData, - }; - - #[allow(clippy::clone_on_copy)] - let cloned = handle.clone(); - assert_eq!(handle.id.0, cloned.id.0); - assert_eq!(cloned.id.0, 42); -} - -#[test] -fn test_task_builder_from_conversion() { - use crate::runner::DagRunner; - - // Test From for TaskHandle (line 69-74) - struct SimpleTask; - #[crate::task] - impl SimpleTask { - async fn run(&self) -> i32 { - 42 - } - } - - let dag = DagRunner::new(); - let builder = dag.add_task(SimpleTask); - let builder_id = builder.id; - - // This conversion uses the From implementation - let handle: TaskHandle<_> = builder.into(); - assert_eq!(handle.id, builder_id); -} - #[test] fn test_task_handle_ref_from_conversion() { // Test From<&TaskHandle> for TaskHandle (line 90-93) diff --git a/tarpaulin.toml b/tarpaulin.toml new file mode 100644 index 0000000..1956cd5 --- /dev/null +++ b/tarpaulin.toml @@ -0,0 +1,13 @@ + +[test_config] +manifest-path = "./Cargo.toml" +skip-clean = true +engine = "Llvm" +# cargo-tarpaulin already reports tracing lines incorrectly when the feature is turned off, +# and this is exacerbated when building with all-features. +all-features = false +run-types = ["Tests"] +command = "Test" +locked = true +frozen = true +fail-under = 80.0 diff --git a/tests/arc_tests.rs b/tests/arc_tests.rs deleted file mode 100644 index e8079aa..0000000 --- a/tests/arc_tests.rs +++ /dev/null @@ -1,1348 +0,0 @@ -//! Comprehensive tests for Arc-wrapped task outputs -//! -//! These tests validate that Arc works correctly for: -//! - Basic 1:1 pipelines -//! - Fan-out patterns (1:N) -//! - Complex multi-layer DAGs -//! - Performance characteristics -//! - Edge cases and error handling -//! -//! These tests serve as both validation and documentation for proper Arc usage. - -use dagx::{task, DagRunner, TaskHandle}; - -use std::collections::{HashMap, HashSet}; -use std::sync::Arc; -use std::time::{Duration, Instant}; - -// ============================================================================ -// SECTION 1: Basic Arc Usage -// ============================================================================ - -/// Test 1: Basic Arc in 1:1 pipeline - validates Arc can flow through simple pipeline -#[tokio::test] -async fn test_arc_basic_pipeline() { - struct Producer; - - #[task] - impl Producer { - async fn run(&mut self) -> Arc { - Arc::new("Hello, Arc!".to_string()) - } - } - - struct Consumer; - - #[task] - impl Consumer { - async fn run(&mut self, data: &Arc) -> String { - format!("Received: {}", data) - } - } - - let dag = DagRunner::new(); - let producer = dag.add_task(Producer); - let consumer = dag.add_task(Consumer).depends_on(producer); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // Note: producer is not a sink (consumer depends on it), so we can't get() its value - // Only sink nodes (nodes with no dependents) store their outputs for retrieval - assert_eq!(dag.get(consumer).unwrap(), "Received: Hello, Arc!"); -} - -/// Test 2: Arc with primitives - validates Arc works with basic types -#[tokio::test] -async fn test_arc_with_primitives() { - struct I32Producer; - - #[task] - impl I32Producer { - async fn run(&mut self) -> Arc { - Arc::new(42) - } - } - - struct F64Producer; - - #[task] - impl F64Producer { - async fn run(&mut self) -> Arc { - Arc::new(std::f64::consts::PI) - } - } - - struct BoolProducer; - - #[task] - impl BoolProducer { - async fn run(&mut self) -> Arc { - Arc::new(true) - } - } - - struct AllConsumer; - - #[task] - impl AllConsumer { - async fn run(&mut self, i: &Arc, f: &Arc, b: &Arc) -> String { - format!("i={}, f={:.2}, b={}", **i, **f, **b) - } - } - - let dag = DagRunner::new(); - let i32_prod = dag.add_task(I32Producer); - let f64_prod = dag.add_task(F64Producer); - let bool_prod = dag.add_task(BoolProducer); - let consumer = dag - .add_task(AllConsumer) - .depends_on((i32_prod, f64_prod, bool_prod)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // Note: producers are not sinks (consumer depends on them), so we can't get() their values - // The consumer output verifies all three producers worked correctly - assert_eq!(dag.get(consumer).unwrap(), "i=42, f=3.14, b=true"); -} - -/// Test 3: Arc with collections - validates Arc works with Vec, HashMap, HashSet -#[tokio::test] -async fn test_arc_with_collections() { - struct VecProducer; - - #[task] - impl VecProducer { - async fn run(&mut self) -> Arc> { - Arc::new(vec![1, 2, 3, 4, 5]) - } - } - - struct MapProducer; - - #[task] - impl MapProducer { - async fn run(&mut self) -> Arc> { - let mut map = HashMap::new(); - map.insert("one".to_string(), 1); - map.insert("two".to_string(), 2); - map.insert("three".to_string(), 3); - Arc::new(map) - } - } - - struct SetProducer; - - #[task] - impl SetProducer { - async fn run(&mut self) -> Arc> { - let mut set = HashSet::new(); - set.insert("apple".to_string()); - set.insert("banana".to_string()); - set.insert("cherry".to_string()); - Arc::new(set) - } - } - - struct CollectionConsumer; - - #[task] - impl CollectionConsumer { - async fn run( - &mut self, - vec: &Arc>, - map: &Arc>, - set: &Arc>, - ) -> String { - format!( - "Vec sum: {}, Map size: {}, Set size: {}", - vec.iter().sum::(), - map.len(), - set.len() - ) - } - } - - let dag = DagRunner::new(); - let vec_prod = dag.add_task(VecProducer); - let map_prod = dag.add_task(MapProducer); - let set_prod = dag.add_task(SetProducer); - let consumer = dag - .add_task(CollectionConsumer) - .depends_on((vec_prod, map_prod, set_prod)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // Note: producers are not sinks (consumer depends on them), so we can't get() their values - // The consumer output verifies all three collection producers worked correctly - assert_eq!( - dag.get(consumer).unwrap(), - "Vec sum: 15, Map size: 3, Set size: 3" - ); -} - -/// Test 4: Arc with custom structs - validates Arc works with user-defined types -#[tokio::test] -async fn test_arc_with_custom_structs() { - #[derive(Debug, Clone, PartialEq)] - struct Person { - name: String, - age: u32, - emails: Vec, - } - - struct PersonProducer; - - #[task] - impl PersonProducer { - async fn run(&mut self) -> Arc { - Arc::new(Person { - name: "Alice".to_string(), - age: 30, - emails: vec![ - "alice@example.com".to_string(), - "alice@work.com".to_string(), - ], - }) - } - } - - struct NameExtractor; - - #[task] - impl NameExtractor { - async fn run(&mut self, person: &Arc) -> String { - person.name.clone() - } - } - - struct AgeExtractor; - - #[task] - impl AgeExtractor { - async fn run(&mut self, person: &Arc) -> u32 { - person.age - } - } - - struct EmailCountExtractor; - - #[task] - impl EmailCountExtractor { - async fn run(&mut self, person: &Arc) -> usize { - person.emails.len() - } - } - - let dag = DagRunner::new(); - let person_prod: TaskHandle<_> = dag.add_task(PersonProducer).into(); - let name_ext = dag.add_task(NameExtractor).depends_on(person_prod); - let age_ext = dag.add_task(AgeExtractor).depends_on(person_prod); - let email_ext = dag.add_task(EmailCountExtractor).depends_on(person_prod); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // Note: person_prod is not a sink (3 extractors depend on it), so we can't get() its value - // The extractor outputs verify the person was created and shared correctly - assert_eq!(dag.get(name_ext).unwrap(), "Alice"); - assert_eq!(dag.get(age_ext).unwrap(), 30); - assert_eq!(dag.get(email_ext).unwrap(), 2); -} - -// ============================================================================ -// SECTION 2: Fan-Out Patterns (CRITICAL!) -// ============================================================================ - -/// Test 5: 1:3 fan-out - validates efficient Arc sharing among multiple consumers -#[tokio::test] -async fn test_arc_fan_out_three_consumers() { - struct DataProducer; - - #[task] - impl DataProducer { - async fn run(&mut self) -> Arc> { - Arc::new(vec![ - "item1".to_string(), - "item2".to_string(), - "item3".to_string(), - "item4".to_string(), - "item5".to_string(), - ]) - } - } - - struct CountConsumer; - - #[task] - impl CountConsumer { - async fn run(&mut self, data: &Arc>) -> usize { - data.len() - } - } - - struct FirstConsumer; - - #[task] - impl FirstConsumer { - async fn run(&mut self, data: &Arc>) -> String { - data.first().unwrap_or(&String::new()).clone() - } - } - - struct JoinConsumer; - - #[task] - impl JoinConsumer { - async fn run(&mut self, data: &Arc>) -> String { - data.join(", ") - } - } - - let dag = DagRunner::new(); - let producer: TaskHandle<_> = dag.add_task(DataProducer).into(); - let c1 = dag.add_task(CountConsumer).depends_on(producer); - let c2 = dag.add_task(FirstConsumer).depends_on(producer); - let c3 = dag.add_task(JoinConsumer).depends_on(producer); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(c1).unwrap(), 5); - assert_eq!(dag.get(c2).unwrap(), "item1"); - assert_eq!(dag.get(c3).unwrap(), "item1, item2, item3, item4, item5"); -} - -/// Test 6: 1:5 fan-out with large data - proves Arc is more efficient than cloning -#[tokio::test] -async fn test_arc_fan_out_five_consumers_large_data() { - struct LargeDataProducer; - - #[task] - impl LargeDataProducer { - async fn run(&mut self) -> Arc> { - Arc::new((0..10_000).map(|i| format!("Item {}", i)).collect()) - } - } - - struct First100Analyzer; - - #[task] - impl First100Analyzer { - async fn run(&mut self, data: &Arc>) -> usize { - data.iter().take(100).map(|s| s.len()).sum() - } - } - - struct Last100Analyzer; - - #[task] - impl Last100Analyzer { - async fn run(&mut self, data: &Arc>) -> usize { - data.iter().rev().take(100).map(|s| s.len()).sum() - } - } - - struct LongestFinder; - - #[task] - impl LongestFinder { - async fn run(&mut self, data: &Arc>) -> usize { - data.iter().map(|s| s.len()).max().unwrap_or(0) - } - } - - struct PatternCounter; - - #[task] - impl PatternCounter { - async fn run(&mut self, data: &Arc>) -> usize { - data.iter().filter(|s| s.contains("999")).count() - } - } - - struct TotalBytesComputer; - - #[task] - impl TotalBytesComputer { - async fn run(&mut self, data: &Arc>) -> usize { - data.iter().map(|s| s.len()).sum() - } - } - - let dag = DagRunner::new(); - let producer: TaskHandle<_> = dag.add_task(LargeDataProducer).into(); - let c1 = dag.add_task(First100Analyzer).depends_on(producer); - let c2 = dag.add_task(Last100Analyzer).depends_on(producer); - let c3 = dag.add_task(LongestFinder).depends_on(producer); - let c4 = dag.add_task(PatternCounter).depends_on(producer); - let c5 = dag.add_task(TotalBytesComputer).depends_on(producer); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // Note: producer is not a sink (has 5 consumers), so we can't get() its value - // We verify correctness through the consumer outputs instead - - // Verify each consumer got correct results - assert!(dag.get(c1).unwrap() > 0); - assert!(dag.get(c2).unwrap() > 0); - assert_eq!(dag.get(c3).unwrap(), 9); // "Item 9999" has 9 chars - assert_eq!(dag.get(c4).unwrap(), 19); // Items containing "999": 999, 1999..8999, 9990-9999 - assert!(dag.get(c5).unwrap() > 0); -} - -/// Test 7: Nested fan-out - Arc propagated through tree of tasks -#[tokio::test] -async fn test_arc_nested_fan_out_tree() { - struct RootProducer; - - #[task] - impl RootProducer { - async fn run(&mut self) -> Arc> { - Arc::new(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) - } - } - - struct Level1Sum; - - #[task] - impl Level1Sum { - async fn run(&mut self, data: &Arc>) -> Arc { - Arc::new(data.iter().sum()) - } - } - - struct Level1Product; - - #[task] - impl Level1Product { - async fn run(&mut self, data: &Arc>) -> Arc { - Arc::new(data.iter().take(5).product()) - } - } - - struct Level2Double; - - #[task] - impl Level2Double { - async fn run(&mut self, sum: &Arc) -> i32 { - **sum * 2 - } - } - - struct Level2Square; - - #[task] - impl Level2Square { - async fn run(&mut self, sum: &Arc) -> i32 { - **sum * **sum - } - } - - struct Level2Negate; - - #[task] - impl Level2Negate { - async fn run(&mut self, product: &Arc) -> i32 { - -**product - } - } - - struct Level2Mod10; - - #[task] - impl Level2Mod10 { - async fn run(&mut self, product: &Arc) -> i32 { - **product % 10 - } - } - - let dag = DagRunner::new(); - let root: TaskHandle<_> = dag.add_task(RootProducer).into(); - let l1_sum = dag.add_task(Level1Sum).depends_on(root); - let l1_prod = dag.add_task(Level1Product).depends_on(root); - let l2_double = dag.add_task(Level2Double).depends_on(l1_sum); - let l2_square = dag.add_task(Level2Square).depends_on(l1_sum); - let l2_negate = dag.add_task(Level2Negate).depends_on(l1_prod); - let l2_mod = dag.add_task(Level2Mod10).depends_on(l1_prod); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // Note: l1_sum and l1_prod are not sinks (they have level 2 tasks depending on them) - // Only the level 2 tasks are sinks and can be retrieved - assert_eq!(dag.get(l2_double).unwrap(), 110); // 55 * 2 - assert_eq!(dag.get(l2_square).unwrap(), 3025); // 55 * 55 - assert_eq!(dag.get(l2_negate).unwrap(), -120); // -120 - assert_eq!(dag.get(l2_mod).unwrap(), 0); // 120 % 10 -} - -/// Test 8: Performance benchmark - Arc vs Clone for large data -#[tokio::test] -async fn test_arc_performance_vs_clone() { - const DATA_SIZE: usize = 100_000; - - struct LargeProducer; - - #[task] - impl LargeProducer { - async fn run(&mut self) -> Arc> { - Arc::new( - (0..DATA_SIZE) - .map(|i| format!("This is a relatively long string number {}", i)) - .collect(), - ) - } - } - - struct Process1; - - #[task] - impl Process1 { - async fn run(&mut self, data: &Arc>) -> usize { - tokio::time::sleep(Duration::from_millis(10)).await; - data.len() - } - } - - struct Process2; - - #[task] - impl Process2 { - async fn run(&mut self, data: &Arc>) -> usize { - tokio::time::sleep(Duration::from_millis(10)).await; - data.iter().filter(|s| s.contains("5")).count() - } - } - - struct Process3; - - #[task] - impl Process3 { - async fn run(&mut self, data: &Arc>) -> usize { - tokio::time::sleep(Duration::from_millis(10)).await; - data.iter().map(|s| s.len()).sum() - } - } - - let dag = DagRunner::new(); - let producer: TaskHandle<_> = dag.add_task(LargeProducer).into(); - let p1 = dag.add_task(Process1).depends_on(producer); - let p2 = dag.add_task(Process2).depends_on(producer); - let p3 = dag.add_task(Process3).depends_on(producer); - - let start = Instant::now(); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - let elapsed = start.elapsed(); - - // Verify results - assert_eq!(dag.get(p1).unwrap(), DATA_SIZE); - assert!(dag.get(p2).unwrap() > 0); - assert!(dag.get(p3).unwrap() > 0); - - // With Arc, this should complete quickly even with large data - assert!( - elapsed < Duration::from_secs(2), - "Arc fan-out was too slow: {:?}", - elapsed - ); -} - -// ============================================================================ -// SECTION 3: Complex Scenarios -// ============================================================================ - -/// Test 9: Arc with Result - validates Arc> and Result, E> -#[tokio::test] -async fn test_arc_with_result() { - struct ArcResultOk; - - #[task] - impl ArcResultOk { - async fn run(&mut self) -> Arc> { - Arc::new(Ok("Success".to_string())) - } - } - - struct ResultArcOk; - - #[task] - impl ResultArcOk { - async fn run(&mut self) -> Result, String> { - Ok(Arc::new("Success".to_string())) - } - } - - struct ArcResultErr; - - #[task] - impl ArcResultErr { - async fn run(&mut self) -> Arc> { - Arc::new(Err("Error".to_string())) - } - } - - struct ArcResultConsumer; - - #[task] - impl ArcResultConsumer { - async fn run(&mut self, data: &Arc>) -> String { - match data.as_ref() { - Ok(s) => format!("Got Ok: {}", s), - Err(e) => format!("Got Err: {}", e), - } - } - } - - struct ResultArcConsumer; - - #[task] - impl ResultArcConsumer { - async fn run(&mut self, data: &Result, String>) -> String { - match data { - Ok(arc) => format!("Got Arc: {}", arc), - Err(e) => format!("Got Error: {}", e), - } - } - } - - let dag = DagRunner::new(); - let arc_result_ok = dag.add_task(ArcResultOk); - let result_arc_ok = dag.add_task(ResultArcOk); - let arc_result_err = dag.add_task(ArcResultErr); - - let consumer1 = dag.add_task(ArcResultConsumer).depends_on(arc_result_ok); - let consumer2 = dag.add_task(ResultArcConsumer).depends_on(result_arc_ok); - - // Create a new instance for the error case - struct ArcResultConsumer2; - - #[task] - impl ArcResultConsumer2 { - async fn run(&mut self, data: &Arc>) -> String { - match data.as_ref() { - Ok(s) => format!("Got Ok: {}", s), - Err(e) => format!("Got Err: {}", e), - } - } - } - - let consumer3 = dag.add_task(ArcResultConsumer2).depends_on(arc_result_err); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(consumer1).unwrap(), "Got Ok: Success"); - assert_eq!(dag.get(consumer2).unwrap(), "Got Arc: Success"); - assert_eq!(dag.get(consumer3).unwrap(), "Got Err: Error"); -} - -/// Test 10: Arc with Option - validates Arc> and Option> -#[tokio::test] -async fn test_arc_with_option() { - struct ArcOptionSome; - - #[task] - impl ArcOptionSome { - async fn run(&mut self) -> Arc> { - Arc::new(Some(42)) - } - } - - struct ArcOptionNone; - - #[task] - impl ArcOptionNone { - async fn run(&mut self) -> Arc> { - Arc::new(None) - } - } - - struct OptionArcSome; - - #[task] - impl OptionArcSome { - async fn run(&mut self) -> Option> { - Some(Arc::new(100)) - } - } - - struct OptionArcNone; - - #[task] - impl OptionArcNone { - async fn run(&mut self) -> Option> { - None - } - } - - struct ArcOptionConsumer1; - - #[task] - impl ArcOptionConsumer1 { - async fn run(&mut self, data: &Arc>) -> String { - match **data { - Some(v) => format!("Some({})", v), - None => "None".to_string(), - } - } - } - - struct ArcOptionConsumer2; - - #[task] - impl ArcOptionConsumer2 { - async fn run(&mut self, data: &Arc>) -> String { - match **data { - Some(v) => format!("Some({})", v), - None => "None".to_string(), - } - } - } - - struct OptionArcConsumer1; - - #[task] - impl OptionArcConsumer1 { - async fn run(&mut self, data: &Option>) -> String { - match data { - Some(arc) => format!("Arc({})", **arc), - None => "No Arc".to_string(), - } - } - } - - struct OptionArcConsumer2; - - #[task] - impl OptionArcConsumer2 { - async fn run(&mut self, data: &Option>) -> String { - match data { - Some(arc) => format!("Arc({})", **arc), - None => "No Arc".to_string(), - } - } - } - - let dag = DagRunner::new(); - let arc_opt_some = dag.add_task(ArcOptionSome); - let arc_opt_none = dag.add_task(ArcOptionNone); - let opt_arc_some = dag.add_task(OptionArcSome); - let opt_arc_none = dag.add_task(OptionArcNone); - - let c1 = dag.add_task(ArcOptionConsumer1).depends_on(arc_opt_some); - let c2 = dag.add_task(ArcOptionConsumer2).depends_on(arc_opt_none); - let c3 = dag.add_task(OptionArcConsumer1).depends_on(opt_arc_some); - let c4 = dag.add_task(OptionArcConsumer2).depends_on(opt_arc_none); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(c1).unwrap(), "Some(42)"); - assert_eq!(dag.get(c2).unwrap(), "None"); - assert_eq!(dag.get(c3).unwrap(), "Arc(100)"); - assert_eq!(dag.get(c4).unwrap(), "No Arc"); -} - -/// Test 11: Diamond dependency - Arc used in diamond pattern -#[tokio::test] -async fn test_arc_diamond_dependency() { - struct Top; - - #[task] - impl Top { - async fn run(&mut self) -> Arc> { - Arc::new(vec![1, 2, 3, 4, 5]) - } - } - - struct LeftBranch; - - #[task] - impl LeftBranch { - async fn run(&mut self, data: &Arc>) -> Arc { - Arc::new(data.iter().sum()) - } - } - - struct RightBranch; - - #[task] - impl RightBranch { - async fn run(&mut self, data: &Arc>) -> Arc { - Arc::new(data.iter().product()) - } - } - - struct Bottom; - - #[task] - impl Bottom { - async fn run(&mut self, left: &Arc, right: &Arc) -> String { - format!("Sum: {}, Product: {}", **left, **right) - } - } - - let dag = DagRunner::new(); - let top: TaskHandle<_> = dag.add_task(Top).into(); - let left = dag.add_task(LeftBranch).depends_on(top); - let right = dag.add_task(RightBranch).depends_on(top); - let bottom = dag.add_task(Bottom).depends_on((&left, &right)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(bottom).unwrap(), "Sum: 15, Product: 120"); -} - -// ============================================================================ -// SECTION 4: Edge Cases -// ============================================================================ - -/// Test 12: Empty Arc - Arc> with empty Vec -#[tokio::test] -async fn test_arc_empty_collections() { - struct EmptyVec; - - #[task] - impl EmptyVec { - async fn run(&mut self) -> Arc> { - Arc::new(Vec::new()) - } - } - - struct EmptyString; - - #[task] - impl EmptyString { - async fn run(&mut self) -> Arc { - Arc::new(String::new()) - } - } - - struct EmptyHashMap; - - #[task] - impl EmptyHashMap { - async fn run(&mut self) -> Arc> { - Arc::new(HashMap::new()) - } - } - - struct EmptyConsumer; - - #[task] - impl EmptyConsumer { - async fn run( - &mut self, - vec: &Arc>, - string: &Arc, - map: &Arc>, - ) -> String { - format!( - "Vec empty: {}, String empty: {}, Map empty: {}", - vec.is_empty(), - string.is_empty(), - map.is_empty() - ) - } - } - - let dag = DagRunner::new(); - let vec_task = dag.add_task(EmptyVec); - let string_task = dag.add_task(EmptyString); - let map_task = dag.add_task(EmptyHashMap); - let consumer = dag - .add_task(EmptyConsumer) - .depends_on((vec_task, string_task, map_task)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!( - dag.get(consumer).unwrap(), - "Vec empty: true, String empty: true, Map empty: true" - ); -} - -/// Test 13: Very large Arc - Arc> with 100K elements -#[tokio::test] -async fn test_arc_very_large_data() { - const SIZE: usize = 100_000; - - struct HugeProducer; - - #[task] - impl HugeProducer { - async fn run(&mut self) -> Arc> { - Arc::new( - (0..SIZE) - .map(|i| format!("Item number {} with some additional text", i)) - .collect(), - ) - } - } - - struct ItemCounter; - - #[task] - impl ItemCounter { - async fn run(&mut self, data: &Arc>) -> usize { - data.len() - } - } - - struct FirstSampler; - - #[task] - impl FirstSampler { - async fn run(&mut self, data: &Arc>) -> String { - data.first().cloned().unwrap_or_default() - } - } - - struct LastSampler; - - #[task] - impl LastSampler { - async fn run(&mut self, data: &Arc>) -> String { - data.last().cloned().unwrap_or_default() - } - } - - struct MiddleSampler; - - #[task] - impl MiddleSampler { - async fn run(&mut self, data: &Arc>) -> String { - data.get(SIZE / 2).cloned().unwrap_or_default() - } - } - - let dag = DagRunner::new(); - let producer: TaskHandle<_> = dag.add_task(HugeProducer).into(); - let counter = dag.add_task(ItemCounter).depends_on(producer); - let first = dag.add_task(FirstSampler).depends_on(producer); - let last = dag.add_task(LastSampler).depends_on(producer); - let middle = dag.add_task(MiddleSampler).depends_on(producer); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(counter).unwrap(), SIZE); - assert!(dag.get(first).unwrap().contains("Item number 0")); - assert!(dag.get(last).unwrap().contains("Item number 99999")); - assert!(dag.get(middle).unwrap().contains("Item number 50000")); -} - -/// Test 14: Arc of Arc - Arc> (unusual but should work) -#[tokio::test] -async fn test_arc_of_arc() { - struct NestedProducer; - - #[task] - impl NestedProducer { - #[allow(clippy::arc_with_non_send_sync)] - async fn run(&mut self) -> Arc> { - Arc::new(Arc::new("Nested Arc".to_string())) - } - } - - struct NestedConsumer; - - #[task] - impl NestedConsumer { - #[allow(clippy::redundant_allocation)] - async fn run(&mut self, data: &Arc>) -> String { - format!("Value: {}", ***data) - } - } - - let dag = DagRunner::new(); - let producer = dag.add_task(NestedProducer); - let consumer = dag.add_task(NestedConsumer).depends_on(producer); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(consumer).unwrap(), "Value: Nested Arc"); -} - -/// Test 15: Mixed types - Some tasks use Arc, others don't -#[tokio::test] -async fn test_arc_mixed_with_non_arc() { - struct ArcProducer; - - #[task] - impl ArcProducer { - async fn run(&mut self) -> Arc { - Arc::new("Arc data".to_string()) - } - } - - struct NonArcProducer; - - #[task] - impl NonArcProducer { - async fn run(&mut self) -> String { - "Non-Arc data".to_string() - } - } - - struct MixedConsumer; - - #[task] - impl MixedConsumer { - async fn run(&mut self, arc_data: &Arc, non_arc_data: &String) -> String { - format!("Arc: {}, Non-Arc: {}", arc_data, non_arc_data) - } - } - - struct Unwrapper; - - #[task] - impl Unwrapper { - async fn run(&mut self, data: &Arc) -> String { - format!("Unwrapped: {}", **data) - } - } - - struct Wrapper; - - #[task] - impl Wrapper { - async fn run(&mut self, data: &String) -> Arc { - Arc::new(format!("Wrapped: {}", data)) - } - } - - let dag = DagRunner::new(); - let arc_prod = dag.add_task(ArcProducer).into(); - let non_arc_prod = dag.add_task(NonArcProducer).into(); - let mixed = dag - .add_task(MixedConsumer) - .depends_on((&arc_prod, &non_arc_prod)); - let unwrap = dag.add_task(Unwrapper).depends_on(arc_prod); - let wrap = dag.add_task(Wrapper).depends_on(non_arc_prod); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!( - dag.get(mixed).unwrap(), - "Arc: Arc data, Non-Arc: Non-Arc data" - ); - assert_eq!(dag.get(unwrap).unwrap(), "Unwrapped: Arc data"); - assert_eq!(*dag.get(wrap).unwrap(), "Wrapped: Non-Arc data"); -} - -// ============================================================================ -// SECTION 5: Advanced Usage -// ============================================================================ - -/// Test 16: Arc in parallel execution paths -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -#[cfg_attr(tarpaulin, ignore)] // Timing tests are unreliable with instrumentation overhead -#[cfg(not(target_os = "macos"))] // macOS CI runners are too slow and unreliable for timing tests -async fn test_arc_parallel_execution() { - struct SharedProducer; - - #[task] - impl SharedProducer { - async fn run(&mut self) -> Arc> { - Arc::new(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) - } - } - - struct ParallelTask1; - - #[task] - impl ParallelTask1 { - async fn run(&mut self, data: &Arc>) -> i32 { - tokio::time::sleep(Duration::from_millis(50)).await; - data.iter().sum() - } - } - - struct ParallelTask2; - - #[task] - impl ParallelTask2 { - async fn run(&mut self, data: &Arc>) -> i32 { - tokio::time::sleep(Duration::from_millis(50)).await; - data.iter().product() - } - } - - struct ParallelTask3; - - #[task] - impl ParallelTask3 { - async fn run(&mut self, data: &Arc>) -> i32 { - tokio::time::sleep(Duration::from_millis(50)).await; - data.len() as i32 - } - } - - let dag = DagRunner::new(); - let producer: TaskHandle<_> = dag.add_task(SharedProducer).into(); - let t1 = dag.add_task(ParallelTask1).depends_on(producer); - let t2 = dag.add_task(ParallelTask2).depends_on(producer); - let t3 = dag.add_task(ParallelTask3).depends_on(producer); - - let start = Instant::now(); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - let elapsed = start.elapsed(); - - // If running in parallel, should take ~50ms, not 150ms - // Allow significant overhead for slower CI runners (especially macOS which can be very slow) - // Serial execution would take 150ms+, so 200ms threshold still proves parallelism - assert!( - elapsed < Duration::from_millis(200), - "Tasks didn't run in parallel: {:?}", - elapsed - ); - - assert_eq!(dag.get(t1).unwrap(), 55); - assert_eq!(dag.get(t2).unwrap(), 3628800); - assert_eq!(dag.get(t3).unwrap(), 10); -} - -/// Test 17: Arc reference counting verification -#[tokio::test] -async fn test_arc_reference_counting() { - struct RefCountProducer; - - #[task] - impl RefCountProducer { - async fn run(&mut self) -> Arc { - let arc = Arc::new("Reference counted".to_string()); - // At creation, strong count should be 1 - assert_eq!(Arc::strong_count(&arc), 1); - arc - } - } - - struct RefCountChecker; - - #[task] - impl RefCountChecker { - async fn run(&mut self, data: &Arc) -> usize { - // When passed to task, count increases due to cloning - Arc::strong_count(data) - } - } - - let dag = DagRunner::new(); - let producer = dag.add_task(RefCountProducer); - let checker = dag.add_task(RefCountChecker).depends_on(producer); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // Note: producer is not a sink (checker depends on it), so we can't get() its value - // The checker task verifies the Arc was passed correctly - let count = dag.get(checker).unwrap(); - assert!(count > 0); // Should be at least 1 -} - -// ============================================================================ -// SECTION 6: Performance Comparison - Arc vs Clone with Massive Fan-Out -// ============================================================================ - -/// Test 18: Performance comparison - Arc vs Clone with optimized fan-out -/// -/// This test demonstrates the critical performance benefit of using Arc for fan-out patterns. -/// It creates two identical DAG structures: -/// 1. WITHOUT Arc: 500KB Vec cloned to 60 downstream tasks (30MB total copied) -/// 2. WITH Arc: Arc> shared with 60 downstream tasks (only pointer copies) -/// -/// The Arc version should be measurably faster due to avoiding deep copies, -/// while keeping test execution well under 1 second for fast CI/CD cycles. -/// -/// Optimized from original 2MB×200 (400MB) to maintain test effectiveness -/// while reducing execution time from 6.5s to under 0.5s. -#[tokio::test] -async fn test_arc_massive_fanout_performance_comparison() { - const DATA_SIZE: usize = 500_000; // 500KB - large enough to show meaningful difference - const FAN_OUT_COUNT: usize = 60; // 60 consumers - balanced for sub-1s execution - - // Generate 500KB of pseudo-random data - let random_data: Vec = (0..DATA_SIZE).map(|i| (i % 256) as u8).collect(); - - // ======================================== - // Scenario 1: WITHOUT Arc (expensive cloning) - // ======================================== - - struct ProducerWithoutArc { - data: Vec, - } - - #[task] - impl ProducerWithoutArc { - async fn run(&mut self) -> Vec { - self.data.clone() // Returns the Vec directly - } - } - - struct ConsumerWithoutArc; - - #[task] - impl ConsumerWithoutArc { - #[allow(clippy::ptr_arg)] - async fn run(&mut self, data: &Vec) -> usize { - // Simulate some work: compute checksum - data.iter().map(|&b| b as usize).sum::() % 1000 - } - } - - let dag_without_arc = DagRunner::new(); - let producer: TaskHandle<_> = dag_without_arc - .add_task(ProducerWithoutArc { - data: random_data.clone(), - }) - .into(); - - // Create consumer tasks for fan-out pattern - let consumers_without_arc: Vec<_> = (0..FAN_OUT_COUNT) - .map(|_| { - dag_without_arc - .add_task(ConsumerWithoutArc) - .depends_on(producer) - }) - .collect(); - - let start_without_arc = Instant::now(); - dag_without_arc - .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - let duration_without_arc = start_without_arc.elapsed(); - - // Verify all consumers completed - for consumer in &consumers_without_arc { - let result = dag_without_arc.get(*consumer).unwrap(); - assert!(result < 1000); // Checksum should be valid - } - - // ======================================== - // Scenario 2: WITH Arc (efficient sharing) - // ======================================== - - struct ProducerWithArc { - data: Vec, - } - - #[task] - impl ProducerWithArc { - async fn run(&mut self) -> Arc> { - Arc::new(self.data.clone()) // Wraps Vec in Arc - } - } - - struct ConsumerWithArc; - - #[task] - impl ConsumerWithArc { - async fn run(&mut self, data: &Arc>) -> usize { - // Same work as before: compute checksum - data.iter().map(|&b| b as usize).sum::() % 1000 - } - } - - let dag_with_arc = DagRunner::new(); - let producer_arc: TaskHandle<_> = dag_with_arc - .add_task(ProducerWithArc { - data: random_data.clone(), - }) - .into(); - - // Create consumer tasks for fan-out pattern - let consumers_with_arc: Vec<_> = (0..FAN_OUT_COUNT) - .map(|_| { - dag_with_arc - .add_task(ConsumerWithArc) - .depends_on(producer_arc) - }) - .collect(); - - let start_with_arc = Instant::now(); - dag_with_arc - .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - let duration_with_arc = start_with_arc.elapsed(); - - // Verify all consumers completed - for consumer in &consumers_with_arc { - let result = dag_with_arc.get(*consumer).unwrap(); - assert!(result < 1000); // Checksum should be valid - } - - // ======================================== - // Performance Comparison - // ======================================== - - println!("\n=== Performance Comparison: Arc vs Clone ==="); - println!("Data size: {} bytes ({}KB)", DATA_SIZE, DATA_SIZE / 1000); - println!("Fan-out count: {} consumers", FAN_OUT_COUNT); - println!( - "Total data copied without Arc: {:.1} MB", - (DATA_SIZE * FAN_OUT_COUNT) as f64 / 1_000_000.0 - ); - println!( - "Total data copied with Arc: ~{} bytes (just pointers)", - FAN_OUT_COUNT * std::mem::size_of::() - ); - println!("\nWithout Arc (cloning): {:?}", duration_without_arc); - println!("With Arc (sharing): {:?}", duration_with_arc); - - if duration_without_arc > duration_with_arc { - let speedup = duration_without_arc.as_secs_f64() / duration_with_arc.as_secs_f64(); - println!("Speedup: {:.2}x faster with Arc", speedup); - } - - // Assert that Arc version is faster (or at least not slower) - // We allow some variance due to system conditions, but Arc should generally be faster - // With 500KB × 60 clones (30MB total), the difference should be clearly measurable - assert!( - duration_with_arc <= duration_without_arc * 2, // Allow 2x slower in worst case (e.g., cold start) - "Arc version should not be significantly slower. Without Arc: {:?}, With Arc: {:?}", - duration_without_arc, - duration_with_arc - ); - - // In most cases, Arc should be noticeably faster - // This is a soft assertion - we expect it to pass most of the time - if duration_with_arc < duration_without_arc { - println!("✓ Arc version is faster as expected"); - } else { - println!("⚠ Arc version was not faster this run (system variance), but still acceptable"); - } -} diff --git a/tests/boundaries/deep_chains.rs b/tests/boundaries/deep_chains.rs index 8ef1dde..a6bb29e 100644 --- a/tests/boundaries/deep_chains.rs +++ b/tests/boundaries/deep_chains.rs @@ -1,70 +1,11 @@ //! Tests for extremely deep dependency chains -use crate::common::task_fn; use dagx::{DagResult, DagRunner}; +use dagx_test::task_fn; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; -#[tokio::test] -async fn test_1000_level_deep_chain() -> DagResult<()> { - // Test a linear chain of 1000 tasks - let dag = DagRunner::new(); - - let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0)); - let mut current = first.into(); // Convert to TaskHandle - - for _i in 1..1000 { - current = dag - .add_task(task_fn::(move |&prev: &i32| prev + 1)) - .depends_on(current); - } - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(dag.get(current)?, 999); - Ok(()) -} - -#[tokio::test] -async fn test_deep_chain_with_branches() -> DagResult<()> { - // Test a deep chain where each node has a side branch - let dag = DagRunner::new(); - - let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0)); - let mut main_chain: dagx::TaskHandle = first.into(); - let mut branches = Vec::new(); - - for i in 1..500 { - // Main chain continues - main_chain = dag - .add_task(task_fn::(move |&prev: &i32| prev + 1)) - .depends_on(main_chain); - - // Create a branch at every 10th node - if i % 10 == 0 { - let branch = dag - .add_task(task_fn::(move |main_val: &i32| main_val * 2)) - .depends_on(main_chain); - branches.push(branch); - } - } - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(dag.get(main_chain)?, 499); - - // Check some branches - if !branches.is_empty() { - assert_eq!(dag.get(branches[0])?, 20); // 10 * 2 - assert_eq!(dag.get(branches[4])?, 100); // 50 * 2 - } - - Ok(()) -} - #[tokio::test] async fn test_multiple_parallel_deep_chains() -> DagResult<()> { // Test 10 parallel chains of depth 500 each @@ -223,30 +164,6 @@ async fn test_fibonacci_chain() -> DagResult<()> { Ok(()) } -#[tokio::test] -async fn test_deep_chain_with_accumulator() -> DagResult<()> { - // Test a deep chain where each node accumulates a value - let dag = DagRunner::new(); - - // Use a simpler accumulation pattern - just sum - let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0i64)); - let mut current: dagx::TaskHandle = first.into(); - - for i in 1..=200 { - current = dag - .add_task(task_fn::(move |sum: &i64| sum + i as i64)) - .depends_on(current); - } - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - let result = dag.get(current)?; - assert_eq!(result, 20100); // sum of 1..=200 - - Ok(()) -} - #[tokio::test] async fn test_zigzag_dependencies() -> DagResult<()> { // Test a zigzag pattern: two chains that cross-reference each other @@ -285,25 +202,3 @@ async fn test_zigzag_dependencies() -> DagResult<()> { Ok(()) } - -#[tokio::test] -#[cfg_attr(tarpaulin, ignore)] // This test is extremely resource intensive -async fn test_10000_level_chain_stress() -> DagResult<()> { - // Ultimate deep chain test - let dag = DagRunner::new(); - - let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0u64)); - let mut current: dagx::TaskHandle = first.into(); - - for _ in 1..10_000 { - current = dag - .add_task(task_fn::(|prev: &u64| prev + 1)) - .depends_on(current); - } - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(dag.get(current)?, 9_999); - Ok(()) -} diff --git a/tests/boundaries/large_scale.rs b/tests/boundaries/large_scale.rs deleted file mode 100644 index 9d092a2..0000000 --- a/tests/boundaries/large_scale.rs +++ /dev/null @@ -1,309 +0,0 @@ -//! Large scale stress tests with thousands of nodes and edges - -use crate::common::task_fn; -use dagx::{DagResult, DagRunner}; - -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; - -#[tokio::test] -async fn test_10000_independent_tasks() -> DagResult<()> { - // Test with 10,000 independent tasks - let dag = DagRunner::new(); - - let counter = Arc::new(AtomicUsize::new(0)); - - let mut tasks: Vec<_> = (0..10_000) - .map(|i| { - let counter = counter.clone(); - dag.add_task(task_fn::<(), _, _>(move |_: ()| { - counter.fetch_add(1, Ordering::Relaxed); - i - })) - }) - .collect(); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Verify all tasks completed - assert_eq!(counter.load(Ordering::Relaxed), 10_000); - - // Spot check some results - assert_eq!(dag.get(tasks.swap_remove(9_999))?, 9_999); - assert_eq!(dag.get(tasks.swap_remove(5_000))?, 5_000); - assert_eq!(dag.get(tasks.swap_remove(0))?, 0); - - Ok(()) -} - -#[tokio::test] -async fn test_5000_node_pyramid() -> DagResult<()> { - // Create a pyramid structure with ~5000 nodes - // Layer sizes: 2048 -> 512 -> 128 -> 32 -> 8 -> 2 -> 1 - let dag = DagRunner::new(); - - // Bottom layer: 2048 nodes - convert to TaskHandles - let mut current_layer: Vec> = (0..2048) - .map(|i| { - let task = dag.add_task(task_fn::<(), _, _>( - move |_: ()| { - if i < 1024 { - 1 - } else { - 0 - } - }, - )); - task.into() - }) - .collect(); - - let layer_sizes = vec![512, 128, 32, 8, 2]; - - for layer_size in layer_sizes { - let mut next_layer = Vec::with_capacity(layer_size); - let group_size = current_layer.len() / layer_size; - - for i in 0..layer_size { - let start = i * group_size; - let deps: Vec<_> = current_layer[start..start + group_size.min(4)] - .iter() - .take(4) - .collect(); - - let task = match deps.len() { - 1 => dag - .add_task(task_fn::(|&a: &i32| a)) - .depends_on(deps[0]), - 2 => dag - .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b)) - .depends_on((deps[0], deps[1])), - 3 => dag - .add_task(task_fn::<(i32, i32, i32), _, _>( - |(a, b, c): (&i32, &i32, &i32)| a + b + c, - )) - .depends_on((deps[0], deps[1], deps[2])), - 4 => dag - .add_task(task_fn::<(i32, i32, i32, i32), _, _>( - |(a, b, c, d): (&i32, &i32, &i32, &i32)| a + b + c + d, - )) - .depends_on((deps[0], deps[1], deps[2], deps[3])), - _ => unreachable!(), - }; - next_layer.push(task); - } - current_layer = next_layer; - } - - // Final layer combines the last 2 - let root = dag - .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b)) - .depends_on((¤t_layer[0], ¤t_layer[1])); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // We created 1024 ones and 1024 zeros at the bottom - // The sum should propagate up to 1024 - let result = dag.get(root)?; - assert!(result > 0, "Expected positive result, got {}", result); - Ok(()) -} - -#[tokio::test] -async fn test_5000_node_pyramid_single_threaded() -> DagResult<()> { - // Create a pyramid structure with ~5000 nodes - // Layer sizes: 2048 -> 512 -> 128 -> 32 -> 8 -> 2 -> 1 - let dag = DagRunner::new(); - - // Bottom layer: 2048 nodes - convert to TaskHandles - let mut current_layer: Vec> = (0..2048) - .map(|i| { - let task = dag.add_task(task_fn::<(), _, _>( - move |_: ()| { - if i < 1024 { - 1 - } else { - 0 - } - }, - )); - task.into() - }) - .collect(); - - let layer_sizes = vec![512, 128, 32, 8, 2]; - - for layer_size in layer_sizes { - let mut next_layer = Vec::with_capacity(layer_size); - let group_size = current_layer.len() / layer_size; - - for i in 0..layer_size { - let start = i * group_size; - let deps: Vec<_> = current_layer[start..start + group_size.min(4)] - .iter() - .take(4) - .collect(); - - let task = match deps.len() { - 1 => dag - .add_task(task_fn::(|&a: &i32| a)) - .depends_on(deps[0]), - 2 => dag - .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b)) - .depends_on((deps[0], deps[1])), - 3 => dag - .add_task(task_fn::<(i32, i32, i32), _, _>( - |(a, b, c): (&i32, &i32, &i32)| a + b + c, - )) - .depends_on((deps[0], deps[1], deps[2])), - 4 => dag - .add_task(task_fn::<(i32, i32, i32, i32), _, _>( - |(a, b, c, d): (&i32, &i32, &i32, &i32)| a + b + c + d, - )) - .depends_on((deps[0], deps[1], deps[2], deps[3])), - _ => unreachable!(), - }; - next_layer.push(task); - } - current_layer = next_layer; - } - - // Final layer combines the last 2 - let root = dag - .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b)) - .depends_on((¤t_layer[0], ¤t_layer[1])); - - dag.run(|fut| fut).await?; - - // We created 1024 ones and 1024 zeros at the bottom - // The sum should propagate up to 1024 - let result = dag.get(root)?; - assert!(result > 0, "Expected positive result, got {}", result); - Ok(()) -} - -#[tokio::test] -async fn test_wide_dag_1000_sources_1000_sinks() -> DagResult<()> { - // Test a very wide DAG with 1000 sources and 1000 sinks - let dag = DagRunner::new(); - - // Create 1000 source tasks - let sources: Vec<_> = (0..1000) - .map(|i| { - dag.add_task(task_fn::<(), _, _>(move |_: ()| i % 10)) - .into() - }) - .collect(); - - // Create 1000 sink tasks, each depending on 3 random sources - let sinks: Vec<_> = (0..1000) - .map(|i| { - let idx1 = (i * 7) % 1000; - let idx2 = (i * 13) % 1000; - let idx3 = (i * 17) % 1000; - - dag.add_task(task_fn::<(i32, i32, i32), _, _>( - move |(a, b, c): (&i32, &i32, &i32)| a + b + c + i as i32, - )) - .depends_on((&sources[idx1], &sources[idx2], &sources[idx3])) - }) - .collect(); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Verify some sinks completed correctly - let result0 = dag.get(sinks[0])?; - let result500 = dag.get(sinks[500])?; - let result999 = dag.get(sinks[999])?; - - assert!(result0 >= 0); - assert!(result500 >= 500); - assert!(result999 >= 999); - - Ok(()) -} - -#[tokio::test] -async fn test_10000_node_linear_chain_segments() -> DagResult<()> { - // Test 10000 nodes arranged as 100 parallel chains of 100 nodes each - let dag = DagRunner::new(); - let counter = Arc::new(AtomicUsize::new(0)); - - let mut chain_ends = Vec::new(); - - for chain_id in 0..100 { - let first = dag.add_task(task_fn::<(), _, _>({ - let counter = counter.clone(); - move |_: ()| { - let counter = counter.clone(); - counter.fetch_add(1, Ordering::Relaxed); - chain_id * 100 - } - })); - let mut current: dagx::TaskHandle = first.into(); - - for _i in 1..100 { - current = dag - .add_task(task_fn::({ - let counter = counter.clone(); - move |&prev: &i32| { - let counter = counter.clone(); - counter.fetch_add(1, Ordering::Relaxed); - prev + 1 - } - })) - .depends_on(current); - } - - chain_ends.push(current); - } - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // All 10,000 tasks should have executed - assert_eq!(counter.load(Ordering::Relaxed), 10_000); - - // Each chain should end with start + 99 - for (chain_id, &chain_end) in chain_ends.iter().enumerate() { - let expected = chain_id as i32 * 100 + 99; - assert_eq!(dag.get(chain_end)?, expected); - } - - Ok(()) -} - -#[tokio::test] -#[cfg_attr(tarpaulin, ignore)] // This test is very resource intensive -async fn test_100000_nodes_stress() -> DagResult<()> { - // Ultimate stress test: 100,000 nodes - let dag = DagRunner::new(); - let completed = Arc::new(AtomicUsize::new(0)); - - // Create 100,000 tasks in batches - let batch_size = 1000; - let num_batches = 100; - - for batch in 0..num_batches { - let _: Vec<_> = (0..batch_size) - .map(|i| { - let completed = completed.clone(); - let task_id = batch * batch_size + i; - dag.add_task(task_fn::<(), _, _>(move |_: ()| { - let completed = completed.clone(); - completed.fetch_add(1, Ordering::Relaxed); - task_id - })) - }) - .collect(); - } - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(completed.load(Ordering::Relaxed), 100_000); - Ok(()) -} diff --git a/tests/boundaries/mod.rs b/tests/boundaries/mod.rs index 7c6945f..33b419c 100644 --- a/tests/boundaries/mod.rs +++ b/tests/boundaries/mod.rs @@ -4,6 +4,5 @@ //! by normal usage patterns. mod deep_chains; -mod large_scale; mod type_limits; mod zero_tasks; diff --git a/tests/boundaries/type_limits.rs b/tests/boundaries/type_limits.rs index 3cae04b..79a3f21 100644 --- a/tests/boundaries/type_limits.rs +++ b/tests/boundaries/type_limits.rs @@ -1,7 +1,7 @@ //! Tests for type system edge cases and limits -use crate::common::task_fn; use dagx::{DagResult, DagRunner, TaskHandle}; +use dagx_test::task_fn; use std::marker::PhantomData; @@ -108,89 +108,6 @@ async fn test_unit_type_chains() -> DagResult<()> { Ok(()) } -#[tokio::test] -async fn test_string_types_variety() -> DagResult<()> { - // Test various string types - let dag = DagRunner::new(); - - let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| "static str")); - let t2 = dag.add_task(task_fn::<(), _, _>(|_: ()| String::from("owned string"))); - let t3 = dag.add_task(task_fn::<(), _, _>(|_: ()| { - std::borrow::Cow::Borrowed("cow str") - })); - let t4 = dag.add_task(task_fn::<(), _, _>(|_: ()| Box::new("boxed str"))); - let t5 = dag.add_task(task_fn::<(), _, _>(|_: ()| { - std::sync::Arc::new("arc str 2") - })); - let t6 = dag.add_task(task_fn::<(), _, _>(|_: ()| std::sync::Arc::new("arc str"))); - - let combined = dag - .add_task(task_fn::< - ( - &str, - String, - std::borrow::Cow, - Box<&str>, - std::sync::Arc<&str>, - std::sync::Arc<&str>, - ), - _, - _, - >( - |(s1, s2, s3, s4, s5, s6): ( - &&str, - &String, - &std::borrow::Cow, - &Box<&str>, - &std::sync::Arc<&str>, - &std::sync::Arc<&str>, - )| { format!("{} {} {} {} {} {}", s1, s2, s3, s4, s5, s6) }, - )) - .depends_on((t1, t2, t3, t4, t5, t6)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - let result = dag.get(combined)?; - assert!(result.contains("static str")); - assert!(result.contains("owned string")); - - Ok(()) -} - -#[tokio::test] -async fn test_generic_type_constraints() -> DagResult<()> { - // Test with types that have trait bounds - #[derive(Debug, Clone, PartialEq)] - struct Wrapper(T); - - let dag = DagRunner::new(); - - let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| Wrapper(42))); - let t2 = dag.add_task(task_fn::<(), _, _>(|_: ()| Wrapper("hello"))); - let t3 = dag.add_task(task_fn::<(), _, _>(|_: ()| Wrapper(vec![1, 2, 3]))); - - let combined = dag - .add_task(task_fn::< - (Wrapper, Wrapper<&str>, Wrapper>), - _, - _, - >( - |(w1, w2, w3): (&Wrapper, &Wrapper<&str>, &Wrapper>)| { - format!("{:?} {:?} {:?}", w1, w2, w3) - }, - )) - .depends_on((t1, t2, t3)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - let result = dag.get(combined)?; - assert!(result.contains("Wrapper(42)")); - - Ok(()) -} - #[tokio::test] async fn test_reference_wrapper_types() -> DagResult<()> { // Test types that wrap references @@ -216,71 +133,3 @@ async fn test_reference_wrapper_types() -> DagResult<()> { Ok(()) } - -#[tokio::test] -async fn test_array_types() -> DagResult<()> { - // Test with fixed-size arrays - let dag = DagRunner::new(); - - let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| [1, 2, 3, 4, 5])); - let t2 = dag.add_task(task_fn::<(), _, _>(|_: ()| [6, 7, 8, 9, 10])); - - let t3 = dag - .add_task(task_fn::<([i32; 5], [i32; 5]), _, _>( - |(a1, a2): (&[i32; 5], &[i32; 5])| { - let mut sum = 0; - for i in 0..5 { - sum += a1[i] + a2[i]; - } - sum - }, - )) - .depends_on((t1, t2)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(dag.get(t3)?, 55); - - Ok(()) -} - -#[tokio::test] -async fn test_enum_variants() -> DagResult<()> { - // Test with various enum types - #[derive(Debug, Clone, PartialEq)] - #[allow(clippy::large_enum_variant)] - enum MyEnum { - Unit, - Tuple(i32, String), - Struct { x: i32, y: i32 }, - LargeVariant([u8; 1024]), - } - - let dag = DagRunner::new(); - - let t1 = dag.add_task(task_fn::<(), _, _>(|_: ()| MyEnum::Unit)); - let t2 = dag.add_task(task_fn::<(), _, _>(|_: ()| { - MyEnum::Tuple(42, "test".to_string()) - })); - let t3 = dag.add_task(task_fn::<(), _, _>(|_: ()| MyEnum::Struct { x: 10, y: 20 })); - let t4 = dag.add_task(task_fn::<(), _, _>(|_: ()| MyEnum::LargeVariant([0; 1024]))); - - let combined = dag - .add_task(task_fn::<(MyEnum, MyEnum, MyEnum, MyEnum), _, _>( - |(e1, e2, e3, e4): (&MyEnum, &MyEnum, &MyEnum, &MyEnum)| { - matches!(e1, MyEnum::Unit) - && matches!(e2, MyEnum::Tuple(42, _)) - && matches!(e3, MyEnum::Struct { x: 10, y: 20 }) - && matches!(e4, MyEnum::LargeVariant(_)) - }, - )) - .depends_on((t1, t2, t3, t4)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert!(dag.get(combined)?); - - Ok(()) -} diff --git a/tests/boundaries/zero_tasks.rs b/tests/boundaries/zero_tasks.rs index d17dddc..3548a63 100644 --- a/tests/boundaries/zero_tasks.rs +++ b/tests/boundaries/zero_tasks.rs @@ -1,89 +1,10 @@ //! Tests for empty DAGs and minimal task configurations -use crate::common::task_fn; use dagx::{task, DagResult, DagRunner}; +use dagx_test::task_fn; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; use tokio::task::yield_now; -#[tokio::test] -async fn test_empty_dag_execution() -> DagResult<()> { - // Test that an empty DAG can be created and run without issues - let dag = DagRunner::new(); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Should complete successfully with no tasks - Ok(()) -} - -#[tokio::test] -async fn test_single_task_no_deps() -> DagResult<()> { - // Test the absolute minimum: one task with no dependencies - let dag = DagRunner::new(); - - let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(dag.get(task)?, 42); - Ok(()) -} - -#[tokio::test] -async fn test_single_task_unit_input_output() -> DagResult<()> { - // Test a task that takes unit and returns unit - let dag = DagRunner::new(); - - let executed = Arc::new(AtomicUsize::new(0)); - let exec_clone = executed.clone(); - - let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| { - let exec = exec_clone.clone(); - exec.fetch_add(1, Ordering::SeqCst); - })); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - dag.get(task)?; // Just verify it doesn't error - assert_eq!(executed.load(Ordering::SeqCst), 1); - Ok(()) -} - -#[tokio::test] -async fn test_zero_sized_type_task() -> DagResult<()> { - // Test with zero-sized types - #[derive(Debug, Clone, PartialEq)] - struct ZeroSized; - - let dag = DagRunner::new(); - - let task = dag.add_task(task_fn::<(), _, _>(|_: ()| ZeroSized)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(dag.get(task)?, ZeroSized); - Ok(()) -} - -#[tokio::test] -async fn test_empty_dag_multiple_runs() -> DagResult<()> { - // Test that an empty DAG can be run multiple times - let dag = DagRunner::new(); - - for _ in 0..10 { - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - } - - Ok(()) -} - #[tokio::test] async fn test_dag_with_only_source_tasks() -> DagResult<()> { // Test a DAG with multiple source tasks but no dependencies @@ -127,47 +48,6 @@ async fn test_single_self_contained_stateful_task() -> DagResult<()> { Ok(()) } -#[tokio::test] -async fn test_alternating_empty_and_filled_dag_runs() -> DagResult<()> { - // Test alternating between empty DAG and DAG with tasks - for i in 0..5 { - let dag = DagRunner::new(); - - if i % 2 == 0 { - // Empty DAG - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - } else { - // DAG with a task - let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - assert_eq!(dag.get(task)?, i); - } - } - - Ok(()) -} - -#[tokio::test] -async fn test_dag_with_never_type_simulation() -> DagResult<()> { - // Test handling of tasks that conceptually "never" complete - // (simulated by immediate completion with special marker) - #[derive(Debug, Clone, PartialEq)] - enum Never {} - - let dag = DagRunner::new(); - - // This task completes immediately but returns a Result containing "never" type - let task = dag.add_task(task_fn::<(), _, _>(|_: ()| Result::::Ok(42))); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(dag.get(task)?, Ok(42)); - Ok(()) -} - #[tokio::test] async fn test_minimal_task_with_minimal_async_work() -> DagResult<()> { // Test a task that does the absolute minimum async work diff --git a/tests/common/mod.rs b/tests/common/mod.rs deleted file mode 100644 index 81e2944..0000000 --- a/tests/common/mod.rs +++ /dev/null @@ -1,112 +0,0 @@ -// Common test utilities and helpers for the test suite - -// Re-export task_fn for internal testing -// Note: task_fn is #[doc(hidden)] and not part of the documented public API -pub use dagx::task_fn; - -// Re-export commonly used test structs -#[allow(dead_code)] -pub mod tasks { - use dagx::task; - - // Simple seed task with mutable state - pub struct Seed { - pub value: i32, - } - - impl Seed { - pub fn new(v: i32) -> Self { - Self { value: v } - } - } - - #[task] - impl Seed { - async fn run(&mut self) -> i32 { - self.value - } - } - - // Stateless increment task - pub struct Inc; - - impl Inc { - pub fn new() -> Self { - Self - } - } - - #[task] - impl Inc { - async fn run(x: &i32) -> i32 { - x + 1 - } - } - - // Stateless add task - pub struct Add; - - impl Add { - pub fn new() -> Self { - Self - } - } - - #[task] - impl Add { - async fn run(a: &i32, b: &i32) -> i32 { - a + b - } - } - - // Stateless multiply task - pub struct Mul; - - impl Mul { - pub fn new() -> Self { - Self - } - } - - #[task] - impl Mul { - async fn run(a: &i32, b: &i32) -> i32 { - a * b - } - } - - // Stateful counter task - pub struct Counter { - pub count: i32, - } - - impl Counter { - pub fn new(initial: i32) -> Self { - Self { count: initial } - } - } - - #[task] - impl Counter { - async fn run(&mut self, input: &i32) -> i32 { - self.count += input; - self.count - } - } - - // Task that outputs multiple types combined as string - pub struct OutputResult; - - impl OutputResult { - pub fn new() -> Self { - Self - } - } - - #[task] - impl OutputResult { - async fn run(s: &String, n: &usize, f: &bool) -> String { - format!("{s}:{n}:{f}") - } - } -} diff --git a/tests/custom_type_test.rs b/tests/custom_type_test.rs index 7031744..f7d5374 100644 --- a/tests/custom_type_test.rs +++ b/tests/custom_type_test.rs @@ -1,4 +1,4 @@ -// Test that custom types work without any trait implementations +// Test that custom types work with the #[task] macro use dagx::{task, DagRunner, TaskHandle}; #[derive(Clone, Debug, PartialEq)] @@ -84,28 +84,6 @@ async fn test_custom_type_passthrough() { assert_eq!(company_result.employees.len(), 1); } -#[tokio::test] -async fn test_custom_type_diamond_pattern() { - let dag = DagRunner::new(); - - // Source - let fetch: TaskHandle<_> = dag.add_task(FetchPerson).into(); - - // 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| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - let result_a = dag.get(path_a).unwrap(); - let result_b = dag.get(path_b).unwrap(); - - assert_eq!(result_a.as_str(), "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(); diff --git a/tests/dependencies/mod.rs b/tests/dependencies/mod.rs index 7b8d946..2552eb4 100644 --- a/tests/dependencies/mod.rs +++ b/tests/dependencies/mod.rs @@ -1,5 +1,4 @@ //! Dependency tests module pub mod resolution; -pub mod single; pub mod tuples; diff --git a/tests/dependencies/resolution.rs b/tests/dependencies/resolution.rs index 3200e97..31ac727 100644 --- a/tests/dependencies/resolution.rs +++ b/tests/dependencies/resolution.rs @@ -1,7 +1,7 @@ //! Tests for dependency resolution order and transitive dependencies -use crate::common::task_fn; -use dagx::{task, DagResult, DagRunner, TaskHandle}; +use dagx::{task, DagResult, DagRunner}; +use dagx_test::task_fn; use std::sync::{Arc, Mutex}; @@ -99,68 +99,3 @@ async fn test_dependency_resolution_order() -> DagResult<()> { Ok(()) } - -#[tokio::test] -async fn test_transitive_dependencies() -> DagResult<()> { - // Test that transitive dependencies work correctly - // A -> B -> C (creates transitive A -> C relationship) - let dag = DagRunner::new(); - - let a: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into(); - let b = dag - .add_task(task_fn::(|&x: &i32| x * 2)) - .depends_on(a); - let c = dag - .add_task(task_fn::(|&x: &i32| x + 5)) - .depends_on(b); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(dag.get(a)?, 10); - assert_eq!(dag.get(b)?, 20); - assert_eq!(dag.get(c)?, 25); // 20 + 5 - - Ok(()) -} - -#[tokio::test] -async fn test_deep_chain_50_levels() -> DagResult<()> { - let dag = DagRunner::new(); - - let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0)); - let mut handles = vec![first.into()]; - - for _i in 1..=50 { - let next = dag - .add_task(task_fn::(move |&x: &i32| x + 1)) - .depends_on(handles.last().unwrap()); - handles.push(next); - } - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(dag.get(handles.last().unwrap())?, 50); - Ok(()) -} - -#[tokio::test] -async fn test_deep_chain_200_levels() -> DagResult<()> { - let dag = DagRunner::new(); - - let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)); - let mut current = first.into(); - - for _ in 0..200 { - current = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(current); - } - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(dag.get(current)?, 201); - Ok(()) -} diff --git a/tests/dependencies/single.rs b/tests/dependencies/single.rs deleted file mode 100644 index d728dd2..0000000 --- a/tests/dependencies/single.rs +++ /dev/null @@ -1,122 +0,0 @@ -//! Tests for single dependencies and basic dependency data flow - -use crate::common::task_fn; -use dagx::{DagResult, DagRunner, TaskHandle}; - -#[tokio::test] -async fn test_single_dependency() -> DagResult<()> { - let dag = DagRunner::new(); - - let source = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); - let dependent = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(source); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(dag.get(dependent)?, 43); - Ok(()) -} - -#[tokio::test] -async fn test_dependency_data_flow() -> DagResult<()> { - // Test that data flows correctly through dependencies - let dag = DagRunner::new(); - - let source: TaskHandle<_> = dag - .add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3, 4, 5])) - .into(); - - let sum = dag - .add_task(task_fn::, _, _>(|v: &Vec| { - v.iter().sum::() - })) - .depends_on(source); - - let product = dag - .add_task(task_fn::, _, _>(|v: &Vec| { - v.iter().product::() - })) - .depends_on(source); - - let final_result = dag - .add_task(task_fn::<(i32, i32), _, _>(|(s, p): (&i32, &i32)| s + p)) - .depends_on((&sum, &product)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Note: sum and product are not sinks (final_result depends on them) - // Only final_result is a sink and can be retrieved - assert_eq!(dag.get(final_result)?, 135); // 15+120 - - Ok(()) -} - -#[tokio::test] -async fn test_dependencies_with_different_types() -> DagResult<()> { - // Test that dependencies with different types work correctly - let dag = DagRunner::new(); - - let int_source = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); - let string_source = dag.add_task(task_fn::<(), _, _>(|_: ()| "hello".to_string())); - let bool_source = dag.add_task(task_fn::<(), _, _>(|_: ()| true)); - - let combined = dag - .add_task(task_fn::<(i32, String, bool), _, _>( - |(i, s, b): (&i32, &String, &bool)| format!("{}: {} ({})", s, i, b), - )) - .depends_on((int_source, string_source, bool_source)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(dag.get(combined)?, "hello: 42 (true)"); - Ok(()) -} - -#[tokio::test] -async fn test_shared_dependencies() -> DagResult<()> { - // Test that a single node can be a dependency for multiple downstream nodes - let dag = DagRunner::new(); - - let shared: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into(); - - // Create 10 tasks that all depend on the shared node - let dependents: Vec<_> = (0..10) - .map(|i| { - dag.add_task(task_fn::(move |&x: &i32| x * (i + 1))) - .depends_on(shared) - }) - .collect(); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Verify all dependents got the correct value from shared - for (i, handle) in dependents.iter().enumerate() { - assert_eq!(dag.get(handle)?, 42 * (i as i32 + 1)); - } - - Ok(()) -} - -#[tokio::test] -async fn test_multiple_source_nodes() -> DagResult<()> { - // Test DAG with multiple source nodes (no dependencies) - let dag = DagRunner::new(); - - let sources: Vec<_> = (0..20) - .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 10))) - .collect(); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - for (i, source) in sources.into_iter().enumerate() { - assert_eq!(dag.get(source)?, i as i32 * 10); - } - - Ok(()) -} diff --git a/tests/dependencies/tuples.rs b/tests/dependencies/tuples.rs index 1b4f141..aafbd3a 100644 --- a/tests/dependencies/tuples.rs +++ b/tests/dependencies/tuples.rs @@ -1,7 +1,7 @@ //! Tests for tuple dependencies (2-8 dependencies) -use crate::common::task_fn; use dagx::{DagResult, DagRunner}; +use dagx_test::task_fn; #[tokio::test] async fn test_two_dependencies() -> DagResult<()> { diff --git a/tests/errors/error_types.rs b/tests/errors/error_types.rs deleted file mode 100644 index 153d3ea..0000000 --- a/tests/errors/error_types.rs +++ /dev/null @@ -1,161 +0,0 @@ -//! Tests for different error types and error handling - -use crate::common::task_fn; -use dagx::{DagError, DagRunner, TaskHandle}; - -#[tokio::test] -async fn test_result_not_found_error() { - let dag = DagRunner::new(); - - let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into(); - - // Try to get result before running - let result = dag.get(t1); - assert!(matches!(result, Err(DagError::ResultNotFound { .. }))); - - // Run and verify it works after - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(t1).unwrap(), 42); -} - -#[tokio::test] -async fn test_nested_result_error_handling() { - let dag = DagRunner::new(); - - // Task that returns a Result - let task1 = dag.add_task(task_fn::<(), _, _>(|_: ()| { - Result::::Err("Internal error".to_string()) - })); - - let task2 = dag.add_task(task_fn::<(), _, _>(|_: ()| Result::::Ok(42))); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // Both tasks complete successfully (they return Results) - assert_eq!(dag.get(task1).unwrap(), Err("Internal error".to_string())); - assert_eq!(dag.get(task2).unwrap(), Ok(42)); -} - -#[tokio::test] -async fn test_option_none_not_error() { - let dag = DagRunner::new(); - - let task = dag.add_task(task_fn::<(), _, _>(|_: ()| None::)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // None is a valid result, not an error - assert_eq!(dag.get(task).unwrap(), None); -} - -#[tokio::test] -async fn test_custom_error_types() { - #[derive(Debug, Clone, PartialEq)] - enum CustomError { - ValidationError(String), - ProcessingError { code: i32, details: String }, - } - - let dag = DagRunner::new(); - - let task1 = dag.add_task(task_fn::<(), _, _>(|_: ()| { - Result::::Err(CustomError::ValidationError("Invalid input".to_string())) - })); - - let task2 = dag.add_task(task_fn::<(), _, _>(|_: ()| { - Result::::Err(CustomError::ProcessingError { - code: 500, - details: "Processing failed".to_string(), - }) - })); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // Check custom errors are preserved - match dag.get(task1).unwrap().as_ref() { - Err(CustomError::ValidationError(msg)) => assert_eq!(msg, "Invalid input"), - _ => panic!("Wrong error type"), - } - - match dag.get(task2).unwrap().as_ref() { - Err(CustomError::ProcessingError { code, .. }) => assert_eq!(*code, 500), - _ => panic!("Wrong error type"), - } -} - -#[tokio::test] -async fn test_error_in_tuple_dependency() { - let dag = DagRunner::new(); - - let ok_task = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)); - let err_task = dag.add_task(task_fn::<(), _, _>(|_: ()| { - panic!("Error in dependency"); - })); - - let dependent = dag - .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b)) - .depends_on((ok_task, err_task)); - - let result = dag - .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await; - - assert!(result.is_err()); - assert!(dag.get(dependent).is_err()); -} - -#[tokio::test] -async fn test_string_panic_vs_structured_panic() { - let dag = DagRunner::new(); - - let string_panic = dag.add_task(task_fn::<(), _, _>(|_: ()| { - panic!("String panic message"); - })); - - let formatted_panic = dag.add_task(task_fn::<(), _, _>(|_: ()| { - panic!("Formatted panic: value={}, code={}", 42, "ERROR"); - })); - - let _ = dag - .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await; - - // Both should result in errors - assert!(dag.get(string_panic).is_err()); - assert!(dag.get(formatted_panic).is_err()); -} - -#[tokio::test] -async fn test_anyhow_like_error_chain() { - // Test error chains with simple string-based errors - let dag = DagRunner::new(); - - let task = dag.add_task(task_fn::<(), _, _>(|_: ()| { - // Simulate an error chain with nested Results - let _inner: Result = Err("Inner error".to_string()); - let outer: Result, String> = Err("Outer error".to_string()); - - outer - })); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // Get the chained error - let result = dag.get(task).unwrap(); - assert!(result.is_err()); - - if let Err(e) = result.as_ref() { - assert_eq!(e, "Outer error"); - } -} diff --git a/tests/errors/mod.rs b/tests/errors/mod.rs index 198240c..52cda9d 100644 --- a/tests/errors/mod.rs +++ b/tests/errors/mod.rs @@ -2,6 +2,5 @@ //! //! Tests for error scenarios and recovery patterns. -mod error_types; mod propagation; mod recovery; diff --git a/tests/errors/propagation.rs b/tests/errors/propagation.rs index 4008f8d..44501c9 100644 --- a/tests/errors/propagation.rs +++ b/tests/errors/propagation.rs @@ -1,9 +1,8 @@ //! Tests for error propagation through the DAG -use crate::common::task_fn; use dagx::{task, DagRunner, TaskHandle}; +use dagx_test::task_fn; -use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use std::time::Duration; use tokio::time::sleep; @@ -76,100 +75,6 @@ async fn test_diamond_error_propagation() { assert!(dag.get(sink).is_err()); // Fails due to right dependency } -#[tokio::test] -async fn test_error_stops_at_boundary() { - let dag = DagRunner::new(); - let execution_tracker = Arc::new(AtomicUsize::new(0)); - - // Branch with error - let error_source: TaskHandle<_> = dag - .add_task(task_fn::<(), _, _>({ - let tracker = execution_tracker.clone(); - move |_: ()| -> i32 { - let tracker = tracker.clone(); - tracker.fetch_add(1, Ordering::SeqCst); - panic!("Error source"); - } - })) - .into(); - - let error_dependent = dag - .add_task(task_fn::({ - let tracker = execution_tracker.clone(); - move |_: &i32| { - let tracker = tracker.clone(); - tracker.fetch_add(100, Ordering::SeqCst); // Should not execute - 42 - } - })) - .depends_on(error_source); - - // Independent branch - let independent = dag.add_task(task_fn::<(), _, _>({ - let tracker = execution_tracker.clone(); - move |_: ()| { - let tracker = tracker.clone(); - tracker.fetch_add(10, Ordering::SeqCst); - 10 - } - })); - - let _ = dag - .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await; - - // Error branch: source runs but dependent doesn't - assert!(dag.get(error_source).is_err()); - assert!(dag.get(error_dependent).is_err()); - - // Independent branch runs - assert_eq!(dag.get(independent).unwrap(), 10); - - // Check execution: 1 (error_source) + 10 (independent) = 11 - assert_eq!(execution_tracker.load(Ordering::SeqCst), 11); -} - -#[tokio::test] -async fn test_multiple_error_sources_convergence() { - let dag = DagRunner::new(); - - // Multiple error sources - let error1 = dag - .add_task(task_fn::<(), _, _>(|_: ()| { - panic!("Error source 1"); - })) - .into(); - - let error2 = dag - .add_task(task_fn::<(), _, _>(|_: ()| { - panic!("Error source 2"); - })) - .into(); - - let success = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into(); - - // Convergence point depends on all three - let convergence = dag - .add_task(task_fn::<(i32, i32, i32), _, _>( - |(_, _, _): (&i32, &i32, &i32)| 100, - )) - .depends_on((&error1, &error2, &success)); - - let _ = dag - .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await; - - // All error sources fail - assert!(dag.get(error1).is_err()); - assert!(dag.get(error2).is_err()); - - // Success task works - assert_eq!(dag.get(success).unwrap(), 42); - - // Convergence fails due to any error dependency - assert!(dag.get(convergence).is_err()); -} - #[tokio::test] async fn test_error_in_wide_fanout() { let dag = DagRunner::new(); @@ -312,85 +217,3 @@ async fn test_error_propagation_timing() { assert!(order.contains(&"t2_start")); assert!(!order.contains(&"t3_should_not_run")); } - -#[tokio::test] -async fn test_partial_branch_failure_propagation() { - let dag = DagRunner::new(); - - // root - // / | \ - // a1 b1 c1 - // | | | - // a2 b2 c2 - // | (err) | - // a3 b3 c3 - // \ | / - // sink - - let root: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)).into(); - - // Branch A - let a1 = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(root); - let a2 = dag - .add_task(task_fn::(|&x: &i32| x + 2)) - .depends_on(a1); - let a3 = dag - .add_task(task_fn::(|&x: &i32| x + 3)) - .depends_on(a2); - - // Branch B (with error) - use Result instead of panic - let b1 = dag - .add_task(task_fn::(|&x: &i32| Ok::(x * 2))) - .depends_on(root); - let b2 = dag - .add_task(task_fn::, _, _>(|_x: &Result| { - Err::("Branch B fails at b2") - })) - .depends_on(b1); - let b3 = dag - .add_task(task_fn::, _, _>(|x: &Result| { - x.map(|v| v * 3) - })) - .depends_on(b2); - - // Branch C - let c1 = dag - .add_task(task_fn::(|&x: &i32| x - 1)) - .depends_on(root); - let c2 = dag - .add_task(task_fn::(|&x: &i32| x - 2)) - .depends_on(c1); - let c3 = dag - .add_task(task_fn::(|&x: &i32| x - 3)) - .depends_on(c2); - - // Sink depends on all branches - handle Result from B - let sink = dag - .add_task(task_fn::<(i32, Result<_, _>, _), _, _>( - |(a, b, c): (&i32, &Result, &i32)| match b { - Ok(b_val) => Ok(a + b_val + c), - Err(e) => Err(*e), - }, - )) - .depends_on((&a3, &b3, &c3)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .ok(); - - // Branch A succeeds - assert_eq!(dag.get(a3).unwrap(), 106); - - // Branch B fails at b2 - assert_eq!(dag.get(b1).unwrap(), Ok(200)); - assert_eq!(dag.get(b2).unwrap(), Err("Branch B fails at b2")); - assert_eq!(dag.get(b3).unwrap(), Err("Branch B fails at b2")); // Propagated error - - // Branch C succeeds - assert_eq!(dag.get(c3).unwrap(), 94); - - // Sink fails due to branch B - assert_eq!(dag.get(sink).unwrap(), Err("Branch B fails at b2")); -} diff --git a/tests/errors/recovery.rs b/tests/errors/recovery.rs index 588bd3a..e1dc5b9 100644 --- a/tests/errors/recovery.rs +++ b/tests/errors/recovery.rs @@ -1,42 +1,11 @@ //! Tests for error recovery patterns -use crate::common::task_fn; use dagx::{DagResult, DagRunner}; +use dagx_test::task_fn; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; -#[tokio::test] -async fn test_fallback_on_error() -> DagResult<()> { - let dag = DagRunner::new(); - - // Primary task that fails - let primary = dag.add_task(task_fn::<(), _, _>(|_: ()| { - panic!("Primary task failed"); - #[allow(unreachable_code)] - 42 - })); - - // Fallback task - let fallback = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)); - - // Selector that uses fallback if primary fails - let _selector = dag.add_task(task_fn::<(), _, _>(|_: ()| {})); - - // Run DAG - let _ = dag - .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await; - - // Primary should fail - assert!(dag.get(primary).is_err()); - - // Fallback should succeed - assert_eq!(dag.get(fallback)?, 100); - - Ok(()) -} - #[tokio::test] async fn test_retry_pattern_simulation() -> DagResult<()> { let dag = DagRunner::new(); @@ -113,38 +82,6 @@ async fn test_circuit_breaker_pattern() -> DagResult<()> { Ok(()) } -#[tokio::test] -async fn test_partial_recovery_with_defaults() -> DagResult<()> { - let dag = DagRunner::new(); - - // Some tasks that might fail - let _tasks: Vec<_> = (0..10) - .map(|i| { - dag.add_task(task_fn::<(), _, _>(move |_: ()| { - if i % 3 == 0 { - panic!("Task {} failed", i); - } - i * 10 - })) - }) - .collect(); - - // Aggregator that handles failures gracefully - let aggregator = dag.add_task(task_fn::<(), _, _>(|_: ()| { - // In real scenario, would check task results and use defaults - vec![0, 10, 20, 0, 40, 50, 0, 70, 80, 0] // 0 for failed tasks - })); - - let _ = dag - .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await; - - let result = dag.get(aggregator)?; - assert_eq!(result.len(), 10); - - Ok(()) -} - #[tokio::test] async fn test_error_accumulation_pattern() -> DagResult<()> { let dag = DagRunner::new(); @@ -183,36 +120,6 @@ async fn test_error_accumulation_pattern() -> DagResult<()> { Ok(()) } -#[tokio::test] -async fn test_graceful_degradation() -> DagResult<()> { - let dag = DagRunner::new(); - - // Critical task - let critical = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3, 4, 5])); - - // Optional enhancement that fails - returns Result - let _enhancement = dag.add_task(task_fn::<(), _, _>(|_: ()| { - Err::, &str>("Enhancement failed") - })); - - // Final task that works with what's available - let final_task = dag - .add_task(task_fn::, _, _>(|base: &Vec| { - // Would combine with enhancement if available - // Here just uses base - base.iter().sum::() - })) - .depends_on(critical); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Critical path works - assert_eq!(dag.get(final_task)?, 15); - - Ok(()) -} - #[tokio::test] async fn test_error_boundary_isolation() -> DagResult<()> { let dag = DagRunner::new(); @@ -248,38 +155,3 @@ async fn test_error_boundary_isolation() -> DagResult<()> { Ok(()) } - -#[tokio::test] -async fn test_compensation_action() -> DagResult<()> { - let dag = DagRunner::new(); - let compensation_triggered = Arc::new(AtomicBool::new(false)); - - // Action that fails - let action = dag.add_task(task_fn::<(), _, _>(|_: ()| { - // Perform some action that fails - panic!("Action failed"); - })); - - // Compensation task (runs independently) - let compensation = dag.add_task(task_fn::<(), _, _>({ - let triggered = compensation_triggered.clone(); - move |_: ()| { - let triggered = triggered.clone(); - triggered.store(true, Ordering::SeqCst); - "Compensation executed" - } - })); - - let _ = dag - .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await; - - // Action failed - assert!(dag.get(action).is_err()); - - // Compensation ran successfully - assert_eq!(dag.get(compensation)?, "Compensation executed"); - assert!(compensation_triggered.load(Ordering::SeqCst)); - - Ok(()) -} diff --git a/tests/execution/basic.rs b/tests/execution/basic.rs deleted file mode 100644 index acbcb66..0000000 --- a/tests/execution/basic.rs +++ /dev/null @@ -1,317 +0,0 @@ -// Basic execution tests for DagRunner -use crate::common::task_fn; - -use dagx::*; - -#[tokio::test] -async fn test_empty_dag() { - let dag = DagRunner::new(); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); // Should complete without error -} - -#[tokio::test] -async fn test_single_task() { - let dag = DagRunner::new(); - let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - assert_eq!(dag.get(task).unwrap(), 42); -} - -#[tokio::test] -async fn test_deep_chain() { - let dag = DagRunner::new(); - - let a = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)); - let b = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(a); - let c = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(b); - let d = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(c); - let e = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(d); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // Note: a, b, c, d are not sinks (they have dependents in the chain) - // Only e is a sink and can be retrieved - assert_eq!(dag.get(e).unwrap(), 5); -} - -#[tokio::test] -async fn test_wide_parallel() { - let dag = DagRunner::new(); - - let tasks: Vec<_> = (0..10) - .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2))) - .collect(); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - for (i, task) in tasks.into_iter().enumerate() { - assert_eq!(dag.get(task).unwrap(), i * 2); - } -} - -#[tokio::test] -async fn test_diamond_dependency() { - let dag = DagRunner::new(); - - let a: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into(); - let b = dag - .add_task(task_fn::(|&x: &i32| x + 5)) - .depends_on(a); - let c = dag - .add_task(task_fn::(|&x: &i32| x * 2)) - .depends_on(a); - let d = dag - .add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x + y)) - .depends_on((&b, &c)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // Note: a, b, and c are not sinks (they have dependents) - // Only d is a sink and can be retrieved - assert_eq!(dag.get(d).unwrap(), 35); // 15 + 20 -} - -#[derive(Clone)] -struct User { - name: String, - age: u32, -} - -#[tokio::test] -async fn test_different_output_types() { - let dag = DagRunner::new(); - - let string_task = dag.add_task(task_fn::<(), _, _>(|_: ()| "hello".to_string())); - let vec_task = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3])); - let struct_task = dag.add_task(task_fn::<(), _, _>(|_: ()| User { - name: "Alice".to_string(), - age: 30, - })); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(&dag.get(string_task).unwrap(), "hello"); - assert_eq!(dag.get(vec_task).unwrap(), vec![1, 2, 3]); - let user = dag.get(struct_task).unwrap(); - assert_eq!(user.name, "Alice"); - assert_eq!(user.age, 30); -} - -#[tokio::test] -async fn test_multiple_sinks() { - let dag = DagRunner::new(); - - let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into(); - - // Branch 1 - let branch1 = dag - .add_task(task_fn::(|&x: &i32| x * 2)) - .depends_on(source); - let sink1 = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(branch1); - - // Branch 2 - let branch2 = dag - .add_task(task_fn::(|&x: &i32| x + 5)) - .depends_on(source); - let sink2 = dag - .add_task(task_fn::(|&x: &i32| x * 3)) - .depends_on(branch2); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); // Should wait for both sinks - - assert_eq!(dag.get(sink1).unwrap(), 21); // (10 * 2) + 1 - assert_eq!(dag.get(sink2).unwrap(), 45); // (10 + 5) * 3 -} - -#[tokio::test] -async fn test_single_task_inline_path() { - // Test single-task layer inline execution path (line 367-410 in runner.rs) - let dag = DagRunner::new(); - - // Single task in its own layer - let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into(); - let t2 = dag - .add_task(task_fn::(|&x: &i32| x * 2)) - .depends_on(t1); - let t3 = dag - .add_task(task_fn::(|&x: &i32| x + 5)) - .depends_on(t2); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(t3).unwrap(), 25); // (10 * 2) + 5 -} - -#[tokio::test] -async fn test_multi_task_spawn_path() { - // Test multi-task layer spawn path (line 411-456 in runner.rs) - let dag = DagRunner::new(); - - let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)).into(); - - // Create multiple tasks in the same layer (all depend on source) - let t1 = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(source); - let t2 = dag - .add_task(task_fn::(|&x: &i32| x + 2)) - .depends_on(source); - let t3 = dag - .add_task(task_fn::(|&x: &i32| x + 3)) - .depends_on(source); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(t1).unwrap(), 101); - assert_eq!(dag.get(t2).unwrap(), 102); - assert_eq!(dag.get(t3).unwrap(), 103); -} - -#[tokio::test] -async fn test_producer_consumer_channels() { - // Test channel creation and management (lines 301-337 in runner.rs) - let dag = DagRunner::new(); - - // Producer - let producer: TaskHandle<_> = dag - .add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3, 4, 5])) - .into(); - - // Consumer - let consumer = dag - .add_task(task_fn::, _, _>(|data: &Vec| { - data.iter().sum::() - })) - .depends_on(producer); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(consumer).unwrap(), 15); -} - -#[tokio::test] -async fn test_multi_consumer_fanout() { - // Test fanout with multiple consumers from one producer - let dag = DagRunner::new(); - - let producer: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into(); - - // Multiple consumers - let c1 = dag - .add_task(task_fn::(|&x: &i32| x * 2)) - .depends_on(producer); - let c2 = dag - .add_task(task_fn::(|&x: &i32| x + 10)) - .depends_on(producer); - let c3 = dag - .add_task(task_fn::(|&x: &i32| x - 5)) - .depends_on(producer); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(c1).unwrap(), 84); - assert_eq!(dag.get(c2).unwrap(), 52); - assert_eq!(dag.get(c3).unwrap(), 37); -} - -#[tokio::test] -async fn test_compute_layers_with_dependents_tracking() { - // Test layer computation with dependent tracking (lines 590-600 in runner.rs) - let dag = DagRunner::new(); - - // Create diamond pattern to ensure dependents are tracked correctly - let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)).into(); - - let left = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(source); - let right = dag - .add_task(task_fn::(|&x: &i32| x + 2)) - .depends_on(source); - - let sink = dag - .add_task(task_fn::<(i32, i32), _, _>(|(l, r): (&i32, &i32)| l + r)) - .depends_on((&left, &right)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(sink).unwrap(), 5); // (1+1) + (1+2) = 5 -} - -#[tokio::test] -async fn test_layer_execution_order() { - // Test that layers execute in correct topological order - use parking_lot::Mutex; - use std::sync::Arc; - - let execution_order = Arc::new(Mutex::new(Vec::new())); - - let dag = DagRunner::new(); - - let order1 = execution_order.clone(); - let t1 = dag.add_task(task_fn::<(), _, _>(move |_: ()| { - let order = order1.clone(); - order.lock().push(1); - 1 - })); - - let order2 = execution_order.clone(); - let t2 = dag - .add_task(task_fn::(move |&x: &i32| { - let order = order2.clone(); - order.lock().push(2); - x + 1 - })) - .depends_on(t1); - - let order3 = execution_order.clone(); - let t3 = dag - .add_task(task_fn::(move |&x: &i32| { - let order = order3.clone(); - order.lock().push(3); - x + 1 - })) - .depends_on(t2); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - let order = execution_order.lock().clone(); - assert_eq!(order, vec![1, 2, 3]); - assert_eq!(dag.get(t3).unwrap(), 3); -} diff --git a/tests/execution/integration.rs b/tests/execution/integration.rs deleted file mode 100644 index 0c808d7..0000000 --- a/tests/execution/integration.rs +++ /dev/null @@ -1,381 +0,0 @@ -//! Integration tests for end-to-end functionality - -use crate::common::task_fn; -use dagx::{DagResult, DagRunner, Task}; - -use std::time::Instant; -use tokio::time::{sleep, Duration}; - -// === Shared Test Tasks === - -struct LoadValue { - value: i32, -} -impl LoadValue { - fn new(value: i32) -> Self { - Self { value } - } -} -#[dagx::task] -impl LoadValue { - async fn run(&self) -> i32 { - self.value // Read-only state: use &self - } -} - -struct Multiply { - factor: i32, -} -impl Multiply { - fn new(factor: i32) -> Self { - Self { factor } - } -} -#[dagx::task] -impl Multiply { - async fn run(&self, input: &i32) -> i32 { - input * self.factor // Read-only state: use &self - } -} - -struct Add; -#[dagx::task] -impl Add { - async fn run(a: &i32, b: &i32) -> i32 { - a + b // Stateless: no self needed - } -} - -// === Integration Tests === - -#[tokio::test] -async fn integration_test_etl_pipeline() -> DagResult<()> { - // Simulate an Extract-Transform-Load pipeline - let dag = DagRunner::new(); - - // Extract: Load data from sources - let source1 = dag.add_task(LoadValue::new(10)); - let source2 = dag.add_task(LoadValue::new(20)); - let source3 = dag.add_task(LoadValue::new(30)); - - // Transform: Process each source - let transform1 = dag.add_task(Multiply::new(2)).depends_on(source1); - let transform2 = dag.add_task(Multiply::new(3)).depends_on(source2); - let transform3 = dag.add_task(Multiply::new(4)).depends_on(source3); - - // Load: Aggregate results - let combine12 = dag.add_task(Add).depends_on((&transform1, &transform2)); - let final_result = dag.add_task(Add).depends_on((&combine12, &transform3)); - - // Execute pipeline - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Verify - let result = dag.get(final_result)?; - assert_eq!(result, (10 * 2) + (20 * 3) + (30 * 4)); // 20 + 60 + 120 = 200 - - Ok(()) -} - -#[tokio::test] -async fn integration_test_multi_layer_dag() -> DagResult<()> { - // Complex multi-layer DAG - let dag = DagRunner::new(); - - // Layer 1: Sources - let a = dag.add_task(LoadValue::new(1)); - let b = dag.add_task(LoadValue::new(2)); - let c = dag.add_task(LoadValue::new(3)); - - // Layer 2: First transformations - let ab = dag.add_task(Add).depends_on((&a, &b)); - let bc = dag.add_task(Add).depends_on((&b, &c)); - - // Layer 3: Second transformations - let double_ab = dag.add_task(Multiply::new(2)).depends_on(ab); - let double_bc = dag.add_task(Multiply::new(2)).depends_on(bc); - - // Layer 4: Final result - let final_result = dag.add_task(Add).depends_on((&double_ab, &double_bc)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - let result = dag.get(final_result)?; - // (1+2)*2 + (2+3)*2 = 6 + 10 = 16 - assert_eq!(result, 16); - - Ok(()) -} - -#[tokio::test] -async fn integration_test_multiple_sinks() -> DagResult<()> { - // DAG with multiple output sinks - let dag = DagRunner::new(); - - let source = dag.add_task(LoadValue::new(10)); - - // Multiple independent branches - let branch1 = dag.add_task(Multiply::new(2)).depends_on(source); - let branch2 = dag.add_task(Multiply::new(3)).depends_on(source); - let branch3 = dag.add_task(Multiply::new(4)).depends_on(source); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // All branches should complete - assert_eq!(dag.get(branch1)?, 20); - assert_eq!(dag.get(branch2)?, 30); - assert_eq!(dag.get(branch3)?, 40); - - Ok(()) -} - -#[tokio::test] -async fn integration_test_diamond_cascade() -> DagResult<()> { - // Multiple diamond patterns in sequence - let dag = DagRunner::new(); - - // Diamond 1 - let source1 = dag.add_task(LoadValue::new(10)); - let left1 = dag.add_task(Multiply::new(2)).depends_on(source1); - let right1 = dag.add_task(Multiply::new(3)).depends_on(source1); - let sink1 = dag.add_task(Add).depends_on((&left1, &right1)); - - // Diamond 2 (depends on Diamond 1) - let left2 = dag.add_task(Multiply::new(2)).depends_on(sink1); - let right2 = dag.add_task(Multiply::new(3)).depends_on(sink1); - let sink2 = dag.add_task(Add).depends_on((&left2, &right2)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - let result = dag.get(sink2)?; - // Diamond 1: (10*2) + (10*3) = 50 - // Diamond 2: (50*2) + (50*3) = 250 - assert_eq!(result, 250); - - Ok(()) -} - -#[tokio::test] -async fn integration_test_wide_and_deep() -> DagResult<()> { - // Combination of wide (parallel) and deep (sequential) patterns - let dag = DagRunner::new(); - - // Wide: 5 parallel sources - let sources: Vec<_> = (0..5) - .map(|i| dag.add_task(LoadValue::new(i * 10))) - .collect(); - - // Deep: Chain each source through 3 transformations - let mut final_nodes = Vec::new(); - for source in &sources { - let step1 = dag.add_task(Multiply::new(2)).depends_on(source); - let step2 = dag - .add_task(task_fn::(|&x: &i32| x + 5)) - .depends_on(step1); - let step3 = dag.add_task(Multiply::new(3)).depends_on(step2); - final_nodes.push(step3); - } - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Verify each chain - for (i, node) in final_nodes.iter().enumerate() { - let expected = ((i as i32 * 10) * 2 + 5) * 3; - assert_eq!(dag.get(node)?, expected); - } - - Ok(()) -} - -#[tokio::test] -async fn integration_test_error_recovery() -> DagResult<()> { - // Test that DAG handles normal execution gracefully - let dag = DagRunner::new(); - - let source = dag.add_task(LoadValue::new(42)); - let transform = dag.add_task(Multiply::new(2)).depends_on(source); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Should be able to access results - assert_eq!(dag.get(transform)?, 84); - - // Getting valid task should return ok - let result = dag.get(transform); - assert!(result.is_ok()); - - Ok(()) -} - -#[tokio::test] -async fn integration_test_large_scale() -> DagResult<()> { - // Large-scale integration test - let dag = DagRunner::new(); - - // Create 100 sources - let sources: Vec<_> = (0..100).map(|i| dag.add_task(LoadValue::new(i))).collect(); - - // Create 50 aggregation tasks (each combines 2 sources) - let aggregations: Vec<_> = (0..50) - .map(|i| { - dag.add_task(Add) - .depends_on((&sources[i * 2], &sources[i * 2 + 1])) - }) - .collect(); - - // Create 25 second-level aggregations - let second_level: Vec<_> = (0..25) - .map(|i| { - dag.add_task(Add) - .depends_on((&aggregations[i * 2], &aggregations[i * 2 + 1])) - }) - .collect(); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Verify some results - assert_eq!(dag.get(aggregations[0])?, 1); // First two - assert_eq!(dag.get(second_level[0])?, 1 + (2 + 3)); // First four - - Ok(()) -} - -#[tokio::test] -async fn integration_test_reused_values() -> DagResult<()> { - // Test that a single source can be used by multiple consumers - let dag = DagRunner::new(); - - let source = dag.add_task(LoadValue::new(100)); - - // Use source in multiple different ways - let double = dag.add_task(Multiply::new(2)).depends_on(source); - let triple = dag.add_task(Multiply::new(3)).depends_on(source); - let quadruple = dag.add_task(Multiply::new(4)).depends_on(source); - - // Combine some of the results - let combined = dag.add_task(Add).depends_on((&double, &triple)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(dag.get(double)?, 200); - assert_eq!(dag.get(triple)?, 300); - assert_eq!(dag.get(quadruple)?, 400); - assert_eq!(dag.get(combined)?, 500); - - Ok(()) -} - -#[tokio::test] -async fn integration_test_parallel_execution_timing() -> DagResult<()> { - // Test that independent tasks actually run in parallel by measuring execution time - let dag = DagRunner::new(); - - let sleep_duration = Duration::from_millis(100); - let num_parallel_tasks = 5; - - // Create 5 independent tasks that each sleep for 100ms - let tasks: Vec<_> = (0..num_parallel_tasks) - .map(|i| { - dag.add_task(task_fn::<(), _, _>(move |_: ()| { - sleep(sleep_duration).await; - i - })) - }) - .collect(); - - let start = Instant::now(); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - let elapsed = start.elapsed(); - - // Verify all tasks completed - for (i, task) in tasks.iter().enumerate() { - assert_eq!(dag.get(task)?, i); - } - - // If tasks ran sequentially, it would take 5 * 100ms = 500ms - // With parallelism, it should take ~100ms (plus overhead) - // We'll check it's less than 300ms (generous margin for CI environments) - assert!( - elapsed < Duration::from_millis(300), - "Parallel execution took {:?}, expected < 300ms (sequential would be 500ms)", - elapsed - ); - - Ok(()) -} - -#[tokio::test] -async fn integration_test_parallel_vs_sequential_timing() -> DagResult<()> { - // Test mixed parallel and sequential execution patterns - let dag = DagRunner::new(); - - let sleep_duration = Duration::from_millis(50); - - // Layer 1: 4 parallel tasks (should take ~50ms) - let parallel_tasks: Vec<_> = (0..4) - .map(|i| { - dag.add_task(task_fn::<(), _, _>(move |_: ()| { - sleep(sleep_duration).await; - i * 10 - })) - }) - .collect(); - - // Layer 2: Sequential chain of 3 tasks depending on the first parallel task - // (should add ~150ms) - let seq1 = dag - .add_task(task_fn::(move |&x: &i32| { - sleep(sleep_duration).await; - x + 1 - })) - .depends_on(parallel_tasks[0]); - - let seq2 = dag - .add_task(task_fn::(move |&x: &i32| { - sleep(sleep_duration).await; - x + 1 - })) - .depends_on(seq1); - - let seq3 = dag - .add_task(task_fn::(move |&x: &i32| { - sleep(sleep_duration).await; - x + 1 - })) - .depends_on(seq2); - - let start = Instant::now(); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - let elapsed = start.elapsed(); - - // Verify results - assert_eq!(dag.get(seq3)?, 3); // 0 + 1 + 1 + 1 - - // Expected time: ~200ms (4 layers of 50ms each: parallel layer + 3 sequential) - // If everything ran sequentially: 7 * 50ms = 350ms - // We'll check it's less than 300ms - assert!( - elapsed < Duration::from_millis(300), - "Execution took {:?}, expected < 300ms (fully sequential would be 350ms)", - elapsed - ); - - // Also verify it took more than just the parallel layer - assert!( - elapsed >= Duration::from_millis(150), - "Execution took {:?}, expected >= 150ms (should include sequential chain)", - elapsed - ); - - Ok(()) -} diff --git a/tests/execution/mod.rs b/tests/execution/mod.rs index 3cd6d17..9f29dd3 100644 --- a/tests/execution/mod.rs +++ b/tests/execution/mod.rs @@ -1,4 +1,169 @@ -// Test module for execution tests +// Basic execution tests for DagRunner +use dagx_test::task_fn; -mod basic; -mod results; +use dagx::*; + +#[tokio::test] +async fn test_empty_dag() { + let dag = DagRunner::new(); + dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + .await + .unwrap(); // Should complete without error +} + +#[tokio::test] +async fn test_single_task() { + let dag = DagRunner::new(); + let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); + dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + .await + .unwrap(); + assert_eq!(dag.get(task).unwrap(), 42); +} + +#[tokio::test] +async fn test_wide_parallel() { + let dag = DagRunner::new(); + + let tasks: Vec<_> = (0..10) + .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2))) + .collect(); + + dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + .await + .unwrap(); + + for (i, task) in tasks.into_iter().enumerate() { + assert_eq!(dag.get(task).unwrap(), i * 2); + } +} + +#[tokio::test] +async fn test_diamond_dependency() { + let dag = DagRunner::new(); + + let a: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into(); + let b = dag + .add_task(task_fn::(|&x: &i32| x + 5)) + .depends_on(a); + let c = dag + .add_task(task_fn::(|&x: &i32| x * 2)) + .depends_on(a); + let d = dag + .add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x + y)) + .depends_on((&b, &c)); + + dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + .await + .unwrap(); + + // Note: a, b, and c are not sinks (they have dependents) + // Only d is a sink and can be retrieved + assert_eq!(dag.get(d).unwrap(), 35); // 15 + 20 +} + +#[derive(Clone)] +struct User { + name: String, + age: u32, +} + +#[tokio::test] +async fn test_different_output_types() { + let dag = DagRunner::new(); + + let string_task = dag.add_task(task_fn::<(), _, _>(|_: ()| "hello".to_string())); + let vec_task = dag.add_task(task_fn::<(), _, _>(|_: ()| vec![1, 2, 3])); + let struct_task = dag.add_task(task_fn::<(), _, _>(|_: ()| User { + name: "Alice".to_string(), + age: 30, + })); + + dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + .await + .unwrap(); + + assert_eq!(&dag.get(string_task).unwrap(), "hello"); + assert_eq!(dag.get(vec_task).unwrap(), vec![1, 2, 3]); + let user = dag.get(struct_task).unwrap(); + assert_eq!(user.name, "Alice"); + assert_eq!(user.age, 30); +} + +#[tokio::test] +async fn test_multiple_sinks() { + let dag = DagRunner::new(); + + let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into(); + + // Branch 1 + let branch1 = dag + .add_task(task_fn::(|&x: &i32| x * 2)) + .depends_on(source); + let sink1 = dag + .add_task(task_fn::(|&x: &i32| x + 1)) + .depends_on(branch1); + + // Branch 2 + let branch2 = dag + .add_task(task_fn::(|&x: &i32| x + 5)) + .depends_on(source); + let sink2 = dag + .add_task(task_fn::(|&x: &i32| x * 3)) + .depends_on(branch2); + + dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + .await + .unwrap(); // Should wait for both sinks + + assert_eq!(dag.get(sink1).unwrap(), 21); // (10 * 2) + 1 + assert_eq!(dag.get(sink2).unwrap(), 45); // (10 + 5) * 3 +} + +#[tokio::test] +async fn test_single_task_inline_path() { + // Test single-task layer inline execution path (line 367-410 in runner.rs) + let dag = DagRunner::new(); + + // Single task in its own layer + let t1: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)).into(); + let t2 = dag + .add_task(task_fn::(|&x: &i32| x * 2)) + .depends_on(t1); + let t3 = dag + .add_task(task_fn::(|&x: &i32| x + 5)) + .depends_on(t2); + + dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + .await + .unwrap(); + + assert_eq!(dag.get(t3).unwrap(), 25); // (10 * 2) + 5 +} + +#[tokio::test] +async fn test_multi_consumer_fanout() { + // Test fanout with multiple consumers from one producer + let dag = DagRunner::new(); + + let producer: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)).into(); + + // Multiple consumers + let c1 = dag + .add_task(task_fn::(|&x: &i32| x * 2)) + .depends_on(producer); + let c2 = dag + .add_task(task_fn::(|&x: &i32| x + 10)) + .depends_on(producer); + let c3 = dag + .add_task(task_fn::(|&x: &i32| x - 5)) + .depends_on(producer); + + dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + .await + .unwrap(); + + assert_eq!(dag.get(c1).unwrap(), 84); + assert_eq!(dag.get(c2).unwrap(), 52); + assert_eq!(dag.get(c3).unwrap(), 37); +} diff --git a/tests/execution/results.rs b/tests/execution/results.rs deleted file mode 100644 index bf234a4..0000000 --- a/tests/execution/results.rs +++ /dev/null @@ -1,117 +0,0 @@ -// Result and error handling tests for DagRunner -use crate::common::task_fn; - -use crate::common::tasks::Counter; -use dagx::*; - -#[tokio::test] -async fn test_result_type() { - let dag = DagRunner::new(); - - let success_task = dag.add_task(task_fn::<(), _, _>(|_: ()| Ok::(42))); - let error_task = dag.add_task(task_fn::<(), _, _>(|_: ()| { - Err::("error".to_string()) - })); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(success_task).unwrap(), Ok(42)); - assert_eq!(dag.get(error_task).unwrap(), Err("error".to_string())); -} - -#[tokio::test] -async fn test_dag_result_type_works() { - let dag = DagRunner::new(); - let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)); - - // Test that DagResult<()> works with ? operator pattern - async fn run_dag(dag: &DagRunner) -> DagResult<()> { - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - Ok(()) - } - - let result = run_dag(&dag).await; - assert!(result.is_ok()); - assert_eq!(dag.get(task).unwrap(), 100); -} - -#[tokio::test] -async fn test_task_not_executed_error() { - let dag = DagRunner::new(); - let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); - - // Try to get result before running - should error - let result = dag.get(task); - assert!(result.is_err()); - match result { - Err(DagError::ResultNotFound { task_id }) => { - assert_eq!(task_id, 0); // First task has ID 0 - } - _ => panic!("Expected ResultNotFound error"), - } -} - -#[tokio::test] -async fn test_error_messages_are_helpful() { - // Test ResultNotFound error message - let dag = DagRunner::new(); - let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); - let result = dag.get(task); - assert!(result.is_err()); - let err_msg = format!("{}", result.unwrap_err()); - assert!(err_msg.contains("Result not found")); - assert!(err_msg.contains("task #0")); // Task ID - assert!(err_msg.contains("Call dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() })")); -} - -#[tokio::test] -async fn test_successful_execution_returns_ok() { - let dag = DagRunner::new(); - let task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); - - // Should return Ok(()) - let result = dag - .run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await; - assert!(result.is_ok()); - - // Should be able to get value - assert_eq!(dag.get(task).unwrap(), 42); -} - -#[tokio::test] -async fn test_stateful_tasks() { - let dag = DagRunner::new(); - - let input = dag.add_task(task_fn::<(), _, _>(|_: ()| 5)); - let counter = dag.add_task(Counter::new(10)).depends_on(input); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(counter).unwrap(), 15); // 10 + 5 -} - -// Cycle detection test removed - it required access to private fields -// The public API correctly prevents creating cycles, so this is tested implicitly - -#[tokio::test] -async fn test_task_fn_with_captured_state() { - let dag = DagRunner::new(); - - let multiplier = 7; - let base = dag.add_task(task_fn::<(), _, _>(|_: ()| 5)); - let scaled = dag - .add_task(task_fn::(move |&x: &i32| x * multiplier)) - .depends_on(base); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(scaled).unwrap(), 35); -} diff --git a/tests/lib_tests.rs b/tests/lib_tests.rs index 53f6e66..3fecea2 100644 --- a/tests/lib_tests.rs +++ b/tests/lib_tests.rs @@ -4,18 +4,11 @@ #![allow(clippy::needless_borrow)] #![allow(clippy::redundant_clone)] -#[path = "common/mod.rs"] -mod common; - mod boundaries; mod dependencies; mod errors; mod execution; mod parallelism; -mod patterns; -mod performance; mod runtimes; -mod task_patterns; mod timing; mod tracing; -mod type_safety; diff --git a/tests/parallelism/concurrent.rs b/tests/parallelism/concurrent.rs index d7d70b1..fd746a6 100644 --- a/tests/parallelism/concurrent.rs +++ b/tests/parallelism/concurrent.rs @@ -1,7 +1,7 @@ //! Concurrent access tests -use crate::common::task_fn; use dagx::{task, DagRunner}; +use dagx_test::task_fn; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; diff --git a/tests/parallelism/execution.rs b/tests/parallelism/execution.rs deleted file mode 100644 index c2f1cc0..0000000 --- a/tests/parallelism/execution.rs +++ /dev/null @@ -1,333 +0,0 @@ -//! Tests for execution correctness and parallelism - -use crate::common::task_fn; -use dagx::{task, DagResult, DagRunner, TaskHandle}; - -use std::sync::{Arc, Mutex}; -use std::time::{Duration, Instant}; -use tokio::time::sleep; - -#[tokio::test] -async fn test_layers_execute_in_parallel() -> DagResult<()> { - // Test that tasks in the same topological layer execute in parallel - let dag = DagRunner::new(); - - let layer_timing = Arc::new(Mutex::new(Vec::new())); - - struct Layer0Task { - timing: Arc>>, - idx: i32, - } - - #[task] - impl Layer0Task { - async fn run(&self) -> i32 { - let start = Instant::now(); - self.timing - .lock() - .unwrap() - .push((format!("L0_{}", self.idx), start)); - sleep(Duration::from_millis(20)).await; - self.idx * 10 - } - } - - // Layer 0: 3 independent sources - let sources: Vec<_> = (0..3) - .map(|i| { - dag.add_task(Layer0Task { - timing: layer_timing.clone(), - idx: i, - }) - }) - .collect(); - - struct Layer1Task { - timing: Arc>>, - idx: usize, - } - - #[task] - impl Layer1Task { - async fn run(&self, x: &i32) -> i32 { - let start = Instant::now(); - self.timing - .lock() - .unwrap() - .push((format!("L1_{}", self.idx), start)); - sleep(Duration::from_millis(20)).await; - x + 1 - } - } - - // Layer 1: Tasks depending on layer 0 - let layer1: Vec<_> = sources - .into_iter() - .enumerate() - .map(|(i, source)| { - dag.add_task(Layer1Task { - idx: i, - timing: layer_timing.clone(), - }) - .depends_on(source) - }) - .collect(); - - // Layer 2: Final aggregation - let final_task = { - let timing = layer_timing.clone(); - dag.add_task(task_fn::<(i32, i32, i32), _, _>( - move |(a, b, c): (&i32, &i32, &i32)| { - let timing = timing.clone(); - let start = Instant::now(); - timing.lock().unwrap().push(("L2_final".to_string(), start)); - a + b + c - }, - )) - .depends_on((&layer1[0], &layer1[1], &layer1[2])) - }; - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(dag.get(final_task)?, 33); // (0+1) + (10+1) + (20+1) - - // Analyze timing - let timing = layer_timing.lock().unwrap(); - - // Get layer 0 start times - let l0_times: Vec = (0..3) - .map(|i| { - timing - .iter() - .find(|(name, _)| name == &format!("L0_{}", i)) - .map(|(_, t)| *t) - .unwrap() - }) - .collect(); - - // Get layer 1 start times - let l1_times: Vec = (0..3) - .map(|i| { - timing - .iter() - .find(|(name, _)| name == &format!("L1_{}", i)) - .map(|(_, t)| *t) - .unwrap() - }) - .collect(); - - // Layer 0 tasks should all start nearly simultaneously - let l0_spread = l0_times - .iter() - .max() - .unwrap() - .duration_since(*l0_times.iter().min().unwrap()); - assert!( - l0_spread < Duration::from_millis(5), - "Layer 0 tasks should start together, spread was {:?}", - l0_spread - ); - - // Layer 1 tasks should also start nearly simultaneously (after layer 0 completes) - let l1_spread = l1_times - .iter() - .max() - .unwrap() - .duration_since(*l1_times.iter().min().unwrap()); - assert!( - l1_spread < Duration::from_millis(5), - "Layer 1 tasks should start together, spread was {:?}", - l1_spread - ); - - // Layer 1 should start after layer 0 - let earliest_l1 = *l1_times.iter().min().unwrap(); - let earliest_l0 = *l0_times.iter().min().unwrap(); - assert!( - earliest_l1 >= earliest_l0 + Duration::from_millis(15), - "Layer 1 should start after layer 0 completes" - ); - - Ok(()) -} - -#[tokio::test] -async fn test_completion_order_matches_dependencies() -> DagResult<()> { - // Verify that tasks complete in dependency order - let dag = DagRunner::new(); - - let completion_order = Arc::new(Mutex::new(Vec::new())); - - struct ATask { - order: Arc>>, - } - - #[task] - impl ATask { - async fn run(&self) -> i32 { - sleep(Duration::from_millis(10)).await; - self.order.lock().unwrap().push("A"); - 1 - } - } - - // Create a complex DAG with clear ordering requirements - let a = dag.add_task(ATask { - order: completion_order.clone(), - }); - - struct BTask { - order: Arc>>, - } - - #[task] - impl BTask { - async fn run(&self) -> i32 { - sleep(Duration::from_millis(5)).await; - self.order.lock().unwrap().push("B"); - 2 - } - } - - let b = dag.add_task(BTask { - order: completion_order.clone(), - }); - - let c = { - let order = completion_order.clone(); - dag.add_task(task_fn::<(i32, i32), _, _>(move |(x, y): (&i32, &i32)| { - let order = order.clone(); - order.lock().unwrap().push("C"); - x + y - })) - .depends_on((a, b)) - }; - - let d = { - let order = completion_order.clone(); - dag.add_task(task_fn::(move |&x: &i32| { - let order = order.clone(); - order.lock().unwrap().push("D"); - x * 2 - })) - .depends_on(c) - }; - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(dag.get(d)?, 6); // (1 + 2) * 2 - - // Check completion order - let order = completion_order.lock().unwrap(); - - // A and B can complete in any order (parallel) - assert!(order.contains(&"A")); - assert!(order.contains(&"B")); - - // C must come after both A and B - let c_pos = order.iter().position(|name| *name == "C").unwrap(); - let a_pos = order.iter().position(|name| *name == "A").unwrap(); - let b_pos = order.iter().position(|name| *name == "B").unwrap(); - assert!(c_pos > a_pos, "C should complete after A"); - assert!(c_pos > b_pos, "C should complete after B"); - - // D must come after C - let d_pos = order.iter().position(|name| *name == "D").unwrap(); - assert!(d_pos > c_pos, "D should complete after C"); - - Ok(()) -} - -#[tokio::test] -async fn test_dependent_tasks_respect_ordering() -> DagResult<()> { - // Prove that dependent tasks wait for their dependencies - let dag = DagRunner::new(); - - let execution_order = Arc::new(Mutex::new(Vec::new())); - - struct ATask { - order: Arc>>, - } - - #[task] - impl ATask { - async fn run(&self) -> i32 { - sleep(Duration::from_millis(20)).await; - self.order.lock().unwrap().push("A"); - 1 - } - } - - // Create a chain: A -> B -> C -> D - let a: TaskHandle<_> = dag - .add_task(ATask { - order: execution_order.clone(), - }) - .into(); - - struct BTask { - order: Arc>>, - } - - #[task] - impl BTask { - async fn run(&self, x: &i32) -> i32 { - self.order.lock().unwrap().push("B"); - sleep(Duration::from_millis(10)).await; - x + 1 - } - } - - let b = dag - .add_task(BTask { - order: execution_order.clone(), - }) - .depends_on(a); - - struct CTask { - order: Arc>>, - } - - #[task] - impl CTask { - async fn run(&self, x: &i32) -> i32 { - self.order.lock().unwrap().push("C"); - sleep(Duration::from_millis(10)).await; - x + 1 - } - } - - let c = dag - .add_task(CTask { - order: execution_order.clone(), - }) - .depends_on(b); - - let d = { - let order = execution_order.clone(); - dag.add_task(task_fn::(move |&x: &i32| { - let order = order.clone(); - order.lock().unwrap().push("D"); - x + 1 - })) - .depends_on(c) - }; - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Verify results - assert_eq!(dag.get(a)?, 1); - assert_eq!(dag.get(b)?, 2); - assert_eq!(dag.get(c)?, 3); - assert_eq!(dag.get(d)?, 4); - - // Verify execution order - let order = execution_order.lock().unwrap(); - let expected = vec!["A", "B", "C", "D"]; - assert_eq!(order.as_slice(), expected.as_slice()); - - Ok(()) -} diff --git a/tests/parallelism/mod.rs b/tests/parallelism/mod.rs index 984f444..93421db 100644 --- a/tests/parallelism/mod.rs +++ b/tests/parallelism/mod.rs @@ -1,7 +1,6 @@ //! Parallelism tests module pub mod concurrent; -pub mod execution; pub mod spawning; pub mod threads; pub mod timing; diff --git a/tests/parallelism/spawning.rs b/tests/parallelism/spawning.rs index 39a9a38..1ddf148 100644 --- a/tests/parallelism/spawning.rs +++ b/tests/parallelism/spawning.rs @@ -1,7 +1,7 @@ //! Tests for spawner functionality -use crate::common::task_fn; use dagx::{DagResult, DagRunner}; +use dagx_test::task_fn; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; diff --git a/tests/parallelism/threads.rs b/tests/parallelism/threads.rs index f424c69..59cbd29 100644 --- a/tests/parallelism/threads.rs +++ b/tests/parallelism/threads.rs @@ -1,81 +1,13 @@ //! Tests for multi-threading and concurrent execution -use crate::common::task_fn; use dagx::{task, DagResult, DagRunner, TaskHandle}; +use dagx_test::task_fn; -use std::collections::HashSet; use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex}; -use std::thread::ThreadId; +use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::time::sleep; -#[tokio::test] -async fn test_tasks_run_on_different_threads() -> DagResult<()> { - // Prove that tasks CAN run on multiple different threads (true parallelism capability) - // Note: Tokio may choose to run fast tasks on the same thread for efficiency - use std::thread; - - let dag = DagRunner::new(); - let thread_ids = Arc::new(Mutex::new(std::collections::HashSet::new())); - - struct LayerTask { - ids: Arc>>, - idx: i32, - } - - #[task] - impl LayerTask { - async fn run(&self) -> i32 { - let tid = thread::current().id(); - self.ids.lock().unwrap().insert(tid); - - // Yield to give tokio reason to distribute - sleep(Duration::from_millis(5)).await; - - // Some CPU work too - let mut sum = 0; - for j in 0..1000 { - sum += j; - } - self.idx + sum - } - } - - // Create 50 tasks with actual work to encourage multi-threading - let tasks: Vec<_> = (0..50) - .map(|i| { - dag.add_task(LayerTask { - ids: thread_ids.clone(), - idx: i, - }) - }) - .collect(); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Verify all tasks completed - for task in tasks.into_iter() { - let _ = dag.get(task)?; - } - - // With a multi-threaded runtime and sufficient work, we typically see multiple threads - // But we can't guarantee it (scheduler's choice), so we just verify the capability exists - let unique_threads = thread_ids.lock().unwrap().len(); - - // On a multi-core system with sufficient work, tokio should use multiple threads - // This is a soft assertion - if it fails, it means tasks CAN'T use multiple threads - if unique_threads == 1 { - eprintln!( - "Warning: All tasks ran on 1 thread. This may indicate parallelism isn't working, \ - or tokio chose not to distribute work (valid for small/fast tasks)" - ); - } - - Ok(()) -} - #[tokio::test] async fn test_concurrent_execution_with_atomic_counter() -> DagResult<()> { // Prove concurrent execution using atomic counters to track simultaneous tasks @@ -155,73 +87,6 @@ async fn test_concurrent_execution_with_atomic_counter() -> DagResult<()> { Ok(()) } -#[tokio::test] -async fn test_diamond_parallel_execution() -> DagResult<()> { - // Test diamond pattern: source -> (parallel1, parallel2) -> sink - let dag = DagRunner::new(); - - let concurrent_middle = Arc::new(AtomicUsize::new(0)); - - let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)).into(); - - struct ParallelTask1 { - counter: Arc, - } - - #[task] - impl ParallelTask1 { - async fn run(&self, x: &i32) -> i32 { - self.counter.fetch_add(1, Ordering::SeqCst); - sleep(Duration::from_millis(20)).await; - let count = self.counter.load(Ordering::SeqCst); - self.counter.fetch_sub(1, Ordering::SeqCst); - assert!(count > 0, "Should see concurrent execution"); - x * 2 - } - } - - // These should run in parallel - let parallel1 = dag - .add_task(ParallelTask1 { - counter: concurrent_middle.clone(), - }) - .depends_on(source); - - struct ParallelTask2 { - counter: Arc, - } - - #[task] - impl ParallelTask2 { - async fn run(&self, x: &i32) -> i32 { - self.counter.fetch_add(1, Ordering::SeqCst); - sleep(Duration::from_millis(20)).await; - let count = self.counter.load(Ordering::SeqCst); - self.counter.fetch_sub(1, Ordering::SeqCst); - // Both should be running - assert!(count > 0, "Should see concurrent execution"); - x * 3 - } - } - - let parallel2 = dag - .add_task(ParallelTask2 { - counter: concurrent_middle.clone(), - }) - .depends_on(source); - - let sink = dag - .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b)) - .depends_on((¶llel1, ¶llel2)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(dag.get(sink)?, 500); // (100 * 2) + (100 * 3) - - Ok(()) -} - #[tokio::test] async fn test_massive_parallel_fanout() -> DagResult<()> { // Test extreme parallelism with 1000 parallel tasks diff --git a/tests/parallelism/timing.rs b/tests/parallelism/timing.rs index 5d1aaa5..8903ea0 100644 --- a/tests/parallelism/timing.rs +++ b/tests/parallelism/timing.rs @@ -1,8 +1,12 @@ //! Tests for timing and speedup verification use dagx::{task, DagResult, DagRunner}; +use dagx_test::task_fn; -use std::time::{Duration, Instant}; +use std::{ + sync::{Arc, Mutex}, + time::{Duration, Instant}, +}; use tokio::time::sleep; struct SleepTask(usize, Duration); @@ -49,7 +53,7 @@ async fn test_parallel_execution_speedup() -> DagResult<()> { // Parallel execution should be significantly faster assert!( - elapsed < sequential_time / 2, + elapsed < sequential_time / 4, "Parallel execution too slow: {:?} vs sequential {:?}", elapsed, sequential_time @@ -57,3 +61,148 @@ async fn test_parallel_execution_speedup() -> DagResult<()> { Ok(()) } + +#[tokio::test] +async fn test_layers_execute_in_parallel() -> DagResult<()> { + // Test that tasks in the same topological layer execute in parallel + let dag = DagRunner::new(); + + let layer_timing = Arc::new(Mutex::new(Vec::new())); + + struct Layer0Task { + timing: Arc>>, + idx: i32, + } + + #[task] + impl Layer0Task { + async fn run(&self) -> i32 { + let start = Instant::now(); + self.timing + .lock() + .unwrap() + .push((format!("L0_{}", self.idx), start)); + sleep(Duration::from_millis(20)).await; + self.idx * 10 + } + } + + // Layer 0: 3 independent sources + let sources: Vec<_> = (0..3) + .map(|i| { + dag.add_task(Layer0Task { + timing: layer_timing.clone(), + idx: i, + }) + }) + .collect(); + + struct Layer1Task { + timing: Arc>>, + idx: usize, + } + + #[task] + impl Layer1Task { + async fn run(&self, x: &i32) -> i32 { + let start = Instant::now(); + self.timing + .lock() + .unwrap() + .push((format!("L1_{}", self.idx), start)); + sleep(Duration::from_millis(20)).await; + x + 1 + } + } + + // Layer 1: Tasks depending on layer 0 + let layer1: Vec<_> = sources + .into_iter() + .enumerate() + .map(|(i, source)| { + dag.add_task(Layer1Task { + idx: i, + timing: layer_timing.clone(), + }) + .depends_on(source) + }) + .collect(); + + // Layer 2: Final aggregation + let final_task = { + let timing = layer_timing.clone(); + dag.add_task(task_fn::<(i32, i32, i32), _, _>( + move |(a, b, c): (&i32, &i32, &i32)| { + let timing = timing.clone(); + let start = Instant::now(); + timing.lock().unwrap().push(("L2_final".to_string(), start)); + a + b + c + }, + )) + .depends_on((&layer1[0], &layer1[1], &layer1[2])) + }; + + dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) + .await?; + + assert_eq!(dag.get(final_task)?, 33); // (0+1) + (10+1) + (20+1) + + // Analyze timing + let timing = layer_timing.lock().unwrap(); + + // Get layer 0 start times + let l0_times: Vec = (0..3) + .map(|i| { + timing + .iter() + .find(|(name, _)| name == &format!("L0_{}", i)) + .map(|(_, t)| *t) + .unwrap() + }) + .collect(); + + // Get layer 1 start times + let l1_times: Vec = (0..3) + .map(|i| { + timing + .iter() + .find(|(name, _)| name == &format!("L1_{}", i)) + .map(|(_, t)| *t) + .unwrap() + }) + .collect(); + + // Layer 0 tasks should all start nearly simultaneously + let l0_spread = l0_times + .iter() + .max() + .unwrap() + .duration_since(*l0_times.iter().min().unwrap()); + assert!( + l0_spread < Duration::from_millis(5), + "Layer 0 tasks should start together, spread was {:?}", + l0_spread + ); + + // Layer 1 tasks should also start nearly simultaneously (after layer 0 completes) + let l1_spread = l1_times + .iter() + .max() + .unwrap() + .duration_since(*l1_times.iter().min().unwrap()); + assert!( + l1_spread < Duration::from_millis(5), + "Layer 1 tasks should start together, spread was {:?}", + l1_spread + ); + + // Layer 1 should start after layer 0 + let earliest_l1 = *l1_times.iter().min().unwrap(); + let earliest_l0 = *l0_times.iter().min().unwrap(); + assert!( + earliest_l1 >= earliest_l0 + Duration::from_millis(15), + "Layer 1 should start after layer 0 completes" + ); + + Ok(()) +} diff --git a/tests/patterns/butterfly.rs b/tests/patterns/butterfly.rs deleted file mode 100644 index 2b53112..0000000 --- a/tests/patterns/butterfly.rs +++ /dev/null @@ -1,228 +0,0 @@ -//! Butterfly pattern DAG tests - -use crate::common::task_fn; -use dagx::{DagResult, DagRunner, TaskHandle}; - -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; - -#[tokio::test] -async fn test_butterfly_network() -> DagResult<()> { - let dag = DagRunner::new(); - - // Simplified butterfly network - avoid tuples entirely - // Layer 0: inputs - let inputs: Vec<_> = (0..8) - .map(|i| { - dag.add_task(task_fn::<(), _, _>(move |_: ()| i as f64)) - .into() - }) - .collect(); - - // Layer 1: butterfly operations (compute pairs separately) - let mut layer1 = Vec::new(); - for i in 0..4 { - let idx1 = i * 2; - let idx2 = i * 2 + 1; - // Sum operation - layer1.push( - dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| { - (a + b) / 2.0 - })) - .depends_on((&inputs[idx1], &inputs[idx2])), - ); - // Difference operation - layer1.push( - dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| { - (a - b) / 2.0 - })) - .depends_on((&inputs[idx1], &inputs[idx2])), - ); - } - - // Layer 2: crossover connections - let layer2: Vec<_> = vec![ - dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| a + b)) - .depends_on((&layer1[0], &layer1[4])), - dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| a - b)) - .depends_on((&layer1[1], &layer1[5])), - dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| a + b)) - .depends_on((&layer1[2], &layer1[6])), - dag.add_task(task_fn::<(f64, f64), _, _>(|(a, b): (&f64, &f64)| a - b)) - .depends_on((&layer1[3], &layer1[7])), - ]; - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Verify some outputs exist - for task in &layer2 { - let _ = dag.get(task)?; - } - - Ok(()) -} - -#[tokio::test] -async fn test_recursive_butterfly() -> DagResult<()> { - let dag = DagRunner::new(); - - fn build_butterfly( - dag: &DagRunner, - inputs: Vec>, - ) -> Vec> { - if inputs.len() <= 2 { - return inputs; - } - - let mid = inputs.len() / 2; - let mut outputs = Vec::new(); - - // Butterfly connections - for i in 0..mid { - let sum = dag - .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b)) - .depends_on((&inputs[i], &inputs[i + mid])); - - let diff = dag - .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| { - (a - b).abs() - })) - .depends_on((&inputs[i], &inputs[i + mid])); - - outputs.push(sum); - outputs.push(diff); - } - - outputs - } - - // Create 4 inputs - let inputs: Vec> = vec![10, 20, 30, 40] - .into_iter() - .map(|val| { - let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| val)); - task.into() - }) - .collect(); - - let outputs = build_butterfly(&dag, inputs); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(outputs.len(), 4); - for output in outputs { - let _ = dag.get(output)?; - } - - Ok(()) -} - -#[tokio::test] -async fn test_benes_network() -> DagResult<()> { - // Simplified Benes network - avoid tuples - let dag = DagRunner::new(); - - // 4 inputs - let inputs: Vec<_> = (0..4) - .map(|i| { - dag.add_task(task_fn::<(), _, _>(move |_: ()| 1 << i)) - .into() - }) // Powers of 2 - .collect(); - - // First stage: sort pairs - let stage1: Vec<_> = vec![ - dag.add_task(task_fn::<(i32, i32), _, _>(|(&a, &b): (&i32, &i32)| { - a.min(b) - })) - .depends_on((&inputs[0], &inputs[1])), - dag.add_task(task_fn::<(i32, i32), _, _>(|(&a, &b): (&i32, &i32)| { - a.max(b) - })) - .depends_on((&inputs[0], &inputs[1])), - dag.add_task(task_fn::<(i32, i32), _, _>(|(&a, &b): (&i32, &i32)| { - a.min(b) - })) - .depends_on((&inputs[2], &inputs[3])), - dag.add_task(task_fn::<(i32, i32), _, _>(|(&a, &b): (&i32, &i32)| { - a.max(b) - })) - .depends_on((&inputs[2], &inputs[3])), - ]; - - // Second stage with crossover - let outputs: Vec<_> = vec![ - dag.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a | b)) - .depends_on((&stage1[0], &stage1[2])), - dag.add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a | b)) - .depends_on((&stage1[1], &stage1[3])), - ]; - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - for output in outputs { - let _ = dag.get(output)?; - } - - Ok(()) -} - -#[tokio::test] -async fn test_shuffle_exchange_network() -> DagResult<()> { - let dag = DagRunner::new(); - let counter = Arc::new(AtomicUsize::new(0)); - - // Create 8 inputs - let inputs: Vec> = (0..8) - .map(|i| { - let counter = counter.clone(); - dag.add_task(task_fn::<(), _, _>(move |_: ()| { - let counter = counter.clone(); - counter.fetch_add(1, Ordering::Relaxed); - i - })) - .into() - }) - .collect(); - - // Shuffle stage: bit rotation - let shuffled: Vec<_> = (0..8) - .map(|i| { - // Shuffle pattern: rotate bits - let source = ((i << 1) | (i >> 2)) & 0x7; - dag.add_task(task_fn::(move |&x: &i32| x)) - .depends_on(inputs[source]) - }) - .collect(); - - // Exchange stage: adjacent pairs - let exchanged: Vec<_> = (0..4) - .flat_map(|i| { - let idx = i * 2; - vec![ - dag.add_task(task_fn::<(i32, i32), _, _>( - |(&a, &b): (&i32, &i32)| if a > b { b } else { a }, - )) - .depends_on((&shuffled[idx], &shuffled[idx + 1])), - dag.add_task(task_fn::<(i32, i32), _, _>( - |(&a, &b): (&i32, &i32)| if a > b { a } else { b }, - )) - .depends_on((&shuffled[idx], &shuffled[idx + 1])), - ] - }) - .collect(); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(counter.load(Ordering::Relaxed), 8); - - for task in exchanged { - let _ = dag.get(task)?; - } - - Ok(()) -} diff --git a/tests/patterns/complex.rs b/tests/patterns/complex.rs deleted file mode 100644 index 4fee8b2..0000000 --- a/tests/patterns/complex.rs +++ /dev/null @@ -1,337 +0,0 @@ -//! Complex DAG pattern tests - -use crate::common::task_fn; -use dagx::{DagResult, DagRunner, TaskHandle}; - -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; - -#[tokio::test] -async fn test_workflow_with_conditional_paths() -> DagResult<()> { - let dag = DagRunner::new(); - - // Input validation - let input = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); - - let validate = dag - .add_task(task_fn::(|&x: &i32| { - if x > 0 && x < 100 { - Ok(x) - } else { - Err("Invalid input") - } - })) - .depends_on(input); - - // Process valid input - let process = dag - .add_task(task_fn::, _, _>( - |result: &Result| match result { - Ok(x) => *x * 2, - Err(_) => -1, - }, - )) - .depends_on(validate); - - // Parallel enhancement steps - let enhance1 = dag - .add_task(task_fn::(|&x: &i32| x + 10)) - .depends_on(process); - - let enhance2 = dag - .add_task(task_fn::(|&x: &i32| x * 3)) - .depends_on(process); - - let enhance3 = dag - .add_task(task_fn::(|&x: &i32| x - 5)) - .depends_on(process); - - // Combine enhancements - let combine = dag - .add_task(task_fn::<(i32, i32, i32), _, _>( - |(&e1, &e2, &e3): (&i32, &i32, &i32)| vec![e1, e2, e3], - )) - .depends_on((&enhance1, &enhance2, &enhance3)); - - // Final aggregation - let final_result = dag - .add_task(task_fn::, _, _>(|values: &Vec| { - values.iter().sum::() - })) - .depends_on(combine); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(dag.get(process)?, 84); - let result = dag.get(final_result)?; - assert!(result > 0); - - Ok(()) -} - -#[tokio::test] -async fn test_map_reduce_pattern() -> DagResult<()> { - let dag = DagRunner::new(); - - // Map phase: process 16 inputs in parallel - let mappers: Vec>> = (0..16) - .map(|i| { - let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| { - // Simulate word count - vec![("word1", i % 3), ("word2", i % 5), ("word3", i % 7)] - })); - task.into() - }) - .collect(); - - // Shuffle phase: group by key (simplified - just 4 reducers) - let shuffled: Vec<_> = (0..4) - .map(|reducer_id| { - // Each reducer gets data from 4 mappers - let mapper_indices: Vec = (0..4).map(|i| reducer_id * 4 + i).collect(); - - let deps: Vec<_> = mapper_indices.iter().map(|&i| mappers[i]).collect(); - - match deps.len() { - 4 => dag - .add_task(task_fn::< - ( - Vec<(&str, usize)>, - Vec<(&str, usize)>, - Vec<(&str, usize)>, - Vec<(&str, usize)>, - ), - _, - _, - >( - move |inputs: ( - &Vec<(&str, usize)>, - &Vec<(&str, usize)>, - &Vec<(&str, usize)>, - &Vec<(&str, usize)>, - )| { - let mut combined = Vec::<(&str, usize)>::new(); - combined.extend(inputs.0); - combined.extend(inputs.1); - combined.extend(inputs.2); - combined.extend(inputs.3); - combined - }, - )) - .depends_on((&deps[0], &deps[1], &deps[2], &deps[3])), - _ => unreachable!(), - } - }) - .collect(); - - // Reduce phase - let reducers: Vec<_> = shuffled - .iter() - .map(|shuffled_data| { - dag.add_task(task_fn::, _, _>(|data: &Vec<(&str, usize)>| { - // Sum values by key - let mut totals = std::collections::HashMap::new(); - for (key, value) in data { - *totals.entry(*key).or_insert(0) += value; - } - totals.len() - })) - .depends_on(shuffled_data) - }) - .collect(); - - // Final aggregation - let final_task = dag - .add_task(task_fn::<(_, _, _, _), _, _>( - |(r1, r2, r3, r4): (&usize, &usize, &usize, &usize)| r1 + r2 + r3 + r4, - )) - .depends_on((&reducers[0], &reducers[1], &reducers[2], &reducers[3])); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - let result = dag.get(final_task)?; - assert!(result > 0); - - Ok(()) -} - -#[tokio::test] -async fn test_pipeline_with_feedback() -> DagResult<()> { - let dag = DagRunner::new(); - let iteration_count = Arc::new(AtomicUsize::new(0)); - - // Initial value - let init = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)); - - // Stage 1: Transform - let stage1 = dag - .add_task(task_fn::({ - let counter = iteration_count.clone(); - move |x: &i32| { - let counter = counter.clone(); - counter.fetch_add(1, Ordering::SeqCst); - x * 2 - } - })) - .depends_on(init); - - // Stage 2: Filter - let stage2 = dag - .add_task(task_fn::( - |&x: &i32| if x < 100 { Some(x) } else { None }, - )) - .depends_on(stage1); - - // Stage 3: Accumulate - let stage3 = dag - .add_task(task_fn::, _, _>(|opt: &Option| { - opt.map(|x| x + 10).unwrap_or(0) - })) - .depends_on(stage2); - - // Note: Can't create actual cycles in DAG, but we simulate feedback - // by having a separate "feedback" task that would feed back in a real system - let _feedback = dag - .add_task(task_fn::(|&x: &i32| { - // In a real feedback loop, this would feed back to stage1 - x / 2 - })) - .depends_on(stage3); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(dag.get(stage1)?, 2); - assert_eq!(dag.get(stage3)?, 12); - assert_eq!(iteration_count.load(Ordering::SeqCst), 1); - - Ok(()) -} - -#[tokio::test] -async fn test_scatter_gather_pattern() -> DagResult<()> { - let dag = DagRunner::new(); - - // Scatter: distribute work to multiple workers - let source: TaskHandle<_> = dag - .add_task(task_fn::<(), _, _>(|_: ()| { - vec![10, 20, 30, 40, 50, 60, 70, 80] - })) - .into(); - - // Workers process chunks - let workers: Vec<_> = (0..4) - .map(|worker_id| { - dag.add_task(task_fn::, _, _>(move |data: &Vec| { - // Each worker processes a chunk - let chunk_size = data.len() / 4; - let start = worker_id * chunk_size; - let end = if worker_id == 3 { - data.len() - } else { - start + chunk_size - }; - - data[start..end].iter().sum::() - })) - .depends_on(source) - }) - .collect(); - - // Gather: collect results from all workers - let gather = dag - .add_task(task_fn::<(i32, i32, i32, i32), _, _>( - |(w0, w1, w2, w3): (&i32, &i32, &i32, &i32)| w0 + w1 + w2 + w3, - )) - .depends_on((&workers[0], &workers[1], &workers[2], &workers[3])); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Sum of 10+20+30+40+50+60+70+80 = 360 - assert_eq!(dag.get(gather)?, 360); - - Ok(()) -} - -#[tokio::test] -async fn test_fork_join_with_barriers() -> DagResult<()> { - let dag = DagRunner::new(); - let phase_counter = Arc::new(AtomicUsize::new(0)); - - // Fork phase - let parent = dag.add_task(task_fn::<(), _, _>({ - let counter = phase_counter.clone(); - move |_: ()| { - let counter = counter.clone(); - counter.store(1, Ordering::SeqCst); - 100 - } - })); - - // Multiple parallel paths - let mut path1 = Vec::new(); - let parent_handle: dagx::TaskHandle = parent.into(); - let mut prev1 = parent_handle; - for i in 0..3 { - let counter = phase_counter.clone(); - let task = dag - .add_task(task_fn::(move |&x: &i32| { - let counter = counter.clone(); - assert_eq!(counter.load(Ordering::SeqCst), 1); - x + i - })) - .depends_on(prev1); - path1.push(task); - prev1 = task; - } - - let mut path2 = Vec::new(); - let mut prev2 = parent_handle; - for i in 0..3 { - let counter = phase_counter.clone(); - let task = dag - .add_task(task_fn::(move |&x: &i32| { - let counter = counter.clone(); - assert_eq!(counter.load(Ordering::SeqCst), 1); - x * 2 + i - })) - .depends_on(prev2); - path2.push(task); - prev2 = task; - } - - // Barrier: wait for all paths - let barrier = dag - .add_task(task_fn::<(i32, i32), _, _>({ - let counter = phase_counter.clone(); - move |(p1, p2): (&i32, &i32)| { - let counter = counter.clone(); - counter.store(2, Ordering::SeqCst); - p1 + p2 - } - })) - .depends_on((&path1[2], &path2[2])); - - // Join phase - let join = dag - .add_task(task_fn::({ - let counter = phase_counter.clone(); - move |&x: &i32| { - let counter = counter.clone(); - assert_eq!(counter.load(Ordering::SeqCst), 2); - x - } - })) - .depends_on(barrier); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - let _ = dag.get(join)?; - assert_eq!(phase_counter.load(Ordering::SeqCst), 2); - - Ok(()) -} diff --git a/tests/patterns/grid.rs b/tests/patterns/grid.rs deleted file mode 100644 index 95c8a29..0000000 --- a/tests/patterns/grid.rs +++ /dev/null @@ -1,239 +0,0 @@ -//! Grid pattern DAG tests - -use crate::common::task_fn; -use dagx::{DagResult, DagRunner}; - -#[tokio::test] -async fn test_2d_grid_pattern() -> DagResult<()> { - let dag = DagRunner::new(); - - // Create a 4x4 grid where each cell depends on the cell above and to the left - let mut grid: Vec>>> = vec![vec![None; 4]; 4]; - - // Initialize first row and column - #[allow(clippy::needless_range_loop)] - for i in 0..4 { - let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| i as i32)); - grid[0][i] = Some(task.into()); - if i > 0 { - let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| i as i32 * 10)); - grid[i][0] = Some(task.into()); - } - } - - // Fill the grid with dependencies - for i in 1..4 { - for j in 1..4 { - let above = grid[i - 1][j].as_ref().unwrap(); - let left = grid[i][j - 1].as_ref().unwrap(); - - let task = dag - .add_task(task_fn::<(i32, i32), _, _>(move |(a, l): (&i32, &i32)| { - a + l - })) - .depends_on((above, left)); - grid[i][j] = Some(task); - } - } - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Check corner values - let bottom_right = dag.get(grid[3][3].as_ref().unwrap())?; - assert!(bottom_right > 0); - - Ok(()) -} - -#[tokio::test] -async fn test_3d_grid_pattern() -> DagResult<()> { - let dag = DagRunner::new(); - - // Create a 3x3x3 grid - let size = 3; - let mut grid: Vec>>>> = - vec![vec![vec![None; size]; size]; size]; - - // Initialize origin - let origin = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)); - grid[0][0][0] = Some(origin.into()); - - // Fill the 3D grid - for x in 0..size { - for y in 0..size { - for z in 0..size { - if x == 0 && y == 0 && z == 0 { - continue; - } - - let deps: Vec> = vec![ - if x > 0 { grid[x - 1][y][z] } else { None }, - if y > 0 { grid[x][y - 1][z] } else { None }, - if z > 0 { grid[x][y][z - 1] } else { None }, - ] - .into_iter() - .flatten() - .collect(); - - let task_handle: dagx::TaskHandle = match deps.len() { - 0 => { - let t = dag.add_task(task_fn::<(), _, _>(move |_: ()| x + y + z)); - t.into() - } - 1 => dag - .add_task(task_fn::(move |prev: &usize| prev + 1)) - .depends_on(deps[0]), - 2 => dag - .add_task(task_fn::<(usize, usize), _, _>( - move |(a, b): (&usize, &usize)| a + b, - )) - .depends_on((&deps[0], &deps[1])), - 3 => dag - .add_task(task_fn::<(usize, usize, usize), _, _>( - move |(a, b, c): (&usize, &usize, &usize)| a + b + c, - )) - .depends_on((&deps[0], &deps[1], &deps[2])), - _ => unreachable!(), - }; - - grid[x][y][z] = Some(task_handle); - } - } - } - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Check that all cells have values - for row in &grid { - for col in row { - for task in col.iter().flatten() { - let _ = dag.get(task)?; - } - } - } - - Ok(()) -} - -#[tokio::test] -async fn test_hexagonal_grid() -> DagResult<()> { - let dag = DagRunner::new(); - - // Hexagonal grid with axial coordinates - let mut hex_grid = std::collections::HashMap::new(); - - // Center hex - let center_builder = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)); - let center: dagx::TaskHandle = center_builder.into(); - hex_grid.insert((0, 0), center); - - // Ring 1 (6 hexes around center) - let directions = [(1, 0), (0, 1), (-1, 1), (-1, 0), (0, -1), (1, -1)]; - - for (i, &(dq, dr)) in directions.iter().enumerate() { - let neighbor: dagx::TaskHandle = dag - .add_task(task_fn::(move |c: &i32| c + (i as i32 + 1) * 10)) - .depends_on(center); - hex_grid.insert((dq, dr), neighbor); - } - - // Ring 2 (12 hexes) - for &(q, r) in &[ - (2, 0), - (1, 1), - (0, 2), - (-1, 2), - (-2, 2), - (-2, 1), - (-2, 0), - (-1, -1), - (0, -2), - (1, -2), - (2, -2), - (2, -1), - ] { - // Find neighbors that exist - let neighbors: Vec<_> = directions - .iter() - .filter_map(|&(dq, dr)| hex_grid.get(&(q - dq, r - dr)).cloned()) - .take(2) // Just use first 2 neighbors for simplicity - .collect(); - - let task: dagx::TaskHandle = if neighbors.len() == 2 { - dag.add_task(task_fn::<(i32, i32), _, _>(move |(a, b): (&i32, &i32)| { - (a + b) / 2 + q + r - })) - .depends_on((&neighbors[0], &neighbors[1])) - } else if neighbors.len() == 1 { - dag.add_task(task_fn::(move |n: &i32| n + q + r)) - .depends_on(neighbors[0]) - } else { - let t = dag.add_task(task_fn::<(), _, _>(move |_: ()| q + r)); - t.into() - }; - - hex_grid.insert((q, r), task); - } - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Verify center and some ring values - assert_eq!(dag.get(hex_grid.get(&(0, 0)).unwrap())?, 100); - - Ok(()) -} - -#[tokio::test] -async fn test_toroidal_grid() -> DagResult<()> { - let dag = DagRunner::new(); - - const SIZE: usize = 4; - let mut grid: Vec>>> = vec![vec![None; SIZE]; SIZE]; - - // Initialize all cells - #[allow(clippy::needless_range_loop)] - for i in 0..SIZE { - for j in 0..SIZE { - let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| (i * SIZE + j) as i32)); - grid[i][j] = Some(task.into()); - } - } - - // Create toroidal connections (wrapping edges) - let mut torus_tasks = Vec::new(); - - #[allow(clippy::needless_range_loop)] - for i in 0..SIZE { - for j in 0..SIZE { - let curr = grid[i][j].as_ref().unwrap(); - let up = grid[(i + SIZE - 1) % SIZE][j].as_ref().unwrap(); - let down = grid[(i + 1) % SIZE][j].as_ref().unwrap(); - let left = grid[i][(j + SIZE - 1) % SIZE].as_ref().unwrap(); - let right = grid[i][(j + 1) % SIZE].as_ref().unwrap(); - - let task = dag - .add_task(task_fn::<(_, _, _, _, _), _, _>( - move |(c, u, d, l, r): (&i32, &i32, &i32, &i32, &i32)| { - // Average of self and 4 neighbors - (c + u + d + l + r) / 5 - }, - )) - .depends_on((curr, up, down, left, right)); - - torus_tasks.push(task); - } - } - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Verify all torus tasks completed - for task in torus_tasks { - let _ = dag.get(task)?; - } - - Ok(()) -} diff --git a/tests/patterns/mod.rs b/tests/patterns/mod.rs deleted file mode 100644 index 1bfcf62..0000000 --- a/tests/patterns/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! DAG pattern tests -//! -//! Tests for various DAG topologies and patterns. - -mod butterfly; -mod complex; -mod grid; -mod tree; diff --git a/tests/patterns/tree.rs b/tests/patterns/tree.rs deleted file mode 100644 index ef713e9..0000000 --- a/tests/patterns/tree.rs +++ /dev/null @@ -1,282 +0,0 @@ -//! Tree pattern DAG tests - -use crate::common::task_fn; -use dagx::{DagResult, DagRunner, TaskHandle}; - -#[tokio::test] -async fn test_binary_tree_reduction() -> DagResult<()> { - let dag = DagRunner::new(); - - // Create 8 leaf nodes - let leaves: Vec<_> = (0..8) - .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| 1 << i))) - .collect(); - - // Build binary tree reduction - let mut current_level: Vec> = - leaves.into_iter().map(|t| t.into()).collect(); - - while current_level.len() > 1 { - let mut next_level = Vec::new(); - - for chunk in current_level.chunks(2) { - if chunk.len() == 2 { - let task = dag - .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b)) - .depends_on((&chunk[0], &chunk[1])); - next_level.push(task); - } else { - next_level.push(chunk[0]); - } - } - - current_level = next_level; - } - - let root = current_level[0]; - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - // Sum of powers of 2 from 0 to 7: 1+2+4+8+16+32+64+128 = 255 - assert_eq!(dag.get(root)?, 255); - - Ok(()) -} - -#[tokio::test] -async fn test_n_ary_tree() -> DagResult<()> { - let dag = DagRunner::new(); - - // Build a 3-ary tree (each node has 3 children) - fn build_tree(dag: &DagRunner, depth: usize, value: i32) -> dagx::TaskHandle { - if depth == 0 { - let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| value)); - return task.into(); - } - - let children: Vec<_> = (0..3) - .map(|i| build_tree(dag, depth - 1, value * 3 + i)) - .collect(); - - dag.add_task(task_fn::<(i32, i32, i32), _, _>( - move |(a, b, c): (&i32, &i32, &i32)| value + a + b + c, - )) - .depends_on((&children[0], &children[1], &children[2])) - } - - let root = build_tree(&dag, 3, 1); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - let result = dag.get(root)?; - assert!(result > 0); - - Ok(()) -} - -#[tokio::test] -async fn test_unbalanced_tree() -> DagResult<()> { - let dag = DagRunner::new(); - - // Create an unbalanced tree - // root - // / \ - // a b - // / \ \ - // c d e - // / \ - // f g - - let f: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 1)).into(); - let g = dag.add_task(task_fn::<(), _, _>(|_: ()| 2)); - - let c = dag - .add_task(task_fn::(|&x: &i32| x * 2)) - .depends_on(f); - - let d_builder = dag.add_task(task_fn::<(), _, _>(|_: ()| 3)); - let d: dagx::TaskHandle = d_builder.into(); - - let e = dag - .add_task(task_fn::(|&x: &i32| x * 3)) - .depends_on(g); - - let a = dag - .add_task(task_fn::<(i32, i32), _, _>(|(c, d): (&i32, &i32)| c + d)) - .depends_on((&c, &d)); - - let b = dag - .add_task(task_fn::(|e: &i32| e + 10)) - .depends_on(e); - - let root = dag - .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a * b)) - .depends_on((&a, &b)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(dag.get(f)?, 1); - assert_eq!(dag.get(c)?, 2); - assert_eq!(dag.get(a)?, 5); // 2 + 3 - assert_eq!(dag.get(b)?, 16); // 6 + 10 - assert_eq!(dag.get(root)?, 80); // 5 * 16 - - Ok(()) -} - -#[tokio::test] -async fn test_trie_like_structure() -> DagResult<()> { - let dag = DagRunner::new(); - - // Build a trie-like structure for paths - let root: TaskHandle<_> = dag - .add_task(task_fn::<(), _, _>(|_: ()| "root".to_string())) - .into(); - - // First level - let usr = dag - .add_task(task_fn::(|r: &String| format!("{}/usr", r))) - .depends_on(root); - - let etc = dag - .add_task(task_fn::(|r: &String| format!("{}/etc", r))) - .depends_on(root); - - let var = dag - .add_task(task_fn::(|r: &String| format!("{}/var", r))) - .depends_on(root); - - // Second level - let usr_bin = dag - .add_task(task_fn::(|u: &String| format!("{}/bin", u))) - .depends_on(usr); - - let usr_lib = dag - .add_task(task_fn::(|u: &String| format!("{}/lib", u))) - .depends_on(usr); - - let etc_config = dag - .add_task(task_fn::(|e: &String| { - format!("{}/config", e) - })) - .depends_on(etc); - - let var_log = dag - .add_task(task_fn::(|v: &String| format!("{}/log", v))) - .depends_on(var); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(dag.get(usr_bin)?, "root/usr/bin"); - assert_eq!(dag.get(usr_lib)?, "root/usr/lib"); - assert_eq!(dag.get(etc_config)?, "root/etc/config"); - assert_eq!(dag.get(var_log)?, "root/var/log"); - - Ok(()) -} - -#[tokio::test] -async fn test_balanced_k_ary_tree() -> DagResult<()> { - let dag = DagRunner::new(); - - // Create a perfectly balanced 4-ary tree of depth 3 - const K: usize = 4; - const DEPTH: usize = 3; - - fn create_balanced_tree( - dag: &DagRunner, - depth: usize, - k: usize, - node_id: usize, - ) -> dagx::TaskHandle { - if depth == 0 { - let task = dag.add_task(task_fn::<(), _, _>(move |_: ()| node_id)); - return task.into(); - } - - let children: Vec<_> = (0..k) - .map(|i| create_balanced_tree(dag, depth - 1, k, node_id * k + i)) - .collect(); - - match k { - 2 => dag - .add_task(task_fn::<(usize, usize), _, _>( - move |(a, b): (&usize, &usize)| node_id + a + b, - )) - .depends_on((&children[0], &children[1])), - 3 => dag - .add_task(task_fn::<(usize, usize, usize), _, _>( - move |(a, b, c): (&usize, &usize, &usize)| node_id + a + b + c, - )) - .depends_on((&children[0], &children[1], &children[2])), - 4 => dag - .add_task(task_fn::<(usize, usize, usize, usize), _, _>( - move |(a, b, c, d): (&usize, &usize, &usize, &usize)| node_id + a + b + c + d, - )) - .depends_on((&children[0], &children[1], &children[2], &children[3])), - _ => panic!("Unsupported k value"), - } - } - - let root = create_balanced_tree(&dag, DEPTH, K, 0); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - let result = dag.get(root)?; - assert!(result > 0); - - Ok(()) -} - -#[tokio::test] -async fn test_merkle_tree_pattern() -> DagResult<()> { - let dag = DagRunner::new(); - - // Simulate a Merkle tree for 8 data blocks - let data_blocks: Vec<_> = (0..8) - .map(|i| { - dag.add_task(task_fn::<(), _, _>(move |_: ()| { - // Simulate hash of data block - format!("hash_{}", i) - })) - }) - .collect(); - - // Convert to TaskHandles - let mut current_level: Vec> = - data_blocks.into_iter().map(|t| t.into()).collect(); - - while current_level.len() > 1 { - let mut next_level = Vec::new(); - - for pair in current_level.chunks(2) { - if pair.len() == 2 { - let hash_task = dag - .add_task(task_fn::<(String, String), _, _>( - |(left, right): (&String, &String)| format!("hash({}_{})", left, right), - )) - .depends_on((&pair[0], &pair[1])); - next_level.push(hash_task); - } else { - next_level.push(pair[0]); - } - } - - current_level = next_level; - } - - let root_hash = current_level[0]; - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - let result = dag.get(root_hash)?; - assert!(result.starts_with("hash")); - - Ok(()) -} diff --git a/tests/performance/mod.rs b/tests/performance/mod.rs deleted file mode 100644 index 8a73a60..0000000 --- a/tests/performance/mod.rs +++ /dev/null @@ -1,102 +0,0 @@ -//! Stress tests and benchmarks - -use crate::common::task_fn; -use dagx::DagRunner; - -use std::sync::Arc; -use std::time::{Duration, Instant}; - -#[tokio::test] -async fn test_deep_chain_100_levels() { - let dag = Arc::new(DagRunner::new()); - - // Create a chain 100 levels deep - let start = Instant::now(); - let first = dag.add_task(task_fn::<(), _, _>(|_: ()| 0)); - let mut handles = vec![first.into()]; - - for _ in 1..=100 { - let next = dag - .add_task(task_fn::(move |&x: &i32| x + 1)) - .depends_on(handles.last().unwrap()); - handles.push(next); - } - let build_time = start.elapsed(); - - println!("Built 100-level chain in {:?}", build_time); - - // Execute - let start = Instant::now(); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - let exec_time = start.elapsed(); - - println!("Executed 100-level chain in {:?}", exec_time); - assert!( - exec_time < Duration::from_secs(5), - "Execution took too long" - ); - - // Verify result - assert_eq!(dag.get(handles.last().unwrap()).unwrap(), 100); -} - -#[tokio::test] -async fn test_stress_mixed_patterns() { - // Stress test with mixed DAG patterns - let dag = Arc::new(DagRunner::new()); - let mut all_tasks = vec![]; - - // Add various patterns - for i in 0..10 { - // Linear chains - let first = dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 1000)); - let mut chain_handles = vec![first.into()]; - for j in 1..10 { - let next = dag - .add_task(task_fn::(move |&x: &i32| x + j)) - .depends_on(chain_handles.last().unwrap()); - chain_handles.push(next); - } - all_tasks.push(*chain_handles.last().unwrap()); - - // Diamonds - let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 100)); - let source_handle: dagx::TaskHandle = source.into(); - let left = dag - .add_task(task_fn::(|&x: &i32| x * 2)) - .depends_on(source_handle); - let right = dag - .add_task(task_fn::(|&x: &i32| x * 3)) - .depends_on(source_handle); - let merge = dag - .add_task(task_fn::<(i32, i32), _, _>(|(l, r): (&i32, &i32)| l + r)) - .depends_on((&left, &right)); - all_tasks.push(merge); - - // Fan-outs - let hub = dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); - let hub_handle: dagx::TaskHandle = hub.into(); - for j in 0..5 { - let fan = dag - .add_task(task_fn::(move |&x: &i32| x + j * 10)) - .depends_on(hub_handle); - all_tasks.push(fan); - } - } - - // Execute - let start = Instant::now(); - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - let exec_time = start.elapsed(); - - println!("Executed mixed pattern stress test in {:?}", exec_time); - - // Verify some results - for task in all_tasks.iter().take(10) { - assert!(dag.get(task).is_ok()); - } -} diff --git a/tests/runtimes/async_std.rs b/tests/runtimes/async_std.rs deleted file mode 100644 index 9ceb24e..0000000 --- a/tests/runtimes/async_std.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! Runtime compatibility tests for async-std - -use crate::common::task_fn; -use dagx::DagRunner; - -struct Value(i32); -#[dagx::task] -impl Value { - async fn run(&mut self) -> i32 { - self.0 - } -} - -struct Add; -#[dagx::task] -impl Add { - async fn run(&mut self, a: &i32, b: &i32) -> i32 { - a + b - } -} - -#[async_std::test] -async fn test_basic_dag_async_std() { - let dag = DagRunner::new(); - - let x = dag.add_task(Value(2)); - let y = dag.add_task(Value(3)); - let sum = dag.add_task(Add).depends_on((x, y)); - - dag.run(async_std::task::spawn).await.unwrap(); - - assert_eq!(dag.get(sum).unwrap(), 5); -} - -#[async_std::test] -async fn test_parallel_execution_async_std() { - let dag = DagRunner::new(); - - let tasks: Vec<_> = (0..10) - .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2))) - .collect(); - - dag.run(async_std::task::spawn).await.unwrap(); - - for (i, task) in tasks.into_iter().enumerate() { - assert_eq!(dag.get(task).unwrap(), i * 2); - } -} - -#[async_std::test] -async fn test_complex_dependencies_async_std() { - let dag = DagRunner::new(); - - let a = dag.add_task(Value(10)); - let b = dag.add_task(Value(20)); - let sum = dag.add_task(Add).depends_on((a, b)); - let double = dag - .add_task(task_fn::(|&x: &i32| x * 2)) - .depends_on(sum); - - dag.run(async_std::task::spawn).await.unwrap(); - - assert_eq!(dag.get(double).unwrap().clone(), 60); -} diff --git a/tests/runtimes/compatibility.rs b/tests/runtimes/compatibility.rs deleted file mode 100644 index 4a0b3f1..0000000 --- a/tests/runtimes/compatibility.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! Runtime integration tests - verify dagx works correctly across different runtimes - -use dagx::{DagResult, DagRunner}; - -struct Value(i32); -#[dagx::task] -impl Value { - async fn run(&mut self) -> i32 { - self.0 - } -} - -struct Add; -#[dagx::task] -impl Add { - async fn run(&mut self, a: &i32, b: &i32) -> i32 { - a + b - } -} - -#[tokio::test] -async fn runtime_integration_tokio() -> DagResult<()> { - let dag = DagRunner::new(); - - let a = dag.add_task(Value(10)); - let b = dag.add_task(Value(20)); - let sum = dag.add_task(Add).depends_on((a, b)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(dag.get(sum)?, 30); - Ok(()) -} diff --git a/tests/runtimes/mod.rs b/tests/runtimes/mod.rs index 25bcf9e..740a1da 100644 --- a/tests/runtimes/mod.rs +++ b/tests/runtimes/mod.rs @@ -1,7 +1,149 @@ //! Runtime tests module -pub mod compatibility; -pub mod quirky; +use std::{any::Any, sync::Arc}; -mod async_std; -mod smol; +use dagx::{DagResult, DagRunner}; +use dagx_test::task_fn; +use futures_util::{future::BoxFuture, task::SpawnExt, Future}; +use test_case::test_case; + +type BoxFunc = Box< + dyn Fn( + BoxFuture<'static, DagResult>>, + ) -> BoxFuture<'static, DagResult>>, +>; + +trait RuntimeTest { + fn run_dagx_test(self, f: impl Fn(BoxFunc) -> F) + where + F: Future; +} + +struct SmolRuntimeTest; + +impl RuntimeTest for SmolRuntimeTest { + fn run_dagx_test(self, f: impl Fn(BoxFunc) -> F) + where + F: Future, + { + smol::block_on(f(Box::new(|fut| Box::pin(smol::spawn(fut))))); + } +} + +struct FuturesExecutorRuntimeTest; + +impl RuntimeTest for FuturesExecutorRuntimeTest { + fn run_dagx_test(self, f: impl Fn(BoxFunc) -> F) + where + F: Future, + { + let spawner = futures_executor::ThreadPool::new().unwrap(); + + futures_executor::block_on(f(Box::new(move |fut| { + Box::pin(spawner.spawn_with_handle(fut).unwrap()) + }))); + } +} + +struct PollsterRuntimeTest; + +impl RuntimeTest for PollsterRuntimeTest { + fn run_dagx_test(self, f: impl Fn(BoxFunc) -> F) + where + F: Future, + { + pollster::block_on(f(Box::new(|fut| { + Box::pin(async { pollster::block_on(fut) }) + }))); + } +} + +static ASYNC_EXECUTOR: async_executor::Executor = async_executor::Executor::new(); + +struct AsyncExecutorRuntimeTest; + +impl RuntimeTest for AsyncExecutorRuntimeTest { + fn run_dagx_test(self, f: impl Fn(BoxFunc) -> F) + where + F: Future, + { + smol::future::block_on( + ASYNC_EXECUTOR.run(f(Box::new(|fut| Box::pin(ASYNC_EXECUTOR.spawn(fut))))), + ); + } +} + +struct Value(i32); +#[dagx::task] +impl Value { + async fn run(&mut self) -> i32 { + self.0 + } +} + +struct Add; +#[dagx::task] +impl Add { + async fn run(&mut self, a: &i32, b: &i32) -> i32 { + a + b + } +} + +#[test_case(SmolRuntimeTest)] +#[test_case(FuturesExecutorRuntimeTest)] +#[test_case(PollsterRuntimeTest)] +#[test_case(AsyncExecutorRuntimeTest)] +fn test_basic_dag(runner: impl RuntimeTest) { + runner.run_dagx_test(|spawner| async { + let dag = DagRunner::new(); + + let x = dag.add_task(Value(2)); + let y = dag.add_task(Value(3)); + let sum = dag.add_task(Add).depends_on((x, y)); + + dag.run(spawner).await.unwrap(); + + assert_eq!(dag.get(sum).unwrap(), 5); + }) +} + +#[test_case(SmolRuntimeTest)] +#[test_case(FuturesExecutorRuntimeTest)] +#[test_case(PollsterRuntimeTest)] +#[test_case(AsyncExecutorRuntimeTest)] +fn test_parallel_execution(runner: impl RuntimeTest) { + runner.run_dagx_test(|spawner| async { + let dag = DagRunner::new(); + + let tasks: Vec<_> = (0..10) + .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2))) + .collect(); + + dag.run(spawner).await.unwrap(); + + for (i, task) in tasks.into_iter().enumerate() { + assert_eq!(dag.get(task).unwrap(), i * 2); + } + }); +} + +#[test_case(SmolRuntimeTest)] +#[test_case(FuturesExecutorRuntimeTest)] +#[test_case(PollsterRuntimeTest)] +#[test_case(AsyncExecutorRuntimeTest)] +fn test_complex_dependencies(runner: impl RuntimeTest) { + runner.run_dagx_test(|spawner| async { + let dag = DagRunner::new(); + + let a = dag.add_task(Value(10)); + let b = dag.add_task(Value(20)); + let sum = dag.add_task(Add).depends_on((a, b)); + let double = dag + .add_task(task_fn::(|&x: &i32| x * 2)) + .depends_on(sum); + + dag.run(spawner).await.unwrap(); + + assert_eq!(dag.get(double).unwrap().clone(), 60); + }); +} diff --git a/tests/runtimes/quirky.rs b/tests/runtimes/quirky.rs deleted file mode 100644 index bad3970..0000000 --- a/tests/runtimes/quirky.rs +++ /dev/null @@ -1,493 +0,0 @@ -//! Tests for quirky/edge-case async runtimes -//! -//! This file tests dagx with unusual async runtimes to ensure -//! the spawner parameter works correctly across different executor types. -//! -//! Tested runtimes: -//! - async-executor: Minimal executor from smol ecosystem -//! - pollster: Blocking/synchronous executor -//! - futures-executor: Basic futures crate executor - -// Note: executor.run() synchronously executes futures to completion. -// We don't need the return value in these tests, so we allow unused results. -#![allow(clippy::let_underscore_future)] -#![allow(unused_must_use)] - -use crate::common::task_fn; -use dagx::{DagRunner, TaskHandle}; -use futures_executor::ThreadPool; -use futures_util::task::SpawnExt; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; - -// ======================= -// async-executor Tests -// ======================= - -#[test] -fn test_async_executor_basic() { - // async-executor is a minimal executor from the smol ecosystem - let executor = async_executor::Executor::new(); - - let future = async { - let dag = DagRunner::new(); - - let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)); - let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 20)); - let sum = dag - .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b)) - .depends_on((x, y)); - - dag.run(|fut| executor.spawn(fut)).await.unwrap(); - - assert_eq!(dag.get(sum).unwrap(), 30); - }; - - executor.run(future); -} - -#[test] -fn test_async_executor_complex_dag() { - let executor = async_executor::Executor::new(); - - let future = async { - let dag = DagRunner::new(); - - // Create a diamond pattern - let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 5)).into(); - - let left = dag - .add_task(task_fn::(|&x: &i32| x * 2)) - .depends_on(source); - - let right = dag - .add_task(task_fn::(|&x: &i32| x * 3)) - .depends_on(source); - - let sink = dag - .add_task(task_fn::<(i32, i32), _, _>(|(l, r): (&i32, &i32)| l + r)) - .depends_on((&left, &right)); - - dag.run(|fut| executor.spawn(fut)).await.unwrap(); - - assert_eq!(dag.get(sink).unwrap(), 25); // (5*2) + (5*3) = 25 - }; - - executor.run(future); -} - -#[test] -fn test_async_executor_parallel_tasks() { - let executor = async_executor::Executor::new(); - - let future = async { - let dag = DagRunner::new(); - let counter = Arc::new(AtomicUsize::new(0)); - - // Create 10 independent tasks - let tasks: Vec<_> = (0..10) - .map(|i| { - let c = counter.clone(); - dag.add_task(task_fn::<(), _, _>(move |_: ()| { - let c = c.clone(); - c.fetch_add(1, Ordering::SeqCst); - i - })) - }) - .collect(); - - dag.run(|fut| executor.spawn(fut)).await.unwrap(); - - // All tasks should have run - assert_eq!(counter.load(Ordering::SeqCst), 10); - - // Verify results - for (i, task) in tasks.into_iter().enumerate() { - assert_eq!(dag.get(task).unwrap(), i as i32); - } - }; - - executor.run(future); -} - -#[test] -fn test_async_executor_with_local_spawn() { - // Test with LocalExecutor (single-threaded) - let executor = async_executor::LocalExecutor::new(); - - let future = async { - let dag = DagRunner::new(); - - let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); - - dag.run(|fut| executor.spawn(fut)).await.unwrap(); - - assert_eq!(dag.get(x).unwrap(), 42); - }; - - executor.run(future); -} - -#[test] -fn test_async_executor_deep_chain() { - let executor = async_executor::Executor::new(); - - let future = async { - let dag = DagRunner::new(); - - // Build a chain of 50 tasks - let start = dag.add_task(task_fn::<(), _, _>(|_: ()| 0)); - let mut current = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(start); - - for _ in 1..50 { - current = dag - .add_task(task_fn::(|&x: &i32| x + 1)) - .depends_on(current); - } - - dag.run(|fut| executor.spawn(fut)).await.unwrap(); - - assert_eq!(dag.get(current).unwrap(), 50); - }; - - executor.run(future); -} - -// ======================= -// pollster Tests -// ======================= - -#[test] -fn test_pollster_basic() { - // pollster is a blocking executor - it blocks the thread - pollster::block_on(async { - let dag = DagRunner::new(); - - let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 100)); - let doubled = dag - .add_task(task_fn::(|&x: &i32| x * 2)) - .depends_on(x); - - dag.run(|fut| async { pollster::block_on(fut) }) - .await - .unwrap(); - - assert_eq!(dag.get(doubled).unwrap(), 200); - }); -} - -#[test] -fn test_pollster_complex() { - pollster::block_on(async { - let dag = DagRunner::new(); - - // Fan-out pattern - let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 7)).into(); - - let tasks: Vec<_> = (0..5) - .map(|i| { - dag.add_task(task_fn::(move |&x: &i32| x * i)) - .depends_on(source) - }) - .collect(); - - dag.run(|fut| async { pollster::block_on(fut) }) - .await - .unwrap(); - - for (i, task) in tasks.iter().enumerate() { - assert_eq!(dag.get(task).unwrap(), 7 * i as i32); - } - }); -} - -#[test] -fn test_pollster_error_handling() { - pollster::block_on(async { - let dag = DagRunner::new(); - - // Task that returns Result - let task = dag.add_task(task_fn::<(), _, _>(|_: ()| Ok::(42))); - - dag.run(|fut| async { pollster::block_on(fut) }) - .await - .unwrap(); - - assert_eq!(dag.get(task).unwrap(), Ok(42)); - }); -} - -// ======================= -// futures-executor Tests -// ======================= - -#[test] -fn test_futures_executor_basic() { - futures_executor::block_on(async { - let dag = DagRunner::new(); - - let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 15)); - let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 25)); - let product = dag - .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a * b)) - .depends_on((x, y)); - - let spawner = ThreadPool::new().unwrap(); - - dag.run(|fut| spawner.spawn_with_handle(fut).unwrap()) - .await - .unwrap(); - - assert_eq!(dag.get(product).unwrap(), 375); // 15 * 25 - }); -} - -#[test] -fn test_futures_executor_threadpool() { - let pool = futures_executor::ThreadPool::new().unwrap(); - let spawner = pool.clone(); - - pool.spawn_ok(async move { - let dag = DagRunner::new(); - - // Create multiple independent tasks - let tasks: Vec<_> = (0..20) - .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * i))) - .collect(); - - dag.run(|fut| spawner.spawn_with_handle(fut).unwrap()) - .await - .unwrap(); - - for (i, task) in tasks.into_iter().enumerate() { - assert_eq!(dag.get(task).unwrap(), (i * i) as i32); - } - }); - - // Give tasks time to complete - std::thread::sleep(std::time::Duration::from_millis(100)); -} - -// Note: LocalPool test removed because it cannot run within another executor context -// (cargo test framework may use tokio, causing EnterError) -// LocalPool is single-threaded anyway, so futures-executor ThreadPool is more representative - -#[test] -fn test_futures_executor_diamond_pattern() { - futures_executor::block_on(async { - let dag = DagRunner::new(); - - let a: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 8)).into(); - - let b = dag - .add_task(task_fn::(|&x: &i32| x + 2)) - .depends_on(a); - - let c = dag - .add_task(task_fn::(|&x: &i32| x - 2)) - .depends_on(a); - - let d = dag - .add_task(task_fn::<(i32, i32), _, _>(|(x, y): (&i32, &i32)| x * y)) - .depends_on((&b, &c)); - - let spawner = ThreadPool::new().unwrap(); - - dag.run(|fut| spawner.spawn_with_handle(fut).unwrap()) - .await - .unwrap(); - - assert_eq!(dag.get(d).unwrap(), 60); // (8+2) * (8-2) = 10 * 6 - }); -} - -// ======================= -// Cross-Runtime Tests -// ======================= - -#[test] -fn test_all_runtimes_compatibility() { - // This test verifies the same DAG works across all runtimes - - // Test 1: async-executor - { - let executor = async_executor::Executor::new(); - executor.run(async { - let dag = DagRunner::new(); - let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 123)); - dag.run(|fut| executor.spawn(fut)).await.unwrap(); - assert_eq!(dag.get(x).unwrap(), 123); - }); - } - - // Test 2: pollster - { - pollster::block_on(async { - let dag = DagRunner::new(); - let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 123)); - dag.run(|fut| async { pollster::block_on(fut) }) - .await - .unwrap(); - assert_eq!(dag.get(x).unwrap(), 123); - }); - } - - // Test 3: futures-executor - { - futures_executor::block_on(async { - let dag = DagRunner::new(); - let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 123)); - let spawner = ThreadPool::new().unwrap(); - dag.run(|fut| spawner.spawn_with_handle(fut).unwrap()) - .await - .unwrap(); - std::thread::sleep(std::time::Duration::from_millis(10)); - assert_eq!(dag.get(x).unwrap(), 123); - }); - } -} - -#[test] -fn test_runtime_edge_cases() { - // Test with LocalExecutor (no Send requirement) - let executor = async_executor::LocalExecutor::new(); - - executor.run(async { - let dag = DagRunner::new(); - - // This tests that our API works even with !Send futures - let x = dag.add_task(task_fn::<(), _, _>(|_: ()| { - // Local state - - 42 - })); - - dag.run(|fut| executor.spawn(fut)).await.unwrap(); - - assert_eq!(dag.get(x).unwrap(), 42); - }); -} - -// ======================= -// Stress Tests for Quirky Runtimes -// ======================= - -#[test] -fn test_async_executor_stress() { - let executor = async_executor::Executor::new(); - - executor.run(async { - let dag = DagRunner::new(); - - // Create 100 tasks - let tasks: Vec<_> = (0..100) - .map(|i| { - dag.add_task(task_fn::<(), _, _>(move |_: ()| { - // Small delay to test executor scheduling - i - })) - }) - .collect(); - - dag.run(|fut| executor.spawn(fut)).await.unwrap(); - - for (i, task) in tasks.into_iter().enumerate() { - assert_eq!(dag.get(task).unwrap(), i as i32); - } - }); -} - -#[test] -fn test_pollster_nested_blocks() { - // Test nested blocking - edge case - pollster::block_on(async { - let dag1 = DagRunner::new(); - let x = dag1.add_task(task_fn::<(), _, _>(|_: ()| 10)); - - dag1.run(|fut| async { pollster::block_on(fut) }) - .await - .unwrap(); - - assert_eq!(dag1.get(x).unwrap(), 10); - }); -} - -#[test] -fn test_futures_executor_multi_pool() { - // Test with multiple thread pools - let pool1 = futures_executor::ThreadPool::new().unwrap(); - let spawner = pool1.clone(); - - pool1.spawn_ok(async move { - let dag = DagRunner::new(); - - let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 50)); - - dag.run(|fut| spawner.spawn_with_handle(fut).unwrap()) - .await - .unwrap(); - - std::thread::sleep(std::time::Duration::from_millis(50)); - assert_eq!(dag.get(x).unwrap(), 50); - }); - - std::thread::sleep(std::time::Duration::from_millis(100)); -} - -// ======================= -// Edge Case: Sync Tasks with Quirky Runtimes -// ======================= - -#[test] -fn test_sync_tasks_on_async_executor() { - struct SyncAdd; - - #[dagx::task] - impl SyncAdd { - fn run(a: &i32, b: &i32) -> i32 { - // Synchronous task - a + b - } - } - - let executor = async_executor::Executor::new(); - - executor.run(async { - let dag = DagRunner::new(); - - let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 5)); - let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 7)); - let sum = dag.add_task(SyncAdd).depends_on((x, y)); - - dag.run(|fut| executor.spawn(fut)).await.unwrap(); - - assert_eq!(dag.get(sum).unwrap(), 12); - }); -} - -#[test] -fn test_sync_tasks_on_pollster() { - struct SyncDouble; - - #[dagx::task] - impl SyncDouble { - fn run(x: &i32) -> i32 { - x * 2 - } - } - - pollster::block_on(async { - let dag = DagRunner::new(); - - let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 21)); - let doubled = dag.add_task(SyncDouble).depends_on(x); - - dag.run(|fut| async { pollster::block_on(fut) }) - .await - .unwrap(); - - assert_eq!(dag.get(doubled).unwrap(), 42); - }); -} diff --git a/tests/runtimes/smol.rs b/tests/runtimes/smol.rs deleted file mode 100644 index 35624e2..0000000 --- a/tests/runtimes/smol.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! Runtime compatibility tests for smol - -use crate::common::task_fn; -use dagx::DagRunner; - -struct Value(i32); -#[dagx::task] -impl Value { - async fn run(&mut self) -> i32 { - self.0 - } -} - -struct Add; -#[dagx::task] -impl Add { - async fn run(&mut self, a: &i32, b: &i32) -> i32 { - a + b - } -} - -#[test] -fn test_basic_dag_smol() { - smol::block_on(async { - let dag = DagRunner::new(); - - let x = dag.add_task(Value(2)); - let y = dag.add_task(Value(3)); - let sum = dag.add_task(Add).depends_on((x, y)); - - dag.run(smol::spawn).await.unwrap(); - - assert_eq!(dag.get(sum).unwrap(), 5); - }); -} - -#[test] -fn test_parallel_execution_smol() { - smol::block_on(async { - let dag = DagRunner::new(); - - let tasks: Vec<_> = (0..10) - .map(|i| dag.add_task(task_fn::<(), _, _>(move |_: ()| i * 2))) - .collect(); - - dag.run(smol::spawn).await.unwrap(); - - for (i, task) in tasks.into_iter().enumerate() { - assert_eq!(dag.get(task).unwrap(), i * 2); - } - }); -} - -#[test] -fn test_complex_dependencies_smol() { - smol::block_on(async { - let dag = DagRunner::new(); - - let a = dag.add_task(Value(10)); - let b = dag.add_task(Value(20)); - let sum = dag.add_task(Add).depends_on((a, b)); - let double = dag - .add_task(task_fn::(|&x: &i32| x * 2)) - .depends_on(sum); - - dag.run(smol::spawn).await.unwrap(); - - assert_eq!(dag.get(double).unwrap().clone(), 60); - }); -} diff --git a/tests/task_patterns/combined.rs b/tests/task_patterns/combined.rs deleted file mode 100644 index fa15c53..0000000 --- a/tests/task_patterns/combined.rs +++ /dev/null @@ -1,119 +0,0 @@ -// Combined task pattern tests -use crate::common::task_fn; -// Comprehensive test showing all three patterns working together - -use dagx::*; - -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; - -// Pattern 1: Stateless - Pure computation, no state -struct StatelessAdd; -#[task] -impl StatelessAdd { - async fn run(a: &i32, b: &i32) -> i32 { - a + b - } -} - -// Pattern 2: Read-only state - Configuration data -struct ReadOnlyMultiplier(i32); -#[task] -impl ReadOnlyMultiplier { - async fn run(&self, input: &i32) -> i32 { - input * self.0 // Read-only access - } -} - -// Pattern 3: Mutable state - Accumulates state -struct MutableAccumulator(i32); -#[task] -impl MutableAccumulator { - async fn run(&mut self, input: &i32) -> i32 { - self.0 += input; // Modifies state - self.0 - } -} - -#[tokio::test] -async fn test_all_three_task_patterns() { - // Comprehensive test showing all three patterns working together - let dag = DagRunner::new(); - - // Create initial values - let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 5)); - let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 3)); - - // Pattern 1: Stateless addition - let sum = dag.add_task(StatelessAdd).depends_on((x, y)); // 5 + 3 = 8 - - // Pattern 2: Read-only multiplication - let doubled = dag.add_task(ReadOnlyMultiplier(2)).depends_on(sum); // 8 * 2 = 16 - - // Pattern 3: Mutable accumulation - let accumulated = dag.add_task(MutableAccumulator(10)).depends_on(doubled); // 10 + 16 = 26 - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(sum).unwrap(), 8); - assert_eq!(dag.get(doubled).unwrap(), 16); - assert_eq!(dag.get(accumulated).unwrap(), 26); -} - -#[tokio::test] -async fn test_parallel_execution() { - let dag = DagRunner::new(); - let counter = Arc::new(AtomicUsize::new(0)); - - // Create 3 independent tasks that should run in parallel - let c1 = counter.clone(); - let c2 = counter.clone(); - let c3 = counter.clone(); - - let t1 = dag.add_task(task_fn::<(), _, _>(move |_: ()| { - let c = c1.clone(); - c.fetch_add(1, Ordering::SeqCst); - 1 - })); - - let t2 = dag.add_task(task_fn::<(), _, _>(move |_: ()| { - let c = c2.clone(); - c.fetch_add(1, Ordering::SeqCst); - 2 - })); - - let t3 = dag.add_task(task_fn::<(), _, _>(move |_: ()| { - let c = c3.clone(); - c.fetch_add(1, Ordering::SeqCst); - 3 - })); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // All three tasks should have run - assert_eq!(counter.load(Ordering::SeqCst), 3); - assert_eq!(dag.get(t1).unwrap(), 1); - assert_eq!(dag.get(t2).unwrap(), 2); - assert_eq!(dag.get(t3).unwrap(), 3); -} - -#[tokio::test] -async fn test_task_fn_with_captured_state() { - let dag = DagRunner::new(); - - let multiplier = 7; - let base = dag.add_task(task_fn::<(), _, _>(|_: ()| 5)); - let scaled = dag - .add_task(task_fn::(move |&x: &i32| x * multiplier)) - .depends_on(base); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(scaled).unwrap(), 35); -} diff --git a/tests/task_patterns/mod.rs b/tests/task_patterns/mod.rs deleted file mode 100644 index d3c8b58..0000000 --- a/tests/task_patterns/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -// Test module for task pattern tests - -mod combined; -mod readonly; -mod stateless; diff --git a/tests/task_patterns/readonly.rs b/tests/task_patterns/readonly.rs deleted file mode 100644 index f2b1c70..0000000 --- a/tests/task_patterns/readonly.rs +++ /dev/null @@ -1,91 +0,0 @@ -// Read-only state task pattern tests -use crate::common::task_fn; -// These tests verify the read-only state pattern using &self - -use dagx::*; - -// Read-only state tasks -struct ReadOnlyValue(i32); - -#[task] -impl ReadOnlyValue { - async fn run(&self) -> i32 { - self.0 // Read-only access - } -} - -struct ReadOnlyMultiplier(i32); - -#[task] -impl ReadOnlyMultiplier { - async fn run(&self, input: &i32) -> i32 { - input * self.0 // Read-only access to state - } -} - -struct ReadOnlyConfig { - multiplier: i32, - offset: i32, -} - -#[task] -impl ReadOnlyConfig { - async fn run(&self, input: &i32) -> i32 { - (input * self.multiplier) + self.offset - } -} - -#[tokio::test] -async fn test_readonly_state_basic() { - // Test basic read-only state task using &self - let dag = DagRunner::new(); - - let value = dag.add_task(ReadOnlyValue(42)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - assert_eq!(dag.get(value).unwrap(), 42); -} - -#[tokio::test] -async fn test_readonly_state_with_input() { - // Test read-only state task with input using &self - let dag = DagRunner::new(); - - let input = dag.add_task(task_fn::<(), _, _>(|_: ()| 5)); - let result = dag.add_task(ReadOnlyMultiplier(7)).depends_on(input); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - assert_eq!(dag.get(result).unwrap(), 35); -} - -#[tokio::test] -async fn test_readonly_state_multiple_uses() { - // Test that read-only state can be used multiple times - let dag = DagRunner::new(); - - let input1 = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)); - let input2 = dag.add_task(task_fn::<(), _, _>(|_: ()| 20)); - - let config = ReadOnlyConfig { - multiplier: 3, - offset: 5, - }; - - let result1 = dag.add_task(config).depends_on(input1); - // Note: Can't reuse config because it was moved, so create another - let config2 = ReadOnlyConfig { - multiplier: 3, - offset: 5, - }; - let result2 = dag.add_task(config2).depends_on(input2); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - assert_eq!(dag.get(result1).unwrap(), 35); // (10 * 3) + 5 - assert_eq!(dag.get(result2).unwrap(), 65); // (20 * 3) + 5 -} diff --git a/tests/task_patterns/stateless.rs b/tests/task_patterns/stateless.rs deleted file mode 100644 index b48baa5..0000000 --- a/tests/task_patterns/stateless.rs +++ /dev/null @@ -1,236 +0,0 @@ -// Stateless task pattern tests -use crate::common::task_fn; - -use dagx::*; - -// Stateless task structs -struct StatelessAdd; - -#[task] -impl StatelessAdd { - async fn run(a: &i32, b: &i32) -> i32 { - a + b - } -} - -struct StatelessDouble; - -#[task] -impl StatelessDouble { - async fn run(x: &i32) -> i32 { - x * 2 - } -} - -struct StatelessSource; - -#[task] -impl StatelessSource { - async fn run() -> i32 { - 42 - } -} - -struct StatelessFormat; - -#[task] -impl StatelessFormat { - async fn run(n: &i32, s: &String, b: &bool) -> String { - format!("{}: {} ({})", s, n, b) - } -} - -struct StatelessMultiply; - -#[task] -impl StatelessMultiply { - async fn run(a: &i32, b: &i32) -> i32 { - a * b - } -} - -struct StatelessInc; - -#[task] -impl StatelessInc { - async fn run(x: &i32) -> i32 { - x + 1 - } -} - -struct StatelessSquare; - -#[task] -impl StatelessSquare { - async fn run(x: &i32) -> i32 { - x * x - } -} - -struct StatelessConcat; - -#[task] -impl StatelessConcat { - async fn run(a: &String, b: &String) -> String { - format!("{}{}", a, b) - } -} - -#[tokio::test] -async fn test_stateless_basic() { - // Test basic stateless task with two inputs - let dag = DagRunner::new(); - - let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 10)); - let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 20)); - let sum = dag.add_task(StatelessAdd).depends_on((x, y)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - assert_eq!(dag.get(sum).unwrap(), 30); -} - -#[tokio::test] -async fn test_stateless_single_input() { - // Test stateless task with single input - let dag = DagRunner::new(); - - let input = dag.add_task(task_fn::<(), _, _>(|_: ()| 21)); - let doubled = dag.add_task(StatelessDouble).depends_on(input); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - assert_eq!(dag.get(doubled).unwrap(), 42); -} - -#[tokio::test] -async fn test_stateless_no_input() { - // Test stateless source task with no inputs - let dag = DagRunner::new(); - - let source = dag.add_task(StatelessSource); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - assert_eq!(dag.get(source).unwrap(), 42); -} - -#[tokio::test] -async fn test_stateless_multiple_inputs() { - // Test stateless task with multiple inputs of different types - let dag = DagRunner::new(); - - let num = dag.add_task(task_fn::<(), _, _>(|_: ()| 42)); - let text = dag.add_task(task_fn::<(), _, _>(|_: ()| "test".to_string())); - let flag = dag.add_task(task_fn::<(), _, _>(|_: ()| true)); - - let result = dag.add_task(StatelessFormat).depends_on((num, text, flag)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - assert_eq!(dag.get(result).unwrap(), "test: 42 (true)"); -} - -#[tokio::test] -async fn test_stateless_mixed_with_stateful() { - // Test mixing stateless and stateful tasks - // Define Counter inline for this test - struct Counter { - count: i32, - } - - impl Counter { - fn new(initial: i32) -> Self { - Self { count: initial } - } - } - - #[task] - impl Counter { - async fn run(&mut self, input: &i32) -> i32 { - self.count += input; - self.count - } - } - - let dag = DagRunner::new(); - - let x = dag.add_task(task_fn::<(), _, _>(|_: ()| 5)); - let y = dag.add_task(task_fn::<(), _, _>(|_: ()| 3)); - - // Stateless multiplication - let product = dag.add_task(StatelessMultiply).depends_on((x, y)); - - // Stateful counter - let counted = dag.add_task(Counter::new(10)).depends_on(product); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(product).unwrap(), 15); - assert_eq!(dag.get(counted).unwrap(), 25); // 10 + 15 -} - -#[tokio::test] -async fn test_stateless_chain() { - // Test chaining stateless tasks - let dag = DagRunner::new(); - - let start = dag.add_task(task_fn::<(), _, _>(|_: ()| 0)); - let step1 = dag.add_task(StatelessInc).depends_on(start); - let step2 = dag.add_task(StatelessInc).depends_on(step1); - let step3 = dag.add_task(StatelessInc).depends_on(step2); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(step1).unwrap(), 1); - assert_eq!(dag.get(step2).unwrap(), 2); - assert_eq!(dag.get(step3).unwrap(), 3); -} - -#[tokio::test] -async fn test_stateless_parallel() { - // Test parallel execution of stateless tasks - let dag = DagRunner::new(); - - let tasks: Vec<_> = (1..=5) - .map(|i| { - let source = dag.add_task(task_fn::<(), _, _>(move |_: ()| i)); - dag.add_task(StatelessSquare).depends_on(source) - }) - .collect(); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(tasks[0]).unwrap(), 1); - assert_eq!(dag.get(tasks[1]).unwrap(), 4); - assert_eq!(dag.get(tasks[2]).unwrap(), 9); - assert_eq!(dag.get(tasks[3]).unwrap(), 16); - assert_eq!(dag.get(tasks[4]).unwrap(), 25); -} - -#[tokio::test] -async fn test_stateless_with_string_types() { - // Test stateless tasks with complex types - let dag = DagRunner::new(); - - let hello = dag.add_task(task_fn::<(), _, _>(|_: ()| "Hello, ".to_string())); - let world = dag.add_task(task_fn::<(), _, _>(|_: ()| "World!".to_string())); - - let greeting = dag.add_task(StatelessConcat).depends_on((hello, world)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(greeting).unwrap(), "Hello, World!"); -} diff --git a/tests/timing/concurrent_access.rs b/tests/timing/concurrent_access.rs index 0477d85..ad5b147 100644 --- a/tests/timing/concurrent_access.rs +++ b/tests/timing/concurrent_access.rs @@ -1,7 +1,7 @@ //! Tests for concurrent access patterns and race conditions -use crate::common::task_fn; use dagx::{task, DagResult, DagRunner, TaskHandle}; +use dagx_test::task_fn; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; diff --git a/tests/timing/interleaving.rs b/tests/timing/interleaving.rs index c47e691..cb430d9 100644 --- a/tests/timing/interleaving.rs +++ b/tests/timing/interleaving.rs @@ -1,7 +1,7 @@ //! Tests for forced execution interleaving patterns -use crate::common::task_fn; -use dagx::{task, DagResult, DagRunner, TaskHandle}; +use dagx::{task, DagResult, DagRunner}; +use dagx_test::task_fn; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; @@ -272,113 +272,3 @@ async fn test_layered_interleaving() -> DagResult<()> { Ok(()) } - -#[tokio::test] -async fn test_cross_branch_synchronization() -> DagResult<()> { - // Test synchronization between parallel branches - let dag = DagRunner::new(); - let sync_point = Arc::new(AtomicUsize::new(0)); - let events = Arc::new(parking_lot::Mutex::new(Vec::new())); - - // Branch A - let a1: TaskHandle<_> = dag - .add_task(task_fn::<(), _, _>({ - let sync = sync_point.clone(); - let events = events.clone(); - move |_: ()| { - events.lock().push("A1"); - sync.fetch_add(1, Ordering::SeqCst); - 1 - } - })) - .into(); - - struct A2Task { - sync: Arc, - events: Arc>>, - } - - #[task] - impl A2Task { - async fn run(&self, x: &i32) -> i32 { - // Wait for B1 - while self.sync.load(Ordering::SeqCst) < 2 { - tokio::task::yield_now().await; - } - self.events.lock().push("A2"); - self.sync.fetch_add(1, Ordering::SeqCst); - x + 1 - } - } - - let a2 = dag - .add_task(A2Task { - sync: sync_point.clone(), - events: events.clone(), - }) - .depends_on(a1); - - struct B1Task { - sync: Arc, - events: Arc>>, - } - - #[task] - impl B1Task { - async fn run(&self) -> i32 { - // Wait for A1 - while self.sync.load(Ordering::SeqCst) < 1 { - tokio::task::yield_now().await; - } - self.events.lock().push("B1"); - self.sync.fetch_add(1, Ordering::SeqCst); - 10 - } - } - - // Branch B - let b1 = dag.add_task(B1Task { - events: events.clone(), - sync: sync_point.clone(), - }); - - struct B2Task { - sync: Arc, - events: Arc>>, - } - - #[task] - impl B2Task { - async fn run(&self, x: &i32) -> i32 { - // Wait for A2 - while self.sync.load(Ordering::SeqCst) < 3 { - tokio::task::yield_now().await; - } - self.events.lock().push("B2"); - x + 1 - } - } - - let b2 = dag - .add_task(B2Task { - events: events.clone(), - sync: sync_point.clone(), - }) - .depends_on(b1); - - // Merge - let merge = dag - .add_task(task_fn::<(i32, i32), _, _>(|(a, b): (&i32, &i32)| a + b)) - .depends_on((&a2, &b2)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await?; - - assert_eq!(dag.get(merge)?, 13); // 2 + 11 - - // Check forced interleaving - let event_list = events.lock().clone(); - assert_eq!(event_list, vec!["A1", "B1", "A2", "B2"]); - - Ok(()) -} diff --git a/tests/tracing/mod.rs b/tests/tracing/mod.rs index f34d427..bde5b9e 100644 --- a/tests/tracing/mod.rs +++ b/tests/tracing/mod.rs @@ -5,6 +5,3 @@ #[cfg(feature = "tracing")] mod with_tracing; - -#[cfg(not(feature = "tracing"))] -mod without_tracing; diff --git a/tests/tracing/with_tracing.rs b/tests/tracing/with_tracing.rs index fc0f77b..c087143 100644 --- a/tests/tracing/with_tracing.rs +++ b/tests/tracing/with_tracing.rs @@ -52,33 +52,6 @@ async fn test_tracing_with_subscriber() { assert_eq!(dag.get(sum).unwrap(), 5); } -#[tokio::test(flavor = "multi_thread")] -async fn test_tracing_with_complex_dag() { - // Initialize tracing subscriber - let _ = fmt() - .with_env_filter(EnvFilter::from_default_env()) - .with_test_writer() - .try_init(); - - let dag = DagRunner::new(); - - // Create a diamond pattern - let source = dag.add_task(Value(10)).into(); - let left = dag.add_task(Add).depends_on((&source, &source)); - let right = dag.add_task(Multiply).depends_on((&source, &source)); - let sink = dag.add_task(Add).depends_on((&left, &right)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // source = 10 - // left = 10 + 10 = 20 - // right = 10 * 10 = 100 - // sink = 20 + 100 = 120 - assert_eq!(dag.get(sink).unwrap(), 120); -} - #[tokio::test(flavor = "multi_thread")] async fn test_tracing_inline_execution() { // Initialize tracing subscriber @@ -103,38 +76,3 @@ async fn test_tracing_inline_execution() { // c = 2 * 2 = 4 assert_eq!(dag.get(c).unwrap(), 4); } - -#[tokio::test(flavor = "multi_thread")] -async fn test_tracing_multiple_layers() { - // Initialize tracing subscriber - let _ = fmt() - .with_env_filter( - EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("debug")), - ) - .with_test_writer() - .try_init(); - - let dag = DagRunner::new(); - - // Layer 0 - let v1 = dag.add_task(Value(1)); - let v2 = dag.add_task(Value(2)).into(); - let v3 = dag.add_task(Value(3)); - - // Layer 1 - let sum12 = dag.add_task(Add).depends_on((v1, &v2)); - let mul23 = dag.add_task(Multiply).depends_on((&v2, v3)); - - // Layer 2 - let final_sum = dag.add_task(Add).depends_on((&sum12, &mul23)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // v1 = 1, v2 = 2, v3 = 3 - // sum12 = 1 + 2 = 3 - // mul23 = 2 * 3 = 6 - // final_sum = 3 + 6 = 9 - assert_eq!(dag.get(final_sum).unwrap(), 9); -} diff --git a/tests/tracing/without_tracing.rs b/tests/tracing/without_tracing.rs deleted file mode 100644 index 7b2ff3c..0000000 --- a/tests/tracing/without_tracing.rs +++ /dev/null @@ -1,57 +0,0 @@ -//! Tests that the library works correctly without tracing feature - -use dagx::{task, DagRunner}; - -struct Value(i32); - -#[task] -impl Value { - async fn run(&self) -> i32 { - self.0 - } -} - -struct Add; - -#[task] -impl Add { - async fn run(a: &i32, b: &i32) -> i32 { - a + b - } -} - -#[tokio::test(flavor = "multi_thread")] -async fn test_basic_dag_without_tracing() { - let dag = DagRunner::new(); - - let a = dag.add_task(Value(2)); - let b = dag.add_task(Value(3)); - let sum = dag.add_task(Add).depends_on((a, b)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(sum).unwrap(), 5); -} - -#[tokio::test(flavor = "multi_thread")] -async fn test_complex_dag_without_tracing() { - let dag = DagRunner::new(); - - // Create multiple layers - let v1 = dag.add_task(Value(1)); - let v2 = dag.add_task(Value(2)); - let v3 = dag.add_task(Value(3)); - let v4 = dag.add_task(Value(4)); - - let sum12 = dag.add_task(Add).depends_on((v1, v2)); - let sum34 = dag.add_task(Add).depends_on((v3, v4)); - let final_sum = dag.add_task(Add).depends_on((&sum12, &sum34)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(final_sum).unwrap(), 10); -} diff --git a/tests/type_safety/mod.rs b/tests/type_safety/mod.rs deleted file mode 100644 index 49504cd..0000000 --- a/tests/type_safety/mod.rs +++ /dev/null @@ -1,72 +0,0 @@ -// Type safety tests for TaskHandle -use crate::common::task_fn; - -// These tests verify dagx's compile-time type safety guarantees. -// Tests cover both runtime behavior (wrong types return None) and -// compile-time checking (type mismatches fail to compile). - -use dagx::*; - -#[tokio::test] -async fn test_get_with_wrong_type_returns_none() { - // Verify that accessing with wrong type returns error, not panic - let dag = DagRunner::new(); - - // Add task that returns i32 - let int_task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42i32)); - - // Run DAG - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // Verify correct type works - assert_eq!(dag.get(int_task), Ok(42i32)); - - // The type safety is enforced at compile time - // We cannot create fake handles with wrong types without accessing private fields - // This is actually better - it means type safety is compile-time enforced! -} - -#[tokio::test] -async fn test_handle_preserves_type() { - // Verify TaskHandle preserves type information correctly - let dag = DagRunner::new(); - - // Create typed handles - let string_task = dag.add_task(task_fn::<(), _, _>(|_: ()| "hello".to_string())); - let int_task = dag.add_task(task_fn::<(), _, _>(|_: ()| 42i32)); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - // Get with correct types - assert_eq!(dag.get(string_task).unwrap(), "hello".to_string()); - assert_eq!(dag.get(int_task).unwrap(), 42); - - // Type safety is enforced at compile time - we cannot create wrong-typed handles -} - -#[tokio::test] -async fn test_type_safety_handle_reuse() { - // Verify that handles maintain type safety across reuse - let dag = DagRunner::new(); - - let source: TaskHandle<_> = dag.add_task(task_fn::<(), _, _>(|_: ()| 100i32)).into(); - - // Use the same source handle multiple times - let double = dag - .add_task(task_fn::(|&x: &i32| x * 2)) - .depends_on(source); - let triple = dag - .add_task(task_fn::(|&x: &i32| x * 3)) - .depends_on(source); - - dag.run(|fut| async move { tokio::spawn(fut).await.unwrap() }) - .await - .unwrap(); - - assert_eq!(dag.get(double).unwrap(), 200); - assert_eq!(dag.get(triple).unwrap(), 300); -}