9 Commits
18 changed files with 521 additions and 54 deletions

No files matched your search

+6 -5
View File
@@ -50,7 +50,7 @@ jobs:
rust: [stable, nightly]
include:
- os: ubuntu-latest
rust: "1.78.0" # MSRV
rust: "1.81" # MSRV
exclude:
- rust: nightly
@@ -111,10 +111,11 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cargo Audit
uses: actions-rs/audit-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install cargo-audit
run: cargo install cargo-audit
- name: Run cargo audit
run: cargo audit
doc:
name: Documentation
+70 -1
View File
@@ -5,6 +5,72 @@ All notable changes to dagx will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.2.2] - 2025-10-07
[View changes](https://github.com/swaits/dagx/compare/v0.2.1...v0.2.2)
### Fixed
- Fixed README.md to reference correct crate version (`0.2` instead of `0.1`)
- Enhanced release check script to validate version consistency in documentation
- Automatically extracts major.minor version and verifies documentation matches
## [0.2.1] - 2025-10-07
[View changes](https://github.com/swaits/dagx/compare/v0.2.0...v0.2.1)
### Fixed
- Fixed test execution on macOS by forcing `tokio::test` to use multi-threaded runtime
- Ensures tests pass consistently across all platforms
## [0.2.0] - 2025-10-08
[View changes](https://github.com/swaits/dagx/compare/v0.1.0...v0.2.0)
### Changed
- **BREAKING**: Updated MSRV (Minimum Supported Rust Version) from 1.78.0 to 1.81.0
- Required by updated dependencies: `criterion@0.7.0`, `half@2.6.0`, `rayon@1.11.0`
- Updated in both `dagx` and `dagx-macros` crates
- Updated all documentation (README.md, CONTRIBUTING.md)
### Added
- Comprehensive test coverage improvements (82.61% → 92.94%)
- Added 9 new unit tests for error paths in `ExtractInput` implementations
- Added 3 new type conversion tests in `src/types/tests.rs`
- Added 1 new trait implementation test in `src/task/tests.rs`
- Added 1 new dependency tuple test in `src/deps/tests.rs`
- Added 6 new execution path tests in `tests/execution/basic.rs`
- Added error handling tests for HashMap, Result, Option, Vec, and Arc types
- Added tests for tuple dependency count validation
- Improved concurrent run protection test with better synchronization
- Added `tarpaulin_include` to lint configuration for coverage tooling compatibility
### Fixed
- Fixed GitHub Actions `cargo-audit` workflow
- Replaced deprecated `actions-rs/audit-check@v1` with direct `cargo audit` command
- Resolved "Resource not accessible by integration" error
- Fixed clippy warnings in test code
- Removed unnecessary borrows in `.depends_on()` calls
- Added `#[allow(clippy::clone_on_copy)]` for explicit clone test
- Marked timing-sensitive and resource-intensive tests with `#[cfg_attr(tarpaulin, ignore)]`
- `test_arc_parallel_execution` (timing unreliable under instrumentation)
- `test_100000_nodes_stress` (resource intensive)
- `test_10000_level_chain_stress` (resource intensive)
### Internal
- Improved test organization following existing patterns in `tests/` directory
- Enhanced error path coverage for all `ExtractInput` trait implementations
- Better separation between unit tests and integration tests
- Refactored `scripts/release_check.sh` to use configurable VERSION variable
- Replaced hardcoded version references throughout script
- Updated instructions to use `jj` commands instead of `git`
- Simplified version bumps for future releases
## [0.1.0] - 2025-10-05
### Added
@@ -97,4 +163,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- macOS (compatible)
- Windows (compatible)
[0.1.0]: https://github.com/swaits/dagx/releases/tag/v0.1.0
[0.2.2]: https://github.com/swaits/dagx/tree/v0.2.2
[0.2.1]: https://github.com/swaits/dagx/tree/v0.2.1
[0.2.0]: https://github.com/swaits/dagx/tree/v0.2.0
[0.1.0]: https://github.com/swaits/dagx/tree/v0.1.0
+1 -1
View File
@@ -21,7 +21,7 @@ cargo clippy -- -D warnings
## Development Setup
**Prerequisites:**
- Rust 1.78+ (edition 2021)
- Rust 1.81+ (edition 2021)
- Git
**Optional tools:**
Generated
+2 -2
View File
@@ -434,7 +434,7 @@ dependencies = [
[[package]]
name = "dagx"
version = "0.1.0"
version = "0.2.2"
dependencies = [
"async-executor",
"async-std",
@@ -452,7 +452,7 @@ dependencies = [
[[package]]
name = "dagx-macros"
version = "0.1.0"
version = "0.2.2"
dependencies = [
"proc-macro2",
"quote",
+6 -3
View File
@@ -3,9 +3,9 @@ members = ["dagx-macros"]
[package]
name = "dagx"
version = "0.1.0"
version = "0.2.2"
edition = "2021"
rust-version = "1.78.0"
rust-version = "1.81"
authors = ["Stephen Waits <steve@waits.net>"]
license = "MIT"
description = "A minimal, type-safe, runtime-agnostic async DAG executor with compile-time dependency validation"
@@ -17,10 +17,13 @@ keywords = ["dag", "async", "task", "parallel", "executor"]
categories = ["asynchronous", "concurrency", "data-structures"]
exclude = [".claude/", ".jj/", "dagx-spec.md"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin)', 'cfg(tarpaulin_include)'] }
[dependencies]
futures = "0.3"
parking_lot = "0.12"
dagx-macros = { version = "0.1.0", path = "./dagx-macros" }
dagx-macros = { version = "0.2.2", path = "./dagx-macros" }
[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "sync"] }
+2 -2
View File
@@ -4,7 +4,7 @@
[![Documentation](https://docs.rs/dagx/badge.svg)](https://docs.rs/dagx)
[![Build Status](https://github.com/swaits/dagx/workflows/CI/badge.svg)](https://github.com/swaits/dagx/actions)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Rust Version](https://img.shields.io/badge/rust-1.78.0+-blue.svg)](https://www.rust-lang.org)
[![Rust Version](https://img.shields.io/badge/rust-1.81+-blue.svg)](https://www.rust-lang.org)
A minimal, type-safe, runtime-agnostic async DAG (Directed Acyclic Graph) executor with compile-time dependency validation and true parallel execution.
@@ -141,7 +141,7 @@ Add to your `Cargo.toml`:
```toml
[dependencies]
dagx = "0.1"
dagx = "0.2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```
+2 -2
View File
@@ -1,8 +1,8 @@
[package]
name = "dagx-macros"
version = "0.1.0"
version = "0.2.2"
edition = "2021"
rust-version = "1.78.0"
rust-version = "1.81"
authors = ["Stephen Waits <steve@waits.net>"]
license = "MIT"
description = "Procedural macros for dagx"
+25 -19
View File
@@ -1,12 +1,17 @@
#!/bin/bash
# Release Readiness Checker for dagx v0.1.0
# Release Readiness Checker for dagx
# This script performs EXHAUSTIVE checks before publishing to crates.io
set -e
echo "========================================"
echo "dagx v0.1.0 EXHAUSTIVE Release Check"
echo "========================================"
# =============================================================================
# CONFIGURATION
# =============================================================================
VERSION="0.2.2"
echo "=================="
echo "dagx Release Check"
echo "=================="
echo ""
# Color codes
@@ -51,7 +56,7 @@ check_pass "Rust version compatible"
echo ""
echo "→ Checking for uncommitted changes..."
if ! git diff --quiet || ! git diff --cached --quiet; then
if jj status 2>&1 | grep -q "Working copy changes:"; then
check_warn "Uncommitted changes found - you'll need to commit before publishing"
else
check_pass "No uncommitted changes"
@@ -131,13 +136,16 @@ grep -q "# dagx" README.md || check_fail "README.md title incorrect"
grep -q "https://crates.io/crates/dagx" README.md || check_fail "README.md crates.io badge incorrect"
grep -q "https://docs.rs/dagx" README.md || check_fail "README.md docs.rs badge incorrect"
grep -q "https://github.com/swaits/dagx" README.md || check_fail "README.md GitHub URL incorrect"
# Extract major.minor version (e.g., "0.2.1" -> "0.2")
MAJOR_MINOR=$(echo "$VERSION" | cut -d'.' -f1-2)
grep -q "dagx = \"$MAJOR_MINOR\"" README.md || check_fail "README.md dependency version not \"$MAJOR_MINOR\" (found: $(grep 'dagx = ' README.md | head -1))"
check_pass "README.md looks perfect"
echo ""
echo "→ Verifying CHANGELOG.md..."
[ -f CHANGELOG.md ] || check_fail "CHANGELOG.md missing"
grep -q "0.1.0" CHANGELOG.md || check_fail "CHANGELOG.md doesn't mention v0.1.0"
check_pass "CHANGELOG.md exists and mentions v0.1.0"
grep -q "$VERSION" CHANGELOG.md || check_fail "CHANGELOG.md doesn't mention v$VERSION"
check_pass "CHANGELOG.md exists and mentions v$VERSION"
echo ""
echo "→ Verifying LICENSE..."
@@ -182,7 +190,7 @@ section "CARGO.TOML METADATA"
echo "→ Checking dagx Cargo.toml..."
grep -q 'name = "dagx"' Cargo.toml || check_fail "Package name incorrect"
grep -q 'version = "0.1.0"' Cargo.toml || check_fail "Version not 0.1.0"
grep -q "version = \"$VERSION\"" Cargo.toml || check_fail "Version not $VERSION"
grep -q 'edition = "2021"' Cargo.toml || check_fail "Edition not 2021"
grep -q 'license = "MIT"' Cargo.toml || check_fail "License not MIT"
grep -q 'description = "A minimal, type-safe' Cargo.toml || check_fail "Description missing"
@@ -197,7 +205,7 @@ check_pass "dagx Cargo.toml metadata complete and correct"
echo ""
echo "→ Checking dagx-macros Cargo.toml..."
grep -q 'name = "dagx-macros"' dagx-macros/Cargo.toml || check_fail "dagx-macros name incorrect"
grep -q 'version = "0.1.0"' dagx-macros/Cargo.toml || check_fail "dagx-macros version not 0.1.0"
grep -q "version = \"$VERSION\"" dagx-macros/Cargo.toml || check_fail "dagx-macros version not $VERSION"
grep -q 'edition = "2021"' dagx-macros/Cargo.toml || check_fail "dagx-macros edition not 2021"
grep -q 'license = "MIT"' dagx-macros/Cargo.toml || check_fail "dagx-macros license not MIT"
grep -q 'description = "Procedural macros for dagx"' dagx-macros/Cargo.toml || check_fail "dagx-macros description missing"
@@ -230,8 +238,8 @@ check_pass "dagx-macros is ready to publish"
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 "no matching package named \`dagx-macros\`"; then
check_warn "dagx package check shows dependency on unpublished dagx-macros (expected)"
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)"
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)"
@@ -284,16 +292,15 @@ echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━
echo -e "${GREEN}✓ ALL CHECKS PASSED!${NC}"
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
echo "dagx v0.1.0 is READY FOR RELEASE!"
echo "dagx v$VERSION is READY FOR RELEASE!"
echo ""
echo -e "${YELLOW}PUBLICATION INSTRUCTIONS:${NC}"
echo ""
echo "1. Commit any uncommitted changes:"
echo " git add ."
echo " git commit -m 'Release v0.1.0'"
echo " jj commit -m 'chore: release v$VERSION'"
echo ""
echo "2. Tag the release:"
echo " git tag -a v0.1.0 -m 'Release v0.1.0'"
echo "2. Create a bookmark for the release:"
echo " jj bookmark create v$VERSION"
echo ""
echo "3. Publish dagx-macros FIRST:"
echo " cd dagx-macros"
@@ -303,9 +310,8 @@ echo "4. Then publish dagx:"
echo " cd .."
echo " cargo publish"
echo ""
echo "5. Push changes and tag:"
echo " git push origin main"
echo " git push origin v0.1.0"
echo "5. Push changes and bookmark:"
echo " jj git push"
echo ""
echo -e "${BLUE}Note: dagx-macros MUST be published before dagx${NC}"
echo -e "${BLUE}because dagx depends on dagx-macros from crates.io${NC}"
+2
View File
@@ -11,6 +11,7 @@ struct TestTask {
#[crate::task]
impl TestTask {
#[cfg(not(tarpaulin_include))]
async fn run(&self) -> i32 {
self.value
}
@@ -20,6 +21,7 @@ struct TestTaskWithInput;
#[crate::task]
impl TestTaskWithInput {
#[cfg(not(tarpaulin_include))]
async fn run(input: &i32) -> i32 {
input * 2
}
+18
View File
@@ -12,6 +12,7 @@ struct TestTask {
#[crate::task]
impl TestTask {
#[cfg(not(tarpaulin_include))]
async fn run(&self) -> i32 {
self.value
}
@@ -22,6 +23,7 @@ struct TestTaskWithInput;
#[crate::task]
impl TestTaskWithInput {
#[allow(dead_code)]
#[cfg(not(tarpaulin_include))]
async fn run(input: &i32) -> i32 {
input * 2
}
@@ -196,3 +198,19 @@ fn test_deps_tuple_large_tuples() {
assert_eq!(*node_id, NodeId(i));
}
}
#[test]
fn test_deps_handle_ref_explicit() {
// Test explicit &TaskHandle DepsTuple impl (line 27-28 in deps.rs)
let handle: TaskHandle<i32> = TaskHandle {
id: NodeId(77),
_phantom: std::marker::PhantomData,
};
// Create a reference and explicitly call to_node_ids
let handle_ref = &handle;
let node_ids = DepsTuple::<i32>::to_node_ids(handle_ref);
assert_eq!(node_ids.len(), 1);
assert_eq!(node_ids[0], NodeId(77));
}
+116
View File
@@ -17,6 +17,7 @@ struct TestTask {
#[crate::task]
#[allow(dead_code)]
impl TestTask {
#[cfg(not(tarpaulin_include))]
async fn run(&self) -> i32 {
self.value
}
@@ -28,6 +29,7 @@ struct TestTaskWithInput;
#[crate::task]
#[allow(dead_code)]
impl TestTaskWithInput {
#[cfg(not(tarpaulin_include))]
async fn run(input: &i32) -> i32 {
input * 2
}
@@ -292,3 +294,117 @@ async fn test_extract_arc_type() {
assert!(result.is_ok());
assert_eq!(*result.unwrap(), *test_arc_clone);
}
// Error path tests for ExtractInput implementations
use std::collections::HashMap;
#[tokio::test]
async fn test_hashmap_extract_wrong_dependency_count() {
let deps = vec![
Box::new(oneshot::channel::<Arc<HashMap<String, i32>>>().1) as Box<dyn Any + Send>,
Box::new(oneshot::channel::<Arc<HashMap<String, i32>>>().1) as Box<dyn Any + Send>,
];
let result = HashMap::<String, i32>::extract_from_channels(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Expected 1 dependency, got 2"));
}
#[tokio::test]
async fn test_hashmap_extract_type_mismatch() {
let (tx, rx) = oneshot::channel::<Arc<String>>();
tokio::spawn(async move {
let _ = tx.send(Arc::new("wrong type".to_string()));
});
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = HashMap::<String, i32>::extract_from_channels(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Type mismatch"));
}
#[tokio::test]
async fn test_hashmap_extract_channel_closed() {
let (tx, rx) = oneshot::channel::<Arc<HashMap<String, i32>>>();
drop(tx);
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = HashMap::<String, i32>::extract_from_channels(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Channel closed"));
}
#[tokio::test]
async fn test_result_extract_downcast_failure() {
let (tx, rx) = oneshot::channel::<Arc<String>>();
tokio::spawn(async move {
let _ = tx.send(Arc::new("wrong".to_string()));
});
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Result::<i32, String>::extract_from_channels(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Type mismatch"));
}
#[tokio::test]
async fn test_option_extract_downcast_failure() {
let (tx, rx) = oneshot::channel::<Arc<String>>();
tokio::spawn(async move {
let _ = tx.send(Arc::new("wrong".to_string()));
});
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Option::<i32>::extract_from_channels(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Type mismatch"));
}
#[tokio::test]
async fn test_vec_extract_downcast_failure() {
let (tx, rx) = oneshot::channel::<Arc<String>>();
tokio::spawn(async move {
let _ = tx.send(Arc::new("wrong".to_string()));
});
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Vec::<i32>::extract_from_channels(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Type mismatch"));
}
#[tokio::test]
async fn test_arc_extract_wrong_dependency_count() {
let deps = vec![];
let result = Arc::<i32>::extract_from_channels(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Expected 1 dependency, got 0"));
}
#[tokio::test]
async fn test_arc_extract_downcast_failure() {
let (tx, rx) = oneshot::channel::<Arc<String>>();
tokio::spawn(async move {
let _ = tx.send(Arc::new("wrong".to_string()));
});
let deps = vec![Box::new(rx) as Box<dyn Any + Send>];
let result = Arc::<i32>::extract_from_channels(deps).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Type mismatch"));
}
#[tokio::test]
async fn test_tuple_wrong_dependency_count() {
let deps = vec![
Box::new(oneshot::channel::<Arc<i32>>().1) as Box<dyn Any + Send>,
Box::new(oneshot::channel::<Arc<i32>>().1) as Box<dyn Any + Send>,
Box::new(oneshot::channel::<Arc<i32>>().1) as Box<dyn Any + Send>,
];
let result = <(i32, i32)>::extract_from_channels(deps).await;
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("Expected 2 dependencies, got 3"));
}
+18 -14
View File
@@ -115,8 +115,14 @@ async fn test_concurrent_run_protection() {
let dag1 = Arc::clone(&dag);
let dag2 = Arc::clone(&dag);
// Use a barrier to ensure both runs start at exactly the same time
let barrier = Arc::new(tokio::sync::Barrier::new(2));
let barrier1 = barrier.clone();
let barrier2 = barrier.clone();
// Start two runs concurrently
let handle1 = tokio::spawn(async move {
barrier1.wait().await;
dag1.run(|fut| {
tokio::spawn(fut);
})
@@ -124,20 +130,27 @@ async fn test_concurrent_run_protection() {
});
let handle2 = tokio::spawn(async move {
// Give the first run a head start
sleep(Duration::from_millis(10)).await;
barrier2.wait().await;
dag2.run(|fut| {
tokio::spawn(fut);
})
.await
});
// First should succeed, second should fail (concurrent execution not supported)
// One should succeed, one should fail (concurrent execution not supported)
let result1 = handle1.await.unwrap();
let result2 = handle2.await.unwrap();
assert!(result1.is_ok());
assert!(result2.is_err()); // Concurrent run() returns error
// Exactly one should be ok, one should be err
assert!(result1.is_ok() != result2.is_ok());
// The error should be about concurrent execution
if result1.is_err() {
matches!(result1.unwrap_err(), crate::error::DagError::CycleDetected { description, .. } if description.contains("already running"));
}
if result2.is_err() {
matches!(result2.unwrap_err(), crate::error::DagError::CycleDetected { description, .. } if description.contains("already running"));
}
}
#[test]
@@ -179,15 +192,6 @@ async fn test_multiple_get_calls() {
assert_eq!(result3, 100);
}
struct TaskWithDep;
#[crate::task]
impl TaskWithDep {
#[allow(dead_code)]
async fn run(input: &i32) -> i32 {
input * 2
}
}
#[tokio::test]
async fn test_invalid_node_id_in_get() {
// Test getting with an invalid node ID
+12 -2
View File
@@ -100,8 +100,6 @@ async fn test_custom_task_implementation() {
fn assert_task_bounds<T: Task>() {
// This function just checks that T implements Task with the right bounds
fn assert_send<T: Send>() {}
#[allow(dead_code)]
fn assert_send_future<F: Future + Send>(_: &F) {}
// These should compile if bounds are correct
assert_send::<T>();
@@ -153,3 +151,15 @@ async fn test_task_fn_with_option_type() {
assert_eq!(task.run(5).await, Some(10));
}
#[tokio::test]
async fn test_task_fn_explicit_trait_call() {
// Test explicit Task trait method call to cover line 121
use crate::task::Task;
let task = task_fn(|x: i32| async move { x * 3 });
// Explicitly call the Task trait's run method
let result = Task::run(task, 10).await;
assert_eq!(result, 30);
}
+52
View File
@@ -1,5 +1,6 @@
//! Unit tests for types module
use crate::task::Task;
use crate::types::{NodeId, TaskHandle};
use std::marker::PhantomData;
@@ -96,3 +97,54 @@ fn test_task_handle_with_different_types() {
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<i32> = 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<TaskBuilder> 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<i32> = builder.into();
assert_eq!(handle.id, builder_id);
}
#[test]
fn test_task_handle_ref_from_conversion() {
// Test From<&TaskHandle<T>> for TaskHandle<T> (line 90-93)
let handle: TaskHandle<String> = TaskHandle {
id: NodeId(123),
_phantom: PhantomData,
};
let handle_ref = &handle;
let converted: TaskHandle<String> = handle_ref.into();
assert_eq!(converted.id.0, 123);
assert_eq!(handle.id, converted.id);
}
+2 -1
View File
@@ -1091,7 +1091,8 @@ async fn test_arc_mixed_with_non_arc() {
// ============================================================================
/// Test 16: Arc in parallel execution paths
#[tokio::test]
#[tokio::test(flavor = "multi_thread")]
#[cfg_attr(tarpaulin, ignore)] // Timing tests are unreliable with instrumentation overhead
async fn test_arc_parallel_execution() {
struct SharedProducer;
+1 -1
View File
@@ -304,7 +304,7 @@ async fn test_zigzag_dependencies() -> DagResult<()> {
}
#[tokio::test]
#[ignore] // This test is extremely resource intensive
#[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();
+1 -1
View File
@@ -213,7 +213,7 @@ async fn test_10000_node_linear_chain_segments() -> DagResult<()> {
}
#[tokio::test]
#[ignore] // This test is very resource intensive
#[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();
+185
View File
@@ -162,3 +162,188 @@ async fn test_multiple_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 = dag.add_task(task_fn(|_: ()| async { 10 }));
let t2 = dag
.add_task(task_fn(|x: i32| async move { x * 2 }))
.depends_on(&t1);
let t3 = dag
.add_task(task_fn(|x: i32| async move { x + 5 }))
.depends_on(t2);
dag.run(|fut| {
tokio::spawn(fut);
})
.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 = dag.add_task(task_fn(|_: ()| async { 100 }));
// Create multiple tasks in the same layer (all depend on source)
let t1 = dag
.add_task(task_fn(|x: i32| async move { x + 1 }))
.depends_on(&source);
let t2 = dag
.add_task(task_fn(|x: i32| async move { x + 2 }))
.depends_on(&source);
let t3 = dag
.add_task(task_fn(|x: i32| async move { x + 3 }))
.depends_on(&source);
dag.run(|fut| {
tokio::spawn(fut);
})
.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 = dag.add_task(task_fn(|_: ()| async { vec![1, 2, 3, 4, 5] }));
// Consumer
let consumer = dag
.add_task(task_fn(
|data: Vec<i32>| async move { data.iter().sum::<i32>() },
))
.depends_on(&producer);
dag.run(|fut| {
tokio::spawn(fut);
})
.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 (channel creation)
let dag = DagRunner::new();
let producer = dag.add_task(task_fn(|_: ()| async { 42 }));
// Multiple consumers
let c1 = dag
.add_task(task_fn(|x: i32| async move { x * 2 }))
.depends_on(&producer);
let c2 = dag
.add_task(task_fn(|x: i32| async move { x + 10 }))
.depends_on(&producer);
let c3 = dag
.add_task(task_fn(|x: i32| async move { x - 5 }))
.depends_on(&producer);
dag.run(|fut| {
tokio::spawn(fut);
})
.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 = dag.add_task(task_fn(|_: ()| async { 1 }));
let left = dag
.add_task(task_fn(|x: i32| async move { x + 1 }))
.depends_on(&source);
let right = dag
.add_task(task_fn(|x: i32| async move { x + 2 }))
.depends_on(&source);
let sink = dag
.add_task(task_fn(|(l, r): (i32, i32)| async move { l + r }))
.depends_on((&left, &right));
dag.run(|fut| {
tokio::spawn(fut);
})
.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();
async move {
order.lock().push(1);
1
}
}));
let order2 = execution_order.clone();
let t2 = dag
.add_task(task_fn(move |x: i32| {
let order = order2.clone();
async move {
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();
async move {
order.lock().push(3);
x + 1
}
}))
.depends_on(t2);
dag.run(|fut| {
tokio::spawn(fut);
})
.await
.unwrap();
let order = execution_order.lock().clone();
assert_eq!(order, vec![1, 2, 3]);
assert_eq!(dag.get(t3).unwrap(), 3);
}